# V3 API — Surveys (hand-maintained; not generated by generate-api-specs). # Implementation: apps/web/app/api/v3/surveys/route.ts and apps/web/app/api/v3/surveys/[surveyId]/route.ts # See apps/web/app/api/v3/README.md and docs/Survey-Server-Actions.md (Part III) for full context. openapi: 3.1.0 info: title: Formbricks API v3 description: | **GET /api/v3/surveys** and **DELETE /api/v3/surveys/{surveyId}** — authenticate with **session cookie** or **`x-api-key`** (management key with access to the workspace). **Spec location:** `docs/api-v3-reference/openapi.yml` (alongside v2 at `docs/api-v2-reference/openapi.yml`). **workspaceId** Query param `workspaceId` is the canonical container identifier for this API. **Deprecated compatibility:** the older `environmentId` identifier is still accepted for compatibility but should no longer be used in new integrations. **Auth** Authenticate with either a session cookie or **`x-api-key`**. In dual-auth mode, V3 checks the API key first when the header is present, otherwise it uses the session path. Unauthenticated callers get **401** before query validation. **Pagination** Cursor-based pagination with **limit** + opaque **cursor** token. Responses return `meta.nextCursor`; pass that value back as `cursor` to fetch the next page. Responses also include `meta.totalCount`, the total number of surveys matching the current filters across all pages. There is no `offset` in this contract. **Filtering** Filters use explicit operator-style query parameters under the **`filter[...]` family**. This endpoint supports `filter[name][contains]`, `filter[status][in]`, and `filter[type][in]`. Multi-value filters use repeated keys or comma-separated values (e.g. `filter[status][in]=draft&filter[status][in]=inProgress` or `filter[status][in]=draft,inProgress`). Sorting remains a flat `sortBy` query parameter. **Security** Missing/forbidden workspace returns **403** with a generic message (not **404**) so resource existence is not leaked. List responses use `private, no-store`. **OpenAPI** This YAML is **not** produced by `pnpm generate-api-specs` (that script only builds v2 → `docs/api-v2-reference/openapi.yml`). Update this file when the route contract changes. **Overview migration note** The v3-backed survey overview page intentionally removes actions that are not yet exposed by this contract: `Created by` filtering, `Duplicate`, `Copy...`, `Preview`, and `Copy link`. **Next steps (out of scope for this spec)** Additional v3 survey endpoints, optional ETag/304, field selection — see Survey-Server-Actions.md Part III. version: 0.1.0 x-implementation-notes: route: apps/web/app/api/v3/surveys/route.ts query-parser: apps/web/app/api/v3/surveys/parse-v3-surveys-list-query.ts auth: apps/web/app/api/v3/lib/auth.ts workspace-resolution: apps/web/app/api/v3/lib/workspace-context.ts openapi-generated: false pagination-model: cursor cursor-pagination: supported paths: /api/v3/surveys: get: operationId: getSurveysV3 summary: List surveys description: | Returns surveys for the workspace. Session cookie or x-api-key. Note: Environments are deprecated. Use workspace/workspaceId terminology. tags: - V3 Surveys parameters: - in: query name: workspaceId required: true schema: type: string format: cuid2 description: | Workspace identifier. This is the canonical container ID for v3 APIs. - in: query name: environmentId required: false deprecated: true schema: type: string format: cuid2 description: | Deprecated: use `workspaceId`. This alias is retained for backward compatibility. - in: query name: limit schema: type: integer minimum: 1 maximum: 100 default: 20 description: Page size (max 100) - in: query name: cursor schema: type: string description: | Opaque cursor returned as `meta.nextCursor` from the previous page. Omit on the first request. - in: query name: filter[name][contains] schema: type: string maxLength: 512 description: Case-insensitive substring match on survey name (same as in-app list filters). - in: query name: filter[status][in] schema: type: array items: type: string enum: [draft, inProgress, paused, completed] style: form explode: true description: | Survey status filter. Repeat the parameter (`filter[status][in]=draft&filter[status][in]=inProgress`) or use comma-separated values (`filter[status][in]=draft,inProgress`). Invalid values → **400**. - in: query name: filter[type][in] schema: type: array items: type: string enum: [link, app] style: form explode: true description: Survey type filter (`link` / `app`). Same repeat-or-comma rules as `filter[status][in]`. - in: query name: sortBy schema: type: string enum: [createdAt, updatedAt, name, relevance] description: Sort order. Defaults to `updatedAt`. The `cursor` token is bound to the selected sort order. responses: "200": description: Surveys retrieved successfully headers: X-Request-Id: schema: { type: string } description: Request correlation ID Cache-Control: schema: { type: string } example: "private, no-store" content: application/json: schema: type: object required: [data, meta] properties: data: type: array items: $ref: "#/components/schemas/SurveyListItem" meta: type: object required: [limit, nextCursor, totalCount] properties: limit: { type: integer } nextCursor: type: string nullable: true description: Opaque cursor for the next page. `null` when there are no more results. totalCount: type: integer minimum: 0 description: Total number of surveys matching the current filters across all pages. "400": description: Bad Request content: application/problem+json: schema: $ref: "#/components/schemas/Problem" "401": description: Not authenticated (no valid session or API key) content: application/problem+json: schema: $ref: "#/components/schemas/Problem" "403": description: Forbidden — no access, or workspace does not exist (404 not used; avoids existence leak) content: application/problem+json: schema: $ref: "#/components/schemas/Problem" "429": description: Rate limit exceeded headers: Retry-After: schema: { type: integer } description: Seconds until the current rate-limit window resets content: application/problem+json: schema: $ref: "#/components/schemas/Problem" "500": description: Internal Server Error content: application/problem+json: schema: $ref: "#/components/schemas/Problem" security: - sessionAuth: [] - apiKeyAuth: [] /api/v3/surveys/{surveyId}: delete: operationId: deleteSurveyV3 summary: Delete a survey description: Deletes a survey by id. Session cookie or x-api-key. tags: - V3 Surveys parameters: - in: path name: surveyId required: true schema: type: string format: cuid2 description: Survey identifier. responses: "200": description: Survey deleted successfully headers: X-Request-Id: schema: { type: string } description: Request correlation ID Cache-Control: schema: { type: string } example: "private, no-store" content: application/json: schema: $ref: "#/components/schemas/SurveyDeleteResponse" "400": description: Bad Request content: application/problem+json: schema: $ref: "#/components/schemas/Problem" "401": description: Not authenticated (no valid session or API key) content: application/problem+json: schema: $ref: "#/components/schemas/Problem" "403": description: Forbidden — no access, or survey does not exist (404 not used; avoids existence leak) content: application/problem+json: schema: $ref: "#/components/schemas/Problem" "429": description: Rate limit exceeded headers: Retry-After: schema: { type: integer } description: Seconds until the current rate-limit window resets content: application/problem+json: schema: $ref: "#/components/schemas/Problem" "500": description: Internal Server Error content: application/problem+json: schema: $ref: "#/components/schemas/Problem" security: - sessionAuth: [] - apiKeyAuth: [] components: securitySchemes: sessionAuth: type: apiKey in: cookie name: next-auth.session-token description: | NextAuth session JWT cookie. **Development:** often `next-auth.session-token`. **Production (HTTPS):** often `__Secure-next-auth.session-token`. Send the cookie your browser receives after sign-in. apiKeyAuth: type: apiKey in: header name: x-api-key description: | Management API key; must include **workspaceId** as an allowed workspace with read, write, or manage permission. schemas: SurveyListItem: type: object description: | Shape returned by `GET /api/v3/surveys`. Serialized dates are ISO 8601 strings. The v3 overview contract intentionally omits deprecated `environmentId` and internal fields such as `_count`. Legacy DB rows may include survey **type** values `website` or `web` (see Prisma); filter **type** only accepts `link` | `app`. properties: id: { type: string } name: { type: string } workspaceId: { type: string } type: { type: string, enum: [link, app, website, web] } status: type: string enum: [draft, inProgress, paused, completed] createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } responseCount: { type: integer } creator: { type: object, nullable: true, properties: { name: { type: string } } } singleUse: type: object nullable: true properties: enabled: { type: boolean } isEncrypted: { type: boolean } SurveyDeleteResponse: type: object required: [data] properties: data: type: object required: [id] properties: id: { type: string } Problem: type: object description: RFC 9457 Problem Details for HTTP APIs (`application/problem+json`). Responses typically include a machine-readable `code` field alongside `title`, `status`, `detail`, and `requestId`. required: [title, status, detail, requestId] properties: type: { type: string, format: uri } title: { type: string } status: { type: integer } detail: { type: string } instance: { type: string } code: type: string enum: [bad_request, not_authenticated, forbidden, internal_server_error, too_many_requests] requestId: { type: string } details: { type: object } invalid_params: type: array items: type: object properties: name: { type: string } reason: { type: string }