> ## Documentation Index
> Fetch the complete documentation index at: https://formbricks.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a response

> Records a survey response. Requires read-write access to the workspace, which is resolved from `surveyId` rather than supplied by the caller.

**This performs a real submission.** It runs the full response pipeline — webhooks, integrations, follow-ups, quota evaluation, and Hub ingestion once the response is finished — and it consumes one metered monthly response. That is the same power a v1/v2 management create has today, and the same power an API key with write access already carries; it is called out because an agent holding that key can create genuine submissions. Use `POST /api/v3/responses/validate` to check a payload without any of that happening.

`createdAt` cannot be supplied. Bulk historical loading is therefore not what this endpoint is for — see `ResponseListItem.createdAt` for why, and use v2 while a dedicated import path is outstanding.




## OpenAPI

````yaml /api-v3-reference/openapi.yml post /api/v3/responses
openapi: 3.1.1
info:
  title: Formbricks API v3
  description: >
    **Surveys**: **GET /api/v3/surveys**, **POST /api/v3/surveys**, **POST
    /api/v3/surveys/generate**, **POST /api/v3/surveys/validate**, **GET
    /api/v3/surveys/{surveyId}**, **PATCH /api/v3/surveys/{surveyId}**, **PATCH
    /api/v3/surveys/{surveyId}/blocks**, **PUT
    /api/v3/surveys/{surveyId}/blocks/order**, and **DELETE
    /api/v3/surveys/{surveyId}**.


    **Custom CSS**: survey CSS through `customCss` on **POST /api/v3/surveys**,
    **GET** and **PATCH /api/v3/surveys/{surveyId}**, workspace CSS through
    **GET** and **PATCH /api/v3/workspaces/{workspaceId}/custom-css**, and
    previews through `operation: customCss` on **POST
    /api/v3/surveys/validate**. Writes take and reads return source only; the
    server compiles it, returns removed constructs as top-level `warnings`, and
    never accepts compiled output.


    **Responses**: **GET /api/v3/responses**, **GET /api/v3/responses/count**,
    **POST /api/v3/responses/validate**, **POST
    /api/v3/responses/batch-delete**, **GET /api/v3/responses/{responseId}**,
    **POST /api/v3/responses**, **PATCH /api/v3/responses/{responseId}**, and
    **DELETE /api/v3/responses/{responseId}**.


    **Workflows extension**: **GET /api/v3/workflows**, **POST
    /api/v3/workflows**, **GET /api/v3/workflows/{workflowId}**, **PATCH
    /api/v3/workflows/{workflowId}**, **DELETE /api/v3/workflows/{workflowId}**,
    **POST /api/v3/workflows/{workflowId}/duplicate**, **POST
    /api/v3/workflows/{workflowId}/enable**, **POST
    /api/v3/workflows/{workflowId}/disable**, **POST
    /api/v3/workflows/{workflowId}/archive**, **POST
    /api/v3/workflows/{workflowId}/unarchive**, **POST
    /api/v3/workflows/{workflowId}/test**, **GET /api/v3/workflows/runs**, and
    **GET /api/v3/workflows/runs/{runId}**.


    Authenticate all management endpoints with **session cookie** or
    **`x-api-key`** (management key with access to the workspace).


    **Spec location:** source of truth is the multi-file tree at
    `docs/api-v3-reference/src/` (root `openapi.yml` plus one file per path and
    component); `docs/api-v3-reference/openapi.yml` is the generated single-file
    bundle (alongside v2 at `docs/api-v2-reference/openapi.yml`).


    **workspaceId**

    `workspaceId` is the canonical container identifier for this API. It is a
    **required query parameter on collection read endpoints** (`GET
    /api/v3/surveys`, `GET /api/v3/action-classes`, `GET
    /api/v3/contact-attribute-keys`) because those operate within a workspace.
    **`POST /api/v3/surveys` instead requires `workspaceId` in the request
    body**, not as a query parameter. Single-resource endpoints
    (`GET`/`PATCH`/`DELETE /api/v3/surveys/{surveyId}`) locate the resource by
    its globally-unique id and resolve the workspace from it, so they do **not**
    accept a `workspaceId` query parameter — sending one returns **400** with an
    `invalid_params` entry naming the key itself (`name: "workspaceId"`,
    `reason: "Unsupported field 'workspaceId'"`), rather than one naming the
    body.


    **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. Survey list responses also include `meta.totalCount`, the total number
    of surveys matching the current filters across all pages. Workflow list
    responses do not require total counts in Scope 1. There is no `offset` in
    this contract.

    The page-size **maximum is 250** for responses, surveys, action classes and
    contact attribute keys; only the *default* varies by resource (20 for
    surveys, 50 for the workspace reference lists). **The workflows and
    workflow-run lists remain at 100** — their bound lives in the
    `packages/workflows` contracts and moves with that package, so each
    documents the value it actually enforces. A `limit` above an endpoint's
    maximum is rejected with **400** rather than silently reduced, consistent
    with v3 rejecting unrecognized query parameters instead of ignoring them.
    Callers should not infer the end of a collection from a page shorter than
    `limit`; `meta.nextCursor` of `null` is the only end-of-collection signal.
    Note that array-valued *input* bounds are a separate concern and stay at 100
    — `filter[id][in]` and the batch-delete id list are bounded by write and
    payload risk, not by page cost.


    **Strictness**

    A query parameter that an operation does not declare returns **400**, with
    the offending key reported in `invalid_params[].name` — including on
    operations that declare no query parameters at all. Unknown parameters are
    rejected rather than ignored on purpose: a silently-dropped filter returns
    *more* data than the caller believes they asked for, and a typo in a filter
    name is indistinguishable from no filter at all. The same applies to request
    bodies, where unsupported fields are rejected instead of stripped.

    Unrecognized **headers** are tolerated, as gateways and proxies legitimately
    add them; strictness is about parameters and body fields, not transport
    metadata.

    This is the rule for the surveys, responses, tags and workspace
    reference-list operations. The workflows family does not enforce it today,
    so treat parameter strictness there as unspecified rather than guaranteed.

    **Request limits**

    A request body may be at most **2 MB**; a larger one is refused with **413**
    before it is read. Inside a body, **no array may hold more than 1 000 items,
    and all arrays together may hold at most 50 000 elements**, at any depth — a
    body that exceeds either bound is refused with **400** and a single
    `invalid_params` entry naming the offending array, before any field is
    validated. The same holds for nesting: a body may nest at most **256**
    levels of objects and arrays, counted from the root, and a deeper one is
    refused the same way, naming where it went too deep. Fields with a tighter
    bound document it as `maxItems` on the field (a survey's `blocks`,
    `endings`, `variables`, `languages`, `hiddenFields.fieldIds` and
    `distribution.triggers` since 0.2.0). A 400 or 422 lists at most **50**
    `invalid_params` entries; when there are more, the last entry says how many
    were left out. Since 0.2.0.

    **Filtering**

    Filters use explicit operator-style query parameters under the
    **`filter[...]` family**. The survey list supports `filter[name][contains]`,
    `filter[status][in]`, and `filter[type][in]`. The workflow list supports
    `filter[name][contains]` and `filter[status][in]`; the workflow run list
    supports `filter[status][in]` and `filter[isDryRun][eq]`. Resource-scoping
    identifiers (`workspaceId`, `workflowId`, `responseId`) stay plain query
    parameters. 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.


    **Responses**

    The response resource is **self-describing**. Answers come back as a typed
    `answers[]` array in which each entry carries the element's id, label, type
    and value, rather than as a map keyed by element id — so a consumer holding
    only a response can read it without fetching the survey and joining. Hidden
    fields, survey variables and auto-captured context are one `embeddedData[]`
    collection rather than three parallel concepts, each entry declaring a
    `kind` and a `type`. Data whose element has since been deleted is reported
    in `unresolved[]` instead of being dropped silently. Labels are resolved at
    read time against the current survey definition, and the `resolution` object
    discloses that policy along with the survey's `updatedAt` so a consumer can
    detect wording drift.

    Writes take answers in the **stored map form** (`data`) while reads return
    the typed array. Resolving ids into labelled answers is unambiguous; the
    reverse would mean matching caller-supplied labels back onto option ids, so
    the write side keeps the stored shape. To stop that costing
    round-trippability, the **detailed read returns `data` unchanged** — so a
    single-response read can be edited and sent back as `PATCH { "data": … }`,
    and the typed `answers[]` array is an addition rather than a replacement.


    One case where that round trip does not close: a write bound applies to
    `data` on the way in (`ResponseDataMapInput`) and none applies to the stored
    map, because v1 and v2 wrote it without one. A response carrying more answer
    keys than the write accepts therefore cannot be corrected through `PATCH` at
    all — the whole map is rejected, and sending a smaller one is not a
    workaround, because `data` is replaced wholesale and every key left out is
    deleted. Correct those through the v1/v2 management API or the dashboard.
    This is reachable only on surveys with more elements than the bound allows.

    Embedded Data is the exception, and it is symmetric rather than
    round-tripped: hidden fields and variables are read and written through
    `embeddedData`, addressed by **name** in both directions. No raw map of them
    is echoed, because a name is already what a write accepts — publishing the
    storage keys underneath (a variable's is its cuid) would expose a layout the
    caller has no use for. `data` therefore carries element answers only.

    Of those two, only `data` carries no `readOnly`, because it is the same map
    on the way in and on the way out. Embedded Data is writable through the
    **request's own** `embeddedData` map, not through the response field of that
    name — so `embeddedData[]`, like every other derived view on the resource,
    is marked `readOnly`, per its meaning of server-managed. `ResponseResource`
    lists them all. The list view omits `data`, since a bulk reader is not
    editing. `POST /api/v3/responses/validate` exists so a caller can see what a
    map would produce, and what a write would do, before committing to it.

    `createdAt` and `updatedAt` are **server-owned** on this resource: they
    cannot be supplied on create or changed by a patch. Response metering counts
    rows by `(surveyId, createdAt)`, and a client-chosen `createdAt` also lets
    rows be inserted into the middle of another consumer's already-paginated
    stream. Backdating stays available on v2 until a dedicated import path
    exists. The respondent's IP address and the `contactAttributes` snapshot
    that v1/v2 expose are not part of this resource in any view; that snapshot
    is copied from the contact at submission time and never refreshed, so it is
    stale PII by construction.

    Response filtering is allow-listed and index-backed:
    `filter[createdAt][gte|gt|lte|lt]` (at most one lower and one upper bound —
    `gte` xor `gt`, `lte` xor `lt`), `filter[finished][eq]`,
    `filter[language][in]` and `filter[id][in]`. `filter[finished][eq]` and
    `filter[language][in]` require `surveyId` or `contactId` as well, because
    both are heap residuals bounded only by an anchored index range. Per-answer,
    hidden-field, submission-metadata, contact-attribute, quota and tag filters
    are **not** offered; each is a JSON path or an unindexed join on the largest
    table in the product. The in-app dashboard does offer them, so this API is
    knowingly behind the dashboard here — closing the gap requires index work
    and is tracked separately rather than quietly omitted.

    `GET /api/v3/responses/count` answers "how many match?" directly. The list
    endpoint's `meta.totalCount` covers a caller that is already paging and is
    **opt-in** via `includeTotalCount` (default `false`, unlike the survey list)
    because responses are the highest-cardinality table in the product. Both
    response counts are **capped at 10 000** and carry an exactness flag —
    `meta.totalCountRelation` on the list, `data.relation` on `/count` — reading
    `eq` when the figure is exact and `gte` when the cap was reached and it is a
    lower bound. An exact count over this table grows superlinearly with the
    data while a capped one stays flat, so an uncapped total would be the only
    part of a response whose cost is unbounded. `/count` accepts
    `precision=exact` for the cases that genuinely need a precise figure,
    documented as the deliberate slow path. The survey list's `totalCount` stays
    exact and uncapped: surveys are low-cardinality and that count has a
    covering index built for it.

    Bulk deletion is `POST /api/v3/responses/batch-delete`, a custom method
    rather than a collection-level `DELETE`, so a call that loses its filters
    cannot degrade into erasing everything in scope. It scope-filters rather
    than rejecting: foreign and already-deleted ids are ignored and the call
    reports how many rows it removed, which keeps it both leak-free and safe to
    retry.

    **Security**

    Missing/forbidden workspace returns **403** with a generic message (not
    **404**) so resource existence is not leaked. Workflow and workflow-run
    detail endpoints also return **403** for unknown IDs or inaccessible IDs to
    avoid IDOR leaks. List responses use `private, no-store`.


    **AI survey creation**

    `POST /api/v3/surveys/generate` returns a draft `POST /api/v3/surveys`
    payload plus validation metadata. It does not create a survey. It generates
    survey content for both `link` and `app` surveys; for `app` it seeds a
    default `distribution` (display once, no triggers, no targeting) that you
    finish configuring before publishing.

    Prompt text is sent to the configured AI provider for generation, but it is
    not stored by this endpoint, not logged by default, and not included in
    audit data.


    **Feedback Records** `/api/v3/feedbackRecords` is served by the **API
    gateway** (Envoy on Formbricks Cloud, Traefik in the one-click Docker
    install), which authorizes each call against the dataset and forwards it to
    the feedback store — not by the application routes that serve the rest of
    v3. Three consequences: the payload is the store's own, so members and
    filters are **`snake_case`** (`tenant_id`, `value_text`) rather than v3's
    camelCase; the dataset is named **`tenant_id`** on the wire and is required
    on every collection operation (it is the feedback dataset id, which the MCP
    `list_feedback_datasets` tool reports as `dataset_id`); and errors come in
    two shapes — gateway refusals (**400**, **401**, **403**, **413**, **503**)
    are `text/plain`, while the store's own problems are
    `application/problem+json` with a stable `code`. These eight operations are
    the whole of the gateway-served surface; any other path it receives under
    the prefix answers **400**. Authenticate with **`x-api-key`**: the key needs
    a permission on a workspace the dataset is assigned to — `read` to list, get
    and search, `write` to create and update, `manage` to delete — and may
    update or delete only in a dataset assigned to exactly one workspace.

    **OpenAPI**

    This spec is **not** produced by `pnpm generate-api-specs` (that script only
    builds v2 → `docs/api-v2-reference/openapi.yml`). When the route contract
    changes, edit the files under `docs/api-v3-reference/src/`, then run `pnpm
    api:v3:bundle` to regenerate the committed single-file bundle. `pnpm
    api:v3:lint` lints the source tree; `pnpm api:v3:check` (CI) fails when the
    bundle is stale.


    **Workflows Scope 1**

    Workflows are a follow-up extension of the existing v3 Survey API work. The
    Survey API establishes API-first survey authoring; Workflows build on that
    same v3 style to automate actions around survey responses through
    workspace-scoped JSON definitions. Scope 1 models the existing Follow-ups
    capability as workflows: one `response.completed` trigger with an optional
    `endingCardIds` filter, and `send_email` actions with Follow-up email field
    parity. The definition and run shapes mirror the shared Zod schemas in
    `packages/workflows` (ENG-1100), which are the implementation source of
    truth for this contract.


    Workflows use `draft`, `enabled`, and `disabled` lifecycle states plus an
    `archived` soft-delete state. Status only changes through the lifecycle
    endpoints (`enable`, `disable`, `archive`, `unarchive`); it is not writable
    via `POST` or `PATCH`. Enabling validates that the definition is executable
    and snapshots an immutable workflow version; runs reference that snapshot
    via `workflowVersionId`. `POST .../test` is a dry run: it validates the
    definition and resolves the trigger's references synchronously, creating no
    run and sending no email, and returns `{ workflowId, ok, problems }`
    directly. Real runs execute asynchronously and are polled via `GET
    /api/v3/workflows/runs/{runId}`.


    **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)**

    Optional ETag/304, field selection, translation-specific writes, and survey
    version history.
  version: 0.2.0
  x-implementation-notes:
    route: apps/web/app/api/v3/surveys/route.ts
    generate-route: apps/web/app/api/v3/surveys/generate/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
    workflow-contract-ticket: ENG-1101
    workflow-shared-schemas: packages/workflows/src/types (feat/workflows-types)
    workflow-prisma-schema: packages/database/schema/workflows.prisma (feat/workflows-prisma-schema)
    openapi-generated: false
    spec-source: docs/api-v3-reference/src (regenerate bundle with `pnpm api:v3:bundle`)
    responses-contract-ticket: ENG-2868
    responses-rfc: v3 Responses API RFC sections 2b (resource) and 2c (endpoints)
    responses-stored-shape: packages/types/responses.ts (ZResponse, ZResponseData, ZResponseInput)
    responses-prisma-model: packages/database/schema/main.prisma (model Response)
    pagination-model: cursor
    cursor-pagination: supported
servers:
  - url: https://app.formbricks.com
    description: >-
      Formbricks Cloud. Paths in this spec already include the `/api/v3` prefix;
      self-hosted instances substitute their own origin.
security: []
paths:
  /api/v3/responses:
    post:
      tags:
        - V3 Responses
      summary: Create a response
      description: >
        Records a survey response. Requires read-write access to the workspace,
        which is resolved from `surveyId` rather than supplied by the caller.


        **This performs a real submission.** It runs the full response pipeline
        — webhooks, integrations, follow-ups, quota evaluation, and Hub
        ingestion once the response is finished — and it consumes one metered
        monthly response. That is the same power a v1/v2 management create has
        today, and the same power an API key with write access already carries;
        it is called out because an agent holding that key can create genuine
        submissions. Use `POST /api/v3/responses/validate` to check a payload
        without any of that happening.


        `createdAt` cannot be supplied. Bulk historical loading is therefore not
        what this endpoint is for — see `ResponseListItem.createdAt` for why,
        and use v2 while a dedicated import path is outstanding.
      operationId: createResponseV3
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateResponseRequest'
            examples:
              finishedResponse:
                summary: A complete response with answers and Embedded Data
                value:
                  surveyId: clsv1234567890123456789012
                  finished: true
                  endingId: cmpthankyou00000000000000
                  language: de
                  data:
                    nps_score: 9
                    what_could_be_better: Die Suche ist zu langsam.
                    channels:
                      - Ein Freund
                      - Podcast
                  embeddedData:
                    plan: enterprise
              partialResponse:
                summary: A partial response, still open
                value:
                  surveyId: clsv1234567890123456789012
                  finished: false
                  data:
                    nps_score: 7
              integrationResponse:
                summary: >-
                  A response created by an integration, with timing, tags and
                  source
                description: >
                  `ttc` carries per-element timing in milliseconds and reads
                  back as `durationSeconds` in seconds. `meta.source` is worth
                  setting on any API-created response — nothing else can
                  distinguish it from an organic submission. `tags` are applied
                  in the same transaction, so no follow-up patch is needed.
                value:
                  surveyId: clsv1234567890123456789012
                  finished: true
                  language: de
                  data:
                    nps_score: 9
                    what_could_be_better: Die Suche ist zu langsam.
                  ttc:
                    nps_score: 12100
                    what_could_be_better: 64800
                  meta:
                    source: zendesk-integration
                    url: https://support.example.com/tickets/4821
                  tags:
                    - cltg1234567890123456789012
              identifiedResponse:
                summary: A response attributed to a known contact
                description: >
                  `contactId` is create-only — a response cannot later be
                  re-attributed to a different person. The contact must already
                  exist in the same workspace.
                value:
                  surveyId: clsv1234567890123456789012
                  finished: true
                  contactId: clct1234567890123456789012
                  data:
                    nps_score: 10
      responses:
        '201':
          description: Response created successfully
          headers:
            Location:
              schema:
                type: string
              description: Relative URL of the created response.
              example: /api/v3/responses/clrs1234567890123456789012
            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
                properties:
                  data:
                    $ref: '#/components/schemas/ResponseResource'
        '400':
          description: >
            Bad Request — the body failed schema validation: invalid JSON, a
            missing required field, a value of the wrong type, or an unsupported
            field. Sending a server-owned or create-forbidden field
            (`createdAt`, `updatedAt`, `contactAttributes`, `userId`, or a
            `meta` member outside the accepted `source` / `url` / `action` —
            notably `meta.ipAddress` and the derived `userAgent` / `country` /
            `device` family) is rejected here with `code: unsupported_field`
            rather than being silently dropped. Failures that need stored state
            to detect return **422** instead.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '401':
          $ref: '#/components/responses/V3Unauthorized'
        '403':
          $ref: '#/components/responses/V3Forbidden'
        '413':
          description: Payload Too Large — the request body exceeds the 2 MB limit.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          description: >
            Unprocessable Content — the body is well-formed but conflicts with
            stored state: `endingId` is not an ending of that survey, `language`
            is not enabled on it, `contactId` is not a contact in the same
            workspace, `displayId` does not exist for that survey or already
            backs another response, `singleUseId` is already used for this
            survey, a tag id does not resolve in the same workspace, an
            `embeddedData` name is not declared on the survey, is ambiguous, or
            is addressed twice by one payload, a file-upload answer references a
            file outside this survey's upload element, or an answer violates the
            survey's own validation rules. Each is itemized in `invalid_params`.


            **`surveyId` is not in that list, and answers 403 instead** —
            whether it names no survey at all or one in another workspace. Every
            other id here is checked *after* authorization, inside a workspace
            the caller already holds, so a distinct answer for each discloses
            nothing. `surveyId` is the scope anchor and is necessarily checked
            *before* authorization, so a separate "no such survey" would let any
            valid API key probe the id space of every other tenant. The two
            cases are therefore byte-identical, as they are on `GET`, `PATCH`
            and `DELETE` of a response id.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '429':
          $ref: '#/components/responses/V3TooManyRequests'
        '500':
          $ref: '#/components/responses/V3InternalServerError'
      security:
        - sessionAuth: []
        - apiKeyAuth: []
        - bearerAuth: []
components:
  schemas:
    CreateResponseRequest:
      type: object
      description: >
        Creates a survey response. **This is a real submission**: it runs the
        full response pipeline — webhooks, integrations, follow-ups, quota
        evaluation, and Hub ingestion once the response is finished — and it
        counts against metered monthly responses, exactly as a v1/v2 management
        create does today. It is not an import primitive; a dedicated import
        path is future work.


        The workspace is resolved from `surveyId`, so unlike `POST
        /api/v3/surveys` there is no `workspaceId` in the body — the caller
        cannot choose which scope it is authorized against.


        Unsupported fields are rejected rather than ignored. Absent by design,
        each for its own reason:


        - `createdAt` and `updatedAt` are **server-owned** — see
        `ResponseListItem.createdAt`. This is the
          one capability v3 does not carry over from v2, and it is why bulk historical loading still
          belongs on v2 until a dedicated import path exists.

        - `userId` is not accepted: a contact `userId` is a non-unique
        **attribute**, not a column, so it
          cannot safely identify who to attribute a response to. Link the contact explicitly with
          `contactId`.

        - `contactAttributes` is not accepted, and never was writable — v1/v2
        snapshot it from the linked
          contact. v3 does not expose the snapshot at all; `contact` covers the need.

        - `meta.ipAddress`, and the derived `userAgent` / `country` / `device`
        family, are not accepted;
          see `ResponseMetaInput` for what is.
      required:
        - surveyId
        - finished
        - data
      properties:
        surveyId:
          type: string
          format: cuid2
          description: The survey to record the response against. Immutable after creation.
        finished:
          type: boolean
          description: >-
            Whether this response is complete. Creating with `finished: true`
            fires the finish side of the pipeline immediately, including Hub
            ingestion.
        data:
          $ref: '#/components/schemas/ResponseDataMapInput'
        embeddedData:
          $ref: '#/components/schemas/ResponseEmbeddedDataInput'
        ttc:
          $ref: '#/components/schemas/ResponseTtcMap'
        meta:
          $ref: '#/components/schemas/ResponseMetaInput'
        tags:
          type: array
          uniqueItems: true
          maxItems: 100
          description: >-
            Tag ids to apply, so a tagged response can be created in one call
            rather than a create followed by a patch. Tags must already exist in
            the same workspace — create them via `POST /api/v3/tags`. Applied in
            the same transaction as the response.
          items:
            type: string
            format: cuid2
        endingId:
          type:
            - string
            - 'null'
          description: >-
            The ending card the respondent reached. Must exist in the survey
            definition.
        language:
          type:
            - string
            - 'null'
          description: >-
            Survey language code to stamp on the response. Must be a language
            the survey has enabled. Omit for single-language surveys.
        contactId:
          type: string
          format: cuid2
          description: >-
            Contact to attribute the response to. **Create-only** — a response
            cannot later be re-linked to a different contact. Must be a contact
            in the same workspace.
        displayId:
          type: string
          format: cuid2
          description: >-
            Display (survey impression) this response answers. Create-only. Each
            display can back at most one response.
        singleUseId:
          type: string
          minLength: 1
          maxLength: 255
          description: >-
            Single-use link id, for surveys configured with single-use links.
            Create-only, and unique per survey. Bounded because the value is
            indexed with `surveyId`: an empty string would slip past the
            uniqueness pre-check and still be stored, and an oversize one
            overflows the index entry.
      additionalProperties: false
    ResponseResource:
      allOf:
        - $ref: '#/components/schemas/ResponseBase'
        - type: object
          required:
            - contact
            - displayId
            - singleUseId
            - data
          properties:
            data:
              allOf:
                - $ref: '#/components/schemas/ResponseDataMap'
              description: >
                Answers in their **stored** form — byte-for-byte the same map
                `POST /api/v3/responses` and `PATCH
                /api/v3/responses/{responseId}` accept. Returned so that a read
                can be edited and written straight back: take this map, change
                what you need, and `PATCH` it. Without it a client wanting to
                correct one answer would have to rebuild the map from
                `answers[]`, which is possible but is work the server can simply
                not impose.


                `answers[]` remains the field to read — it is typed, labelled
                and ordered, and it separates answers from hidden fields. This
                is the same data in the shape the write side speaks, offered
                alongside rather than instead.


                Present on the detailed view only. The list view omits it
                deliberately: on a page of 250 it would carry every answer
                twice, and a bulk reader is not editing.
            contact:
              anyOf:
                - $ref: '#/components/schemas/ResponseContact'
                - type: 'null'
              description: >-
                The contact that submitted the response, or `null` when the
                response is anonymous. Present on the detailed view only.
            displayId:
              type:
                - string
                - 'null'
              description: >-
                The display (survey impression) this response was submitted
                against, when one was recorded.
            singleUseId:
              type:
                - string
                - 'null'
              description: >-
                The single-use link id this response was submitted through, when
                the survey uses single-use links.
      unevaluatedProperties: false
      description: >
        A survey response in the detailed view, as returned by `GET
        /api/v3/responses/{responseId}` and by the write endpoints. Everything
        in `ResponseListItem`, plus the identity and provenance fields the list
        view omits.


        It also carries `data` — the answers as stored, returned unchanged, so a
        client can read a response, edit an answer and send it straight back.
        Embedded Data is not echoed in stored form: the read's `embeddedData[]`
        and a request's `embeddedData` map already address the same fields by
        the same names, so a second raw map would only republish the storage
        layout. That is the property v1/v2 had for free by being symmetric, and
        the one OpenAPI 3.1.1 argues for directly: its "Validating `readOnly`
        and `writeOnly`" section notes that making clients strip fields before a
        write is "burdensome for clients, particularly when the JSON data is
        complex or deeply nested".


        They are not the *same* view, and the differences are deliberate. A
        write accepts `ingested` and `computed` fields only: a `reserved` entry
        describes the submission environment, and being able to read one back is
        not a licence to author it. An unset field is **omitted** from the read
        while `null` on a write **clears** one, because a merge-shaped map has
        no other way to spell deletion. And a name the read returns twice under
        different `kind`s cannot be written at all — see
        `ResponseEmbeddedDataInput`, which refuses an ambiguous name rather than
        guessing.


        Values themselves round-trip: within those two kinds every value the
        read emits is accepted verbatim on a write, and a `date` reads and
        writes as the same ISO-8601 string. So a read-edit-write cycle loses
        nothing except in two cases, both confined to rows v1 or v2 wrote. The
        first is the finite set of legacy surveys carrying one name as both a
        variable and a hidden field, where the write refuses the name outright.
        The second is a `data` map holding more keys than `ResponseDataMapInput`
        accepts: the stored map is unbounded and the write bound is not, so such
        a response cannot be patched here — and trimming it to fit is not a
        workaround, since `data` is replaced wholesale and an omitted key is a
        deletion.


        Note which fields carry `readOnly` here and which do not, because it is
        the opposite of what an "echo" framing would suggest. `readOnly` means a
        value is managed exclusively by the server, so it marks the **derived**
        views — `answers[]`, `embeddedData[]`, `unresolved[]`, `resolution`, the
        denormalized `surveyName` and `workspaceId`, the timestamps,
        `durationSeconds` and `id`. `data` is *not* read-only: it is exactly
        what the write endpoints accept, and leaving it unmarked is what makes
        this representation round-trippable rather than merely informative.


        To correct one answer, read this resource, take `data`, change the entry
        you want and `PATCH` `{ "data": … }`. Send only the fields you are
        changing — `PATCH` rejects unknown and non-patchable keys, so passing
        the whole representation back verbatim is not the intended flow; passing
        `data` back is.


        **Never exposed on this resource, in either view:** the respondent's IP
        address, and the `contactAttributes` snapshot that v1/v2 return. That
        snapshot is copied from the contact at submission time and never updated
        after, so it is stale PII by construction — `contact` covers the
        legitimate need to know who answered.
    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`.


        Branch on `code`, not on `detail` or `title`: `code` is stable and
        locale-independent, whereas `detail` is prose that may be reworded. No
        problem sets `type`, so per RFC 9457 §4.2.1 `title` is always the HTTP
        reason phrase for `status` and carries no information of its own.


        Member naming is frozen as published, mixed conventions included
        (`requestId` alongside `invalid_params` and `details.resource_type`):
        renaming any of them would break every client that reads them.
      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:
            - ai_features_not_enabled
            - ai_generated_payload_invalid
            - ai_instance_not_configured
            - ai_output_too_long
            - ai_provider_auth_failed
            - ai_smart_tools_disabled
            - bad_gateway
            - bad_request
            - conflict
            - custom_css_plan_required
            - forbidden
            - internal_server_error
            - invalid_workflow_state
            - not_authenticated
            - not_found
            - payload_too_large
            - projection_pending
            - service_unavailable
            - stored_survey_invalid
            - survey_not_workspace_visible
            - too_many_requests
            - unprocessable_content
            - visibility_blocked_by_connections
            - visibility_change_not_allowed
            - visibility_not_enabled
            - workflow_not_executable
            - workspace_survey_limit_reached
        requestId:
          type: string
        details:
          type: object
        invalid_params:
          type: array
          items:
            $ref: '#/components/schemas/InvalidParam'
    ResponseDataMapInput:
      allOf:
        - $ref: '#/components/schemas/ResponseDataMap'
        - type: object
          description: >
            The same map on the way in, with the cardinality bounds a v3 write
            enforces.


            At most 500 keys, and at most 1000 entries in a multi-select array
            or matrix object. Both sit far above any real survey — a response
            holds one entry per element, a multi-select answer one per choice —
            and exist because every later reader pays per entry: the
            serializer's per-answer mapping, export columns, and the positional
            probe the "Other" response filter emits. Exceeding either is a
            **400** from the request schema, not a 422.


            Deliberately separate from `ResponseDataMap`, which the read
            returns. These bounds are v3-only — the v1 and v2 write paths store
            the same map without them — so a stored response may exceed them,
            and the read schema must not claim otherwise.
          maxProperties: 500
          additionalProperties:
            if:
              type: array
            then:
              maxItems: 1000
            else:
              if:
                type: object
              then:
                maxProperties: 1000
    ResponseEmbeddedDataInput:
      type: object
      description: >
        Embedded Data values to write, **keyed by field name**.


        This is the write counterpart of `embeddedData[]` on the read, and it is
        keyed the way an author names a field rather than the way storage
        addresses it. A hidden field is stored under its name but a variable is
        stored under its cuid, and that split is an implementation detail of the
        storage layout — the server resolves a name to its storage key through
        the survey's Embedded Data links, so a caller never sees a cuid and
        never has to know which kind it is writing.


        **Which kinds are writable:**


        - `ingested` (hidden fields) — accepted, through the same ingest
        contract the SDK uses: the name
          must be declared on the survey, the value is coerced to the field's declared `type`, a `locked`
          field ignores the write, and the whole payload is bounded. A name the survey does not declare is
          a **422** rather than a silent drop — knowing whether a name is declared means loading the
          survey, and these operations route every failure that needs stored state to 422.

        - `computed` (variables) — accepted, because a management caller
        correcting a response needs to,
          and v1 and v2 both allow it today.

        - `reserved` — refused. Auto-captured context describes the submission
        environment, and a
          caller-supplied value would be fiction. The three exceptions are `source`, `url` and `action`,
          which `ResponseMetaInput` accepts for exactly the replay case.


        Values follow their field's declared type. A field the payload omits is
        left as it is; this is a merge over named fields, not a replacement of
        the collection.


        `null` **clears** a field. Omission and `null` therefore mean different
        things — leave it alone versus remove its value — which is the only
        reading that makes a merge-shaped map able to express deletion at all. A
        cleared field is then absent from `embeddedData[]` on the next read,
        exactly as one that was never written, and a field whose definition
        carries a `defaultValue` falls back to that default rather than
        disappearing.


        **An ambiguous name is refused.** The read says `key` is unique per
        `kind` rather than across the collection, because a survey predating the
        reserved-name guard can hold a variable and a hidden field under one
        name. This map has no `kind`, so on such a survey a name cannot say
        which field it means — and guessing would write the caller's value into
        the wrong one. A name that matches more than one declared field is a
        **422** naming it, for the same reason as an undeclared one; rename one
        side on the survey to clear it. The set is finite, frozen and tracked,
        and nothing new can enter it.


        Reserved names collide differently and are not ambiguous: `reserved` is
        not writable at all, so a name that matches both a declared field and a
        catalog entry resolves to the declared one.


        **At most 500 names.** Every name that matches no declared field becomes
        its own `invalid_params` entry naming it, so without a cap a large body
        answers with a 422 several times its own size. Far above any real
        survey; a v3-only bound, as on `data`.
      maxProperties: 500
      additionalProperties:
        oneOf:
          - type: string
          - type: number
          - type: boolean
          - type: 'null'
    ResponseTtcMap:
      type: object
      description: >
        Per-element time-to-complete, keyed by element id, in **milliseconds**.
        Accepted on create so that a caller replaying a real submission can
        carry its timing; reads report the same information as `durationSeconds`
        in seconds, on each answer and summed at the top level.


        This is client-reported telemetry, not a measurement the server can
        vouch for, so each value is clamped into `[0, 86400000]` (24 hours per
        element) rather than rejected — a noisy timing value should never cost
        you the response. Any finite *value* is therefore accepted, and none is
        declared out of range.


        The number of **entries** is capped at 500, matching `data`: a response
        holds one timing per element, so anything beyond that is not telemetry
        the server can use. A map with more entries is **rejected** —
        `maxProperties` below is enforced during request validation, so the
        whole write fails rather than the excess being trimmed or stored.


        **Do not send a `_total` key.** The server owns that bucket: it drops
        any `_total` you supply and writes its own when the response is
        finished, by summing the element entries. Sending one therefore changes
        nothing rather than skewing the stored total, and `durationSeconds` on
        the read is computed from the element entries regardless.


        **An omitted map is not the same as an empty one.** Omitting `ttc`
        stores no timings at all and the response reports no `durationSeconds`;
        sending `{}` on a finished response stores a `_total` of zero, which
        reads as "completed instantly".
      maxProperties: 500
      additionalProperties:
        type: number
    ResponseMetaInput:
      type: object
      description: >
        Submission context, on the narrow subset a caller can legitimately
        supply. A management-API create has no browser behind it, so unlike a
        client-side submission there is nothing for the server to auto-capture —
        if these are not sent they are simply absent, which is why the API
        accepts them at all rather than treating all context as server-derived.


        Deliberately **not** accepted here, because the routes derive them from
        the request itself and a caller-supplied value would be fiction:
        `country`, `userAgent` (the browser/OS/device breakdown is nested under
        it, there is no top-level `device` key) and `ipAddress`. `ipAddress` is
        additionally never projected by v3 on read, in any view.


        The stored `meta` is wider than this input. It also carries a block of
        browser-runtime context — `pagePath`, `pageReferrer`, the five `utm*`
        keys, the four screen/viewport dimensions, `timezone` and `locale` —
        which the SDK captures and which this endpoint does **not** currently
        accept, so sending one is a 400. That is a gap rather than a decision:
        an integration replaying a real submission can legitimately carry
        `utmSource` or `pagePath`, and those are live catalog entries that read
        back in `embeddedData[]`. Whether v3 create accepts them is open on the
        write ticket.


        Reads surface what is stored as `reserved` entries in `embeddedData[]`.
      properties:
        source:
          type: string
          maxLength: 512
          description: >-
            Where the submission came from — the integration or channel name.
            The field most worth setting on an API create, since nothing else
            can distinguish an imported or integration-created response from an
            organic one.
        url:
          type: string
          maxLength: 2048
          description: The page or context the response was collected from.
        action:
          type: string
          maxLength: 512
          description: >-
            The action or trigger that produced the submission, when one
            applies.
      additionalProperties: false
    ResponseBase:
      type: object
      description: >
        Fields shared by both response views. Self-describing: `answers[]`
        carries each element's id, label, type and value, so a consumer can read
        a response without also fetching the survey.


        Deliberately open — `ResponseListItem` and `ResponseResource` compose
        this and close themselves, so that the detailed view can add fields
        without the base rejecting them.
      required:
        - id
        - surveyId
        - surveyName
        - workspaceId
        - createdAt
        - updatedAt
        - finished
        - endingId
        - language
        - resolution
        - answers
        - embeddedData
        - unresolved
        - tags
      properties:
        id:
          readOnly: true
          type: string
          format: cuid2
          description: Response identifier.
        surveyId:
          type: string
          format: cuid2
          description: The survey this response belongs to.
        surveyName:
          readOnly: true
          type: string
          description: >-
            The survey's current name, denormalized into the payload so a
            consumer listing responses need not resolve survey names separately.
        workspaceId:
          readOnly: true
          type: string
          format: cuid2
          description: >-
            The owning workspace, resolved through the survey. Responses carry
            no workspace column of their own; this is a derived field.
        createdAt:
          readOnly: true
          type: string
          format: date-time
          description: >
            When the response was created. **Server-owned** — it cannot be
            supplied on create or changed by a patch. This is the AIP-148
            output-only convention, and it is deliberate on two counts: monthly
            response metering counts rows by `(surveyId, createdAt)`, and a
            client-chosen `createdAt` can insert rows into the middle of another
            consumer's already-paginated stream. Backdating remains available on
            v1/v2; a dedicated import path is future work.
        updatedAt:
          readOnly: true
          type: string
          format: date-time
          description: >-
            When the response was last modified. Server-owned. Neither
            filterable nor sortable — it has no index, and being mutable it
            cannot anchor a pagination cursor.
        finished:
          type: boolean
          description: Whether the respondent reached an ending.
        endingId:
          type:
            - string
            - 'null'
          description: >-
            The ending card the respondent reached. `null` for unfinished
            responses.
        language:
          type:
            - string
            - 'null'
          description: >-
            The survey language this response was collected in, as a survey
            language code (not necessarily a BCP-47 locale — `default` is a
            legitimate value). `null` when the survey is single-language or no
            language was stamped.
        durationSeconds:
          readOnly: true
          type: number
          minimum: 0
          description: >-
            Total seconds the respondent spent, summed from per-element timing
            and **excluding** the stored `_total` bucket, which the server
            maintains alongside real element ids and which would otherwise be
            double-counted. Absent when the submission carried no timing.


            Summing rather than reading `_total` is deliberate: the stored
            bucket is only written when a response is finished, so reading it
            would leave every partial response without a duration even when it
            carries per-element timing. This is also why the value can differ
            from the reserved `durationSeconds` catalog entry, which does read
            the bucket and rounds to whole seconds — that entry is not projected
            into `embeddedData[]`, so the two never appear side by side.
        resolution:
          $ref: '#/components/schemas/ResponseResolution'
          readOnly: true
        answers:
          readOnly: true
          type: array
          description: >-
            One entry per element the respondent reached, in survey order. Empty
            for a response that recorded no answers.
          items:
            $ref: '#/components/schemas/ResponseAnswer'
        embeddedData:
          readOnly: true
          type: array
          description: >-
            Hidden fields, variables and auto-captured context, as one
            collection. Empty when the response carries none.
          items:
            $ref: '#/components/schemas/ResponseEmbeddedDatum'
        unresolved:
          readOnly: true
          type: array
          description: >-
            Stored data whose element or key no longer exists in the survey
            definition. Empty in the normal case.
          items:
            $ref: '#/components/schemas/ResponseUnresolvedEntry'
        tags:
          type: array
          description: >-
            Tags applied to this response, as `{ id, name }` references. Empty
            when the response is untagged.
          items:
            $ref: '#/components/schemas/ResponseTag'
    ResponseDataMap:
      type: object
      description: >
        Answers as stored, **keyed by element id**.


        Hidden-field values do not live here. They are stored in the same
        underlying map, but that is a storage detail rather than part of this
        contract: a hidden field is an Embedded Data field, and Embedded Data is
        addressed by name through `embeddedData`.


        A hidden field's name appearing as a key here is rejected rather than
        silently written to the wrong namespace — with **422**, not 400, because
        knowing that a key names a hidden field means loading the survey, and
        these operations route every failure that needs stored state to 422. The
        problem carries `invalid_params[]` with `name` set to the offending key,
        `code: unsupported_field` and `referenceType: hiddenField`, so a client
        can point at it without parsing prose.


        A key that is **also** an element id is the exception, and it is
        **accepted**: the answer owns that address. Answers and hidden fields do
        share one slot, and the older surveys that declare a field named after a
        question have exactly one value under that key — but it is always the
        answer's. The field is dropped at ingest with `element_id_collision`
        (`packages/types/embedded-data-ingest.ts`) and filtered again in the
        renderer before it can reach a submission, precisely so a display-time
        field value cannot land on top of what the respondent answered. So on
        such a survey `answers[]` carries the value, `embeddedData` neither
        reports it nor accepts a write for it, and refusing the key here would
        block the only write that fits it.


        New surveys cannot reach that state through the editor or `POST`/`PATCH
        /api/v3/surveys`, which refuse an element id colliding with a declared
        field name. Both v1 management survey writes still accept one — that
        guard reads variable and hidden-field names but never element ids — so
        this set, unlike the reserved-name one, is still growing.


        Keeping the two apart is what lets the storage layout change underneath
        — the legacy columns go away eventually — without this contract either
        following it or breaking a round-trip it promised.


        The value shapes are the stored ones, and they differ by element type.
        `ResponseAnswer` documents each; `answers[]` on the read is the resolved
        view of this same map.


        **Bounds are a write-side concern and live on `ResponseDataMapInput`,
        not here.** This schema is the stored shape, and a read must describe
        what the API can actually return: the v1 and v2 write paths store this
        same map with no cardinality limit, so a response written through them
        can carry more keys — or a longer multi-select — than a v3 write would
        accept. Declaring the write bounds on this schema would make `GET
        /api/v3/responses/{responseId}` promise something it cannot keep for
        those rows.
      additionalProperties:
        $ref: '#/components/schemas/ResponseRawValue'
    ResponseContact:
      type: object
      description: >
        The contact that submitted the response, when the response is
        identified. Present on the detailed view only — the list view omits it.
        `null` for anonymous (link-survey) responses.
      required:
        - id
      properties:
        id:
          type: string
          format: cuid2
          description: Contact identifier.
        userId:
          type:
            - string
            - 'null'
          description: >-
            The contact's `userId` attribute — the id your application knows the
            person by. This is a contact **attribute**, not a column, and it is
            not guaranteed unique within a workspace, so it identifies the
            person for display but must not be used as a lookup key. `null` when
            the contact has no `userId` attribute set.
      additionalProperties: false
    InvalidParam:
      type: object
      description: >
        Field-level validation error. `name` and `reason` are always intended
        for humans.

        Optional machine-readable fields are included when the API can identify
        the semantic error,

        especially for survey locale, identifier, and reference validation used
        by MCP and agent clients.
      required:
        - name
        - reason
      properties:
        name:
          type: string
          description: Dot-path to the invalid request field.
        reason:
          type: string
          description: Human-readable repair guidance.
        code:
          type: string
          enum:
            - dangling_reference
            - duplicate_identifier
            - duplicate_locale
            - forbidden_identifier
            - immutable_identifier
            - invalid_locale
            - invalid_reference
            - missing_required_field
            - misordered_reference
            - missing_translation
            - qsf_limit_exceeded
            - qsf_not_recognized
            - read_only_field
            - unsupported_field
            - unsupported_locale
          description: >
            Machine-readable validation category, when available.


            This is the shared error shape every v3 operation returns, so it
            lists what the API can emit today and nothing more —
            `INVALID_PARAM_CODES` in `app/api/v3/lib/response.ts` is the
            registry, and a code absent from it is dropped before the response
            is built. Categories for operations that do not exist yet are added
            with the code that emits them, not ahead of it.


            `qsf_not_recognized` and `qsf_limit_exceeded` come from the
            Qualtrics import (an internal, session-only route sharing this
            shape): the file is not a Qualtrics export it can read, or it is one
            past an import limit, which `identifier` names.
        identifier:
          type: string
          description: >-
            Identifier involved in the validation failure, when available. For
            language errors, this can be the normalized language code.
        referenceType:
          type: string
          enum:
            - block
            - element
            - ending
            - hiddenField
            - language
            - variable
            - variableName
            - recall
          description: >-
            Identifier namespace involved in the validation failure. Same rule
            as `code`: these are the namespaces the API emits today, and more
            arrive with the operations that emit them.
        missingId:
          type: string
          description: Referenced id that does not exist in the submitted survey document.
        firstUsedAt:
          type: string
          description: Dot-path where a duplicate identifier was first used.
        conflictsWith:
          type: string
          description: Dot-path to the existing identifier that conflicts with this field.
      additionalProperties: false
    ResponseResolution:
      type: object
      description: >
        Receipt describing how `answers[]` and `embeddedData[]` labels were
        produced. It exists so a consumer can tell whether the labels it is
        reading are the wording the respondent actually saw.
      required:
        - labelPolicy
        - labelsLanguage
        - surveyUpdatedAt
      properties:
        labelPolicy:
          type: string
          enum:
            - currentSurveyDefinition
          description: >
            Where labels came from. Today the only value is
            `currentSurveyDefinition`: labels are resolved at **read time**
            against the survey as it stands now. Nothing about the survey is
            snapshotted when a response is written and there is no
            survey-version model, so a response collected under an older
            headline reads back under today's wording — text the respondent may
            never have seen. Typeform, SurveyMonkey, Qualtrics, Google Forms and
            Jotform all behave this way, so this is the industry norm rather
            than a shortcut. Compare `surveyUpdatedAt` against the response's
            `createdAt` to detect that the definition moved after collection. If
            a write-time label snapshot ever ships, a second value `asAsked` is
            added here and consumers branch on it — treat this as an open enum.
        labelsLanguage:
          type:
            - string
            - 'null'
          description: >-
            The language the labels in this payload were resolved in. Follows
            the response's `language` when it is set and the survey has that
            language enabled, otherwise the survey's default language. `null`
            when the survey carries no translations.
        surveyUpdatedAt:
          type: string
          format: date-time
          description: >-
            When the survey definition was last modified. Later than the
            response's `createdAt` means the wording in `elementLabel` may have
            drifted from what was asked.
      additionalProperties: false
    ResponseAnswer:
      oneOf:
        - $ref: '#/components/schemas/ResponseAnswerText'
        - $ref: '#/components/schemas/ResponseAnswerNumber'
        - $ref: '#/components/schemas/ResponseAnswerBoolean'
        - $ref: '#/components/schemas/ResponseAnswerSelection'
        - $ref: '#/components/schemas/ResponseAnswerDate'
        - $ref: '#/components/schemas/ResponseAnswerFileUpload'
        - $ref: '#/components/schemas/ResponseAnswerBooking'
        - $ref: '#/components/schemas/ResponseAnswerMatrix'
        - $ref: '#/components/schemas/ResponseAnswerComposite'
      discriminator:
        propertyName: elementType
        mapping:
          openText: '#/components/schemas/ResponseAnswerText'
          nps: '#/components/schemas/ResponseAnswerNumber'
          rating: '#/components/schemas/ResponseAnswerNumber'
          csat: '#/components/schemas/ResponseAnswerNumber'
          ces: '#/components/schemas/ResponseAnswerNumber'
          consent: '#/components/schemas/ResponseAnswerBoolean'
          cta: '#/components/schemas/ResponseAnswerBoolean'
          multipleChoiceSingle: '#/components/schemas/ResponseAnswerSelection'
          multipleChoiceMulti: '#/components/schemas/ResponseAnswerSelection'
          pictureSelection: '#/components/schemas/ResponseAnswerSelection'
          ranking: '#/components/schemas/ResponseAnswerSelection'
          date: '#/components/schemas/ResponseAnswerDate'
          fileUpload: '#/components/schemas/ResponseAnswerFileUpload'
          cal: '#/components/schemas/ResponseAnswerBooking'
          matrix: '#/components/schemas/ResponseAnswerMatrix'
          address: '#/components/schemas/ResponseAnswerComposite'
          contactInfo: '#/components/schemas/ResponseAnswerComposite'
      description: >
        One answer, carrying the element's id, its label, its type and its
        value. `elementType` selects the shape: switch on it rather than probing
        for which value field is present.


        Answers are a **typed array**, not a map keyed by element id, because a
        consumer holding only a response would otherwise see bare ids and have
        to fetch the survey and join to learn what was asked — a join that is
        easy to get wrong and that agent clients cannot make cheaply. The array
        also separates answers from hidden fields and variables, which the
        stored representation mixes into one flat map. Non-answer data lives in
        `embeddedData[]`.


        Several element types share a value shape and therefore a schema — the
        four numeric scales, the four choice styles, the two composites. All
        seventeen element types are covered.


        Only elements the respondent actually reached appear here. Data whose
        element has since been deleted appears in `unresolved[]` instead.
    ResponseEmbeddedDatum:
      type: object
      description: >
        One piece of non-answer data carried by the response. Survey
        **variables**, **hidden fields** and **auto-captured context** are one
        concept here rather than three parallel ones: they all have a key, a
        provenance, a declared type and a label, and consumers treat them alike.


        Auto-captured (`reserved`) entries are drawn from the Embedded Data
        field catalog, which declares a privacy policy per field (`keep` /
        `drop` / `redactQuery`). Two of those act **before** this API sees the
        response, and one is a rule this API adds:


        - `drop` is an **ingest-time** policy, not a filter applied here: when
        the survey has "Anonymize
          responses" enabled the field is never captured, so it is absent from the response for every
          consumer. With anonymization off it is captured and appears here normally.

        - `redactQuery` is applied on **every** read, anonymization or not —
        `url` and `pageReferrer` carry
          their query string stripped.

        - `ipAddress` is **never** projected by this API, in any view. That is a
        v3 rule rather than a
          catalog behaviour: the catalog reads the field, and the dashboard shows it. Do not infer the same
          suppression for the other `drop` fields.


        Where a survey's own field and a catalog entry share a name, both appear
        — the declared one as `ingested` or `computed`, the auto-captured one as
        `reserved` — and `kind` is what tells them apart. Neither shadows the
        other, because each is a real value the response carries and dropping
        one would hide data the author can see elsewhere.


        Which catalog entries appear at all is a third axis, `display`, and this
        collection carries the ones that are not `none`. The `none` set is the
        response's own identity and timing — `responseId`, `surveyId`,
        `finished`, `language`, `durationSeconds`, `startedAt`, `finishedAt` —
        every one of which is already a first-class field on the response.
        Without that filter they would each appear twice in the same payload,
        once at the top level and once here.
      required:
        - key
        - kind
        - type
        - label
        - value
      properties:
        key:
          type: string
          description: >
            The field's **name** — what an author calls it, and the key
            `embeddedData` on a write accepts.


            Not the storage key. A hidden field happens to be stored under its
            name and a variable under its cuid, and that split is a property of
            the storage layout rather than of the field; exposing it would
            publish an internal id and make the writable and readable views of
            one field disagree on how to address it.


            **Unique per `kind`, not across the collection.** A survey created
            before the reserved names were enforced can declare a hidden field
            called `country`, and the auto-captured `country` still exists — so
            the same name can appear once as `ingested` and once as `reserved`.
            That set is finite and no longer growing: a new declaration may no
            longer take a reserved name, or the name of a variable or hidden
            field the survey already has. Key on `(key, kind)` rather than `key`
            alone, or read the pair you care about.
        kind:
          type: string
          enum:
            - ingested
            - computed
            - reserved
          description: >
            Provenance of the value:


            - `ingested` — supplied by the caller or the embedding page (hidden
            fields).

            - `computed` — derived during the submission by survey logic
            (variables).

            - `reserved` — auto-captured context from the submission environment
            (for example `country`,
              `deviceType`), drawn from the Embedded Data field catalog.
        type:
          type: string
          enum:
            - string
            - number
            - boolean
            - date
          description: >-
            Declared type of `value`, so a consumer need not infer it.
            `computed` entries are only ever `string` or `number`, because
            survey variables are typed that narrowly.
        label:
          type: string
          description: >-
            Human-readable name for the key, in the resolved language where one
            exists.
        value:
          oneOf:
            - type: string
            - type: number
            - type: boolean
          description: >-
            The value, matching `type`. `date` values are ISO-8601 strings.


            Always a scalar. A legacy row can hold an array or an object under a
            declared field's key — the ingest contract refuses those now, but
            older rows have them — and such a value resolves to nothing rather
            than being coerced, so the entry is omitted here and reported in
            `unresolved[]` with the bytes intact. Nothing is silently dropped,
            and no `type` would describe an array anyway.


            There is no `null` member: a declared key that resolves to nothing
            is **omitted** from the collection rather than carried with a null.
            So `embeddedData[]` enumerates what the response actually holds, not
            what the survey declares — a survey with twelve declared fields and
            two filled in yields two entries. A field with a default value is
            never unset, since the default is the fallback before the key is
            considered missing.
      additionalProperties: false
    ResponseUnresolvedEntry:
      type: object
      description: >
        Stored data that nothing in the current survey definition can account
        for. Deleting an element, a hidden field or a variable leaves its values
        behind on already-collected responses and nothing cleans them up, so the
        payload reports them here instead of dropping them silently or faking
        them into `answers[]` without a type or a label. Consumers doing
        analysis can ignore this array; consumers doing export or migration
        should not.


        **A declared field's `defaultValue` takes precedence over anything
        unreadable stored under its key**, and that is worth stating because
        this collection otherwise reads as exhaustive. Resolution falls back to
        the default before a value is ever judged unreadable, so a field that
        declares one always resolves — and the stored bytes, whatever their
        shape, are reported nowhere. Only a field with **no** `defaultValue` can
        reach `valueShapeMismatch`.

        A **scalar** under a defaultless field is not reported either, and the
        cause is the reporting guard rather than `locked`. Only
        `resolveEmbeddedValue` consults the lock; the guard that decides what
        reaches this collection admits objects only. So an uncoercible scalar —
        `"yes"` under a boolean field — resolves to nothing and is then excluded
        for being a scalar, lock or no lock. A non-scalar under a lockless,
        defaultless field is what does reach `valueShapeMismatch`. Neither
        omission is a silent drop by accident — one follows from resolution
        order, the other from the reporting guard — and both are written down so
        a consumer does not read `unresolved[]` as a guarantee that nothing was
        left behind. If either proves to matter in practice a reason code can be
        added without breaking anyone, whereas removing one could not — which is
        why this is documentation rather than a guess at a fifth enum member.
      required:
        - key
        - rawValue
        - reason
      properties:
        key:
          type: string
          description: >-
            The key as stored on the response. For `elementNotInSurvey` it is a
            former element id or hidden-field name; for `variableNotInSurvey` a
            variable's cuid, since that map is keyed by id rather than by name;
            and for `valueShapeMismatch` the id or name of the element or
            declared field that *does* still exist — that reason means the key
            resolved and the value did not.
        rawValue:
          $ref: '#/components/schemas/ResponseRawValue'
          description: >-
            The stored value, returned verbatim and untyped because nothing
            describes it any more.
        reason:
          type: string
          enum:
            - elementNotInSurvey
            - variableNotInSurvey
            - valueShapeMismatch
          description: >
            Why the entry could not be resolved:


            - `elementNotInSurvey` — nothing in the current definition claims
            this key. It covers a deleted
              element **and** a deleted hidden field, because the two cannot be told apart: an answer is
              stored under its element id and a hidden field under its name, in one shared map, and storage
              keeps no record of which kind a vanished key used to be.

            - `variableNotInSurvey` — no variable with this id exists in the
            current definition. Variables
              *are* distinguishable, because they live in their own map keyed by cuid rather than sharing
              the answer map.

            - `valueShapeMismatch` — the key resolves to a current element **or
            a declared Embedded Data
              field**, but the stored value's shape cannot be read as an
              answer of that element's type (for example a string where the element expects a selection
              list). Signals element-type churn rather than deletion.
      additionalProperties: false
    ResponseTag:
      type: object
      description: >
        A tag applied to a response, as a lightweight reference. Deliberately
        **not** the full `TagResource`: that shape carries a `count` of how many
        responses hold the tag, which would mean an extra aggregate per tag on
        every row of every page. Read tags in full — with counts and timestamps
        — from `GET /api/v3/tags`.
      required:
        - id
        - name
      properties:
        id:
          type: string
          format: cuid2
        name:
          type: string
          description: Tag label as shown in the app.
      additionalProperties: false
    ResponseRawValue:
      description: >
        One stored answer or field value, in the four shapes the v3 contract can
        carry: a string, a number, a list of strings, or a string-to-string map.


        Declared once and referenced by `ResponseDataMap` and
        `ResponseUnresolvedEntry.rawValue`, so the two places that publish
        stored JSON cannot drift apart. A stored value outside these shapes (a
        legacy JSON `null`, an array with non-string items, a map with
        non-string values) is not published at all rather than reaching a client
        as a body this contract would reject.
      oneOf:
        - type: string
        - type: number
        - type: array
          items:
            type: string
        - type: object
          additionalProperties:
            type: string
    ResponseAnswerText:
      allOf:
        - $ref: '#/components/schemas/ResponseAnswerBase'
        - type: object
          required:
            - valueText
          properties:
            elementType:
              type: string
              enum:
                - openText
            valueText:
              type: string
              description: The submitted text, verbatim.
            inputType:
              type: string
              enum:
                - text
                - email
                - url
                - number
                - phone
              description: >-
                The element's configured input type in the current definition.
                Useful for rendering and for knowing how the value was validated
                at collection time; the value itself is always returned as a
                string, including for `number`.
      unevaluatedProperties: false
      description: Free-text answer to an `openText` element.
    ResponseAnswerNumber:
      allOf:
        - $ref: '#/components/schemas/ResponseAnswerBase'
        - type: object
          required:
            - valueNumber
            - range
          properties:
            elementType:
              type: string
              enum:
                - nps
                - rating
                - csat
                - ces
            valueNumber:
              type: number
              description: The submitted score.
            range:
              type: object
              description: >-
                The scale the score was collected on, from the current
                definition. Returned because a bare `8` is uninterpretable
                without it, and because scales are editable — a consumer
                comparing scores across time needs to see that the range moved.
              required:
                - min
                - max
              properties:
                min:
                  type: number
                max:
                  type: number
              additionalProperties: false
            scale:
              type: string
              enum:
                - number
                - smiley
                - star
              description: >-
                How the scale was presented. Carried by the element types that
                configure one — `rating`, `csat` and `ces` — and absent for
                `nps`, which has no scale presentation.
      unevaluatedProperties: false
      description: >-
        Numeric-scale answer. Covers `nps` (0–10), `rating` (configurable range
        and presentation), `csat` and `ces`.
    ResponseAnswerBoolean:
      allOf:
        - $ref: '#/components/schemas/ResponseAnswerBase'
        - type: object
          required:
            - valueBoolean
            - rawValue
          properties:
            elementType:
              type: string
              enum:
                - consent
                - cta
            valueBoolean:
              type: boolean
              description: >-
                Whether the respondent accepted (`consent`) or clicked through
                (`cta`). Derived from the stored token, which is why `rawValue`
                is returned alongside it.
            rawValue:
              type: string
              description: >-
                The token as stored — conventionally `accepted` for `consent`
                and `clicked` for `cta`. Returned because the boolean is a
                derivation and this is the underlying fact.
      unevaluatedProperties: false
      description: >-
        Acknowledgement answer. Covers `consent` (accepted) and `cta` (clicked
        through). A `cta` element the respondent skipped has no entry in
        `answers[]` at all.
    ResponseAnswerSelection:
      allOf:
        - $ref: '#/components/schemas/ResponseAnswerBase'
        - type: object
          required:
            - selections
          properties:
            elementType:
              type: string
              enum:
                - multipleChoiceSingle
                - multipleChoiceMulti
                - pictureSelection
                - ranking
            selections:
              type: array
              description: >-
                The chosen options, resolved against the current definition.
                Always an array, including for `multipleChoiceSingle`, so
                consumers need one code path rather than two. For `ranking`, the
                array is in rank order and each entry also carries an explicit
                `rank`.
              items:
                $ref: '#/components/schemas/ResponseSelection'
      unevaluatedProperties: false
      description: >-
        Choice-style answer. Covers `multipleChoiceSingle`,
        `multipleChoiceMulti`, `pictureSelection` and `ranking`. See
        `ResponseValueMatch` for how each selection was resolved and why an
        unresolvable value is never reclassified as a write-in.
    ResponseAnswerDate:
      allOf:
        - $ref: '#/components/schemas/ResponseAnswerBase'
        - type: object
          required:
            - valueDate
          properties:
            elementType:
              type: string
              enum:
                - date
            valueDate:
              type: string
              description: >-
                The submitted date as stored, an ISO-8601 date or date-time
                string. No `format` is asserted because historical rows were
                written by several client versions and the stored precision
                varies; parse defensively.
      unevaluatedProperties: false
      description: Date answer.
    ResponseAnswerFileUpload:
      allOf:
        - $ref: '#/components/schemas/ResponseAnswerBase'
        - type: object
          required:
            - fileUrls
          properties:
            elementType:
              type: string
              enum:
                - fileUpload
            fileUrls:
              type: array
              description: >
                The uploaded files, **as stored** — workspace-relative paths,
                not absolute URLs, and neither resolved nor signed.


                A deliberate choice between two things this field could be.
                Storage writes a relative path and the app's own read paths
                rewrite it to an absolute URL for display; v3 returns the stored
                bytes instead, because `answers[]` promises verbatim values a
                client can write straight back, and an absolute URL is neither
                what is stored nor what a write accepts. Fetching a file is a
                separate concern with its own ticket (ENG-513, signed
                short-lived URLs behind a dedicated endpoint); this field
                identifies the file, it does not serve it.


                Not publicly fetchable in any case: access follows the same
                authorization as the response, and deleting the response deletes
                the underlying files.
              items:
                type: string
            fileCount:
              type: integer
              minimum: 0
              description: >-
                Convenience count of `fileUrls`, so a list consumer need not
                materialize the array.
      unevaluatedProperties: false
      description: File-upload answer.
    ResponseAnswerBooking:
      allOf:
        - $ref: '#/components/schemas/ResponseAnswerBase'
        - type: object
          required:
            - booked
            - rawValue
          properties:
            elementType:
              type: string
              enum:
                - cal
            booked:
              type: boolean
              description: Whether the respondent completed a booking.
            rawValue:
              type: string
              description: >-
                The token as stored, verbatim — conventionally `booked`.
                Required, like the equivalent field on `consent` and `cta`,
                because `booked` is a derivation and this is the underlying
                value a client needs to rebuild the stored map.
      unevaluatedProperties: false
      description: >-
        Scheduling answer for a `cal` element. Only the fact of the booking is
        recorded on the response; the meeting itself lives in the scheduling
        provider and is not proxied by this API.
    ResponseAnswerMatrix:
      allOf:
        - $ref: '#/components/schemas/ResponseAnswerBase'
        - type: object
          required:
            - rows
          properties:
            elementType:
              type: string
              enum:
                - matrix
            rows:
              type: array
              description: >-
                One entry per row the respondent answered. Rows left blank are
                omitted rather than returned with a null column. Both sides of a
                matrix cell need resolving against the current definition, so
                each entry reports its own `match` — see `ResponseValueMatch`.
              items:
                type: object
                required:
                  - rawKey
                  - rowLabel
                  - rawValue
                  - columnLabel
                  - match
                properties:
                  rawKey:
                    type: string
                    description: >-
                      The stored map key for this row, verbatim. Matrix answers
                      are stored keyed by the **localized row label** in the
                      respondent's language, not by row id — so this is the
                      string to use when rebuilding the stored object, and it is
                      not necessarily equal to `rowLabel`, which is resolved in
                      the *current* definition's language.
                  rowId:
                    type:
                      - string
                      - 'null'
                    description: >-
                      Row id resolved from the current definition, or `null`
                      when the stored key matches no current row. Derived, never
                      stored.
                  rowLabel:
                    type: string
                    description: >-
                      Row label in the resolved language. Falls back to `rawKey`
                      when the row no longer resolves.
                  columnId:
                    type:
                      - string
                      - 'null'
                    description: >-
                      Column id resolved from the current definition, or `null`
                      when it no longer resolves. Derived, never stored.
                  columnLabel:
                    type:
                      - string
                      - 'null'
                    description: >-
                      Column label in the resolved language. `null` when `match`
                      is `unmatched`.
                  rawValue:
                    type: string
                    description: >-
                      The selected column as stored, verbatim — the localized
                      column label, not a column id. This is the value to use
                      when rebuilding the stored object.
                  match:
                    $ref: '#/components/schemas/ResponseValueMatch'
                additionalProperties: false
      unevaluatedProperties: false
      description: Matrix answer — one selected column per answered row.
    ResponseAnswerComposite:
      allOf:
        - $ref: '#/components/schemas/ResponseAnswerBase'
        - type: object
          required:
            - fields
          properties:
            elementType:
              type: string
              enum:
                - address
                - contactInfo
            fields:
              type: array
              description: >
                The element's sub-fields, **every slot, in storage order**,
                including ones the respondent left blank (`valueText: ""`).
                Blanks are returned rather than omitted because the stored value
                is a positional fixed-length array — `address` has 6 slots,
                `contactInfo` has 5 — so dropping an empty slot would shift
                every later value onto the wrong field and make the stored array
                impossible to rebuild.
              items:
                type: object
                required:
                  - slot
                  - fieldId
                  - fieldLabel
                  - valueText
                properties:
                  slot:
                    type: integer
                    minimum: 0
                    description: >-
                      0-based index of this sub-field in the stored array.
                      Reconstruct the stored value by ordering on this and
                      taking `valueText`.
                  fieldId:
                    type: string
                    enum:
                      - addressLine1
                      - addressLine2
                      - city
                      - state
                      - zip
                      - country
                      - firstName
                      - lastName
                      - email
                      - phone
                      - company
                    description: >-
                      Stable sub-field id. `address` uses `addressLine1`,
                      `addressLine2`, `city`, `state`, `zip`, `country` in that
                      order; `contactInfo` uses `firstName`, `lastName`,
                      `email`, `phone`, `company`.
                  fieldLabel:
                    type: string
                    description: Sub-field label in the resolved language.
                  valueText:
                    type: string
                    description: >-
                      The submitted value, verbatim. `""` when the respondent
                      left it blank.
                additionalProperties: false
      unevaluatedProperties: false
      description: >-
        Composite answer whose value is a set of labelled sub-fields. Covers
        `address` and `contactInfo`. Stored as a positional array of strings,
        which is why every slot is reported.
    ResponseAnswerBase:
      type: object
      description: >-
        Fields every answer carries, whatever the element type. `elementType`
        selects the value-bearing shape — see `ResponseAnswer`.
      required:
        - position
        - elementId
        - elementType
        - elementLabel
      properties:
        position:
          type: integer
          minimum: 1
          description: >-
            1-based position of the element in the current survey definition, so
            a consumer can present answers in survey order without fetching the
            survey. Elements the respondent never reached are absent from
            `answers[]` entirely rather than present-and-empty.
        elementId:
          type: string
          description: Stable element id, as authored on the survey.
        elementType:
          type: string
          enum:
            - openText
            - multipleChoiceSingle
            - multipleChoiceMulti
            - nps
            - rating
            - csat
            - ces
            - consent
            - pictureSelection
            - cta
            - date
            - fileUpload
            - cal
            - matrix
            - address
            - ranking
            - contactInfo
          description: >-
            The element's type in the current survey definition. Matches the
            `type` of the corresponding `SurveyElement`.
        elementLabel:
          type: string
          description: >-
            The element's headline, resolved in `resolution.labelsLanguage`.
            Read at request time from the current definition — see
            `ResponseResolution` for what that means for older responses.
        durationSeconds:
          type: number
          minimum: 0
          maximum: 86400
          description: >
            Seconds the respondent spent on this element. Converted from the
            stored per-element time-to-complete, which is recorded in
            milliseconds. This telemetry is reported by the client and is
            therefore not trustworthy input: treat it as an indication rather
            than a measurement. Absent when the submission carried no timing for
            this element.


            The bounds hold on **every** row, including ones collected before
            the write-side clamp shipped. The stored representation is
            deliberately unbounded so that historical data still parses, and a
            row written then can hold a negative value or a tab left open for
            days; those are clamped into range on read rather than returned
            as-is, because a schema a real row violates is worse than a slightly
            blunted outlier. So `86400` means "at least 24 hours", not exactly.
    ResponseSelection:
      type: object
      description: >
        One selected option within a choice-style answer. `optionId` plus
        `rawValue` is deliberately a two-field split — the same split the Hub
        stores as `value_id` plus `value_text` — because option ids are stable
        while labels are editable and language-dependent. Consumers that need to
        group or compare across time should key on `optionId`; consumers
        rendering to a human should use `optionLabel`.
      required:
        - optionId
        - optionLabel
        - rawValue
        - match
      properties:
        optionId:
          type:
            - string
            - 'null'
          description: >-
            Resolved option id, or the literal `other` for a write-in. `null`
            when `match` is `unmatched`.
        optionLabel:
          type:
            - string
            - 'null'
          description: >-
            Option label in the resolved language. `null` when `match` is
            `unmatched`.
        rawValue:
          type: string
          description: >-
            The value as stored on the response, verbatim and before resolution
            — always present, and the string to use when rebuilding the stored
            map. Note what is stored differs by element type:
            `multipleChoiceSingle`, `multipleChoiceMulti` and `ranking` store
            the option **label** in the respondent's language, while
            `pictureSelection` stores the option **id**. `""` occurs when the
            respondent selected Other and left it blank, and is returned rather
            than dropped.
        match:
          $ref: '#/components/schemas/ResponseValueMatch'
        rank:
          type: integer
          minimum: 1
          description: >-
            1-based rank, present only for `ranking` elements. Array order
            already reflects the rank; this field makes it explicit so consumers
            need not rely on ordering.
      additionalProperties: false
    ResponseValueMatch:
      type: string
      enum:
        - exact
        - label
        - other
        - unmatched
      description: >
        How a stored raw value was resolved against the current survey
        definition. Precedence is fixed and applied in this order:


        - `exact` — the stored value matched an option **id** in the current
        definition.

        - `label` — the stored value matched an option's **label text** (ids are
        stable, labels are
          editable and language-dependent, so a label match is weaker evidence).

        - `other` — the value arrived through the element's **Other** free-text
        input. Only reported when
          the element actually has that input enabled and the value was submitted through it.

        - `unmatched` — no option resolves. The raw value is still returned; it
        is **not** silently
          reclassified as `other`, because a *renamed* option is also unmatched text and filing it as a
          write-in would misattribute the answer.
  responses:
    V3Unauthorized:
      description: Not authenticated (no valid session or API key).
      headers:
        WWW-Authenticate:
          schema:
            type: string
          example: Bearer realm="formbricks"
          description: >-
            Authentication scheme challenge. RFC 9110 §15.5.2 asks a 401 for a
            challenge "applicable to the target resource", and Bearer is
            applicable here because these operations accept the API key as
            `Authorization: Bearer <key>` as well as `x-api-key`.
            Session-cookie-only operations send no challenge — see
            `V3UnauthorizedSession`. The MCP endpoints send a richer OAuth
            challenge carrying `resource_metadata` and `scope`.
        X-Request-Id:
          schema:
            type: string
          description: Request correlation ID
        Cache-Control:
          schema:
            type: string
          example: private, no-store
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    V3Forbidden:
      description: >-
        Forbidden — no workspace access, or resource does not exist (404 not
        used; avoids existence leak).
      headers:
        X-Request-Id:
          schema:
            type: string
          description: Request correlation ID
        Cache-Control:
          schema:
            type: string
          example: private, no-store
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    V3TooManyRequests:
      description: Rate limit exceeded.
      headers:
        X-Request-Id:
          schema:
            type: string
          description: Request correlation ID
        Cache-Control:
          schema:
            type: string
          example: private, no-store
        Retry-After:
          schema:
            type: integer
          description: Seconds until the current rate-limit window resets.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    V3InternalServerError:
      description: Internal Server Error.
      headers:
        X-Request-Id:
          schema:
            type: string
          description: Request correlation ID
        Cache-Control:
          schema:
            type: string
          example: private, no-store
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  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.
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        The same management API key, presented as `Authorization: Bearer <key>`
        instead of `x-api-key`. Equivalent in every way — same key, same
        workspace permissions — and offered because it is what most HTTP clients
        and SDK generators reach for by default.

        A **401** from any operation that accepts it carries `WWW-Authenticate:
        Bearer realm="formbricks"`. Session-only operations accept neither this
        nor `x-api-key`.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.