> ## 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.

# Update a survey

> Updates a survey through a strict top-level partial document. Omitted top-level fields are
preserved. Provided top-level objects and arrays replace that whole subtree; this endpoint
does not deep-merge nested objects and does not implement JSON Patch.

This write endpoint accepts the canonical multilingual authoring document only. Translatable
fields must be locale-code maps, not strings. `PATCH ?lang=...` and `?version=...` are not
supported in Scope 1.

For MCP and AI-agent workflows, fetch the current survey first, modify only the intended
top-level fields, call `/api/v3/surveys/validate` with `operation: patch` for preflight, then
submit the same patch here. Missing ids in replaced arrays are treated as deletions. Variable
name renames are allowed but may be destructive for downstream exports or integrations.
Element id changes are rejected once the survey and element are no longer drafts.

Server-owned fields that `GET` returns (`id`, `workspaceId`, `type`, `createdAt`, `archivedAt`, `defaultLanguage`, `languages[].alias`) are accepted and verified, so the output of a `GET` can be sent straight back. Echoing the value you were given is a no-op; changing one is a **422** `read_only_field`.

A round-tripped `updatedAt` acts as an optimistic-concurrency precondition: the write is applied only if the survey has not changed since your read, and is otherwise rejected with **409**. Omit it and overlapping writes remain last-write-wins. Header-based preconditions (`ETag` / `If-Match`) are still future work.




## OpenAPI

````yaml /api-v3-reference/openapi.yml patch /api/v3/surveys/{surveyId}
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/surveys/{surveyId}:
    patch:
      tags:
        - V3 Surveys
      summary: Update a survey
      description: >
        Updates a survey through a strict top-level partial document. Omitted
        top-level fields are

        preserved. Provided top-level objects and arrays replace that whole
        subtree; this endpoint

        does not deep-merge nested objects and does not implement JSON Patch.


        This write endpoint accepts the canonical multilingual authoring
        document only. Translatable

        fields must be locale-code maps, not strings. `PATCH ?lang=...` and
        `?version=...` are not

        supported in Scope 1.


        For MCP and AI-agent workflows, fetch the current survey first, modify
        only the intended

        top-level fields, call `/api/v3/surveys/validate` with `operation:
        patch` for preflight, then

        submit the same patch here. Missing ids in replaced arrays are treated
        as deletions. Variable

        name renames are allowed but may be destructive for downstream exports
        or integrations.

        Element id changes are rejected once the survey and element are no
        longer drafts.


        Server-owned fields that `GET` returns (`id`, `workspaceId`, `type`,
        `createdAt`, `archivedAt`, `defaultLanguage`, `languages[].alias`) are
        accepted and verified, so the output of a `GET` can be sent straight
        back. Echoing the value you were given is a no-op; changing one is a
        **422** `read_only_field`.


        A round-tripped `updatedAt` acts as an optimistic-concurrency
        precondition: the write is applied only if the survey has not changed
        since your read, and is otherwise rejected with **409**. Omit it and
        overlapping writes remain last-write-wins. Header-based preconditions
        (`ETag` / `If-Match`) are still future work.
      operationId: patchSurveyV3
      parameters:
        - in: path
          name: surveyId
          required: true
          schema:
            type: string
            format: cuid2
          description: Survey identifier.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchSurveyRequest'
            examples:
              renameSurvey:
                summary: Rename a survey
                value:
                  name: Updated Product Feedback
              replaceMetadata:
                summary: Replace metadata
                value:
                  metadata:
                    title:
                      en-US: Product Feedback
                      de-DE: Produktfeedback
                    cx_operation: product_feedback
              setCustomCss:
                summary: Replace the survey's custom CSS
                value:
                  customCss:
                    light: '[data-fb-part="button-primary"] { border-radius: 999px; }'
                    dark: '#fbjs[data-appearance="dark"] { --acme-ink: #e6eef5; }'
              clearCustomCss:
                summary: Clear the survey's custom CSS
                value:
                  customCss: null
              replaceBlocks:
                summary: Replace survey blocks
                value:
                  blocks:
                    - id: clbk1234567890123456789012
                      name: Main Block
                      elements:
                        - id: satisfaction
                          type: rating
                          headline:
                            en-US: How satisfied are you?
                            de-DE: Wie zufrieden sind Sie?
                          required: true
                          range: 5
                          scale: smiley
      responses:
        '200':
          description: Survey updated successfully
          headers:
            X-Request-Id:
              schema:
                type: string
              description: Request correlation ID
            Cache-Control:
              schema:
                type: string
              example: private, no-store
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/SurveyResource'
                  warnings:
                    $ref: '#/components/schemas/CustomCssWarnings'
        '400':
          description: >
            Bad Request — the patch failed schema validation, i.e. any rule
            checkable from the request body alone, without consulting stored
            state. Covers: malformed JSON; an unsupported query parameter;
            unknown or unsupported fields (`distribution`/`targeting` are
            `app`-only — sending them when patching a `link` survey is
            rejected); wrong types or out-of-range values; bad enum values;
            malformed multilingual maps; and intra-document field-combination
            rules — notably `displayLimit` is required (must be >= 1) when
            `displayOption` is `displaySome`. Cross-reference failures that need
            stored state to detect return **422** instead. Every offending field
            is itemized in `invalid_params`. Unknown, forbidden, immutable, or
            wrongly-combined fields carry `code: unsupported_field` (e.g.
            `distribution`/`targeting` on a link survey; a changed server-owned
            field such as `type` is **422** `read_only_field`, not this);
            omissions carry `code: missing_required_field`. Bare type, range,
            enum, and malformed-locale-map violations report the field `name`
            with a human-readable `reason` and no machine `code`.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '401':
          $ref: '#/components/responses/V3Unauthorized'
        '403':
          description: >-
            Forbidden — no write access, missing external URL permission,
            missing Contacts entitlement for app-survey targeting, or survey
            does not exist (404 not used; avoids existence leak). Adding or
            editing `customCss` on an organization without the Scale plan
            (Formbricks Cloud) is refused with `code: custom_css_plan_required`;
            clearing it, or a patch that leaves it unchanged, is not.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: >-
            Conflict — a round-tripped `updatedAt` did not match the stored
            survey, so it changed since your read. Nothing was written.
            `details` carries `expectedUpdatedAt` and the live
            `currentUpdatedAt`, so a client can re-read and retry without
            guessing.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              example:
                title: Conflict
                status: 409
                detail: >-
                  Survey was modified since it was last read; updatedAt is now
                  2026-04-21T11:30:00.000Z. Re-read the survey and retry.
                code: conflict
                requestId: 6f1f7f3e-3b0e-4a1a-9f0a-7c9a1d2b3c4d
                details:
                  expectedUpdatedAt: '2026-04-21T10:00:00.000Z'
                  currentUpdatedAt: '2026-04-21T11:30:00.000Z'
        '422':
          description: >-
            Unprocessable Content — the request cannot be processed against
            stored state. Five cases:

            1. The target survey is archived (soft-deleted) and therefore
            read-only; restore it before editing. The `invalid_params` entry
            reports `name: archivedAt` with the reason `This survey is archived.
            Restore it before editing.`

            2. The patched document passed schema validation but failed a
            cross-reference check that requires stored state to detect: an
            undeclared locale key used in content, a duplicate stable id, an
            immutable element-id change on a published survey, a dangling
            logic/jump reference, an invalid media URL, a
            `distribution.triggers[].actionClassId` referencing an action class
            that does not exist in the workspace (discover valid ids via `GET
            /api/v3/action-classes`), or a `targeting.filters` entry that cannot
            resolve in the workspace — an unknown contact-attribute key or
            segment, an unsupported person identifier, or an unknown device
            value. The `invalid_params` array pinpoints each issue (e.g. `code:
            invalid_reference` with the offending `identifier`). A patch that
            newly *introduces* a forward reference — a recall of an answer not
            yet given, a logic condition reading a later block, or a
            `requireAnswer` target that is not in a later block — is refused
            with `code: misordered_reference`; a survey that already contains
            one stays patchable, only newly created violations are rejected. A
            patch that newly *introduces* a loop in the conditional logic —
            jumps, fallbacks and next blocks leading back on themselves, so a
            respondent could never reach an ending — is refused the same way,
            reported at the `logic` of each block a new looping step leaves
            ("Cyclic logic detected in block N"). A survey that already loops
            stays patchable, and blocks that only lead into a loop are fine.


            3. A server-owned field was echoed back with a different value —
            `code: read_only_field`, with the submitted value in `identifier`.
            Sending the value `GET` returned is always a no-op.


            4. The *stored* survey cannot be read as a v3 document at all —
            `code: stored_survey_invalid`. The reported paths are into the
            survey, not into the request; repair it in the editor. A stored
            survey that reads fine but already fails validation (a missing
            translation, a dangling recall) stays patchable: only issues a patch
            newly introduces are refused. The exception is publishing: moving a
            `draft` out of `draft` still requires a survey that passes, and is
            refused as `stored_survey_invalid` while it does not.


            5. `customCss` could not be processed (a syntax error, the 20
            000-byte survey budget, a complexity limit). `invalid_params` names
            `customCss.light` / `customCss.dark`; `details.errors` carries the
            processor's located errors. Nothing is written, so the saved CSS
            stays in effect.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              examples:
                archived:
                  summary: Survey is archived (restore before editing)
                  value:
                    type: about:blank
                    title: Unprocessable Content
                    status: 422
                    detail: Survey is archived
                    code: unprocessable_content
                    requestId: req_clsv1234567890123456789012
                    invalid_params:
                      - name: archivedAt
                        reason: This survey is archived. Restore it before editing.
                invalidReference:
                  summary: Cross-reference validation failed
                  value:
                    type: about:blank
                    title: Unprocessable Content
                    status: 422
                    detail: Survey document failed validation
                    code: unprocessable_content
                    requestId: req_clsv1234567890123456789012
                    invalid_params:
                      - name: blocks[0].logic[0].target
                        reason: References a block that does not exist in the survey.
                        code: invalid_reference
                        identifier: e0tfwzqk63op37y14z95qq3k
        '429':
          $ref: '#/components/responses/V3TooManyRequests'
        '500':
          $ref: '#/components/responses/V3InternalServerError'
      security:
        - sessionAuth: []
        - apiKeyAuth: []
        - bearerAuth: []
components:
  schemas:
    PatchSurveyRequest:
      type: object
      minProperties: 1
      description: >
        Patch payload shape. Top-level fields are partial; any provided nested
        object or array

        fully replaces that subtree. Omitted top-level fields are preserved.


        The server-owned fields `GET` returns — `id`, `workspaceId`, `type`,
        `createdAt`, `updatedAt`, `archivedAt`, `defaultLanguage` and
        `languages[].alias` — are accepted and verified, so a survey fetched
        with `GET` can be sent straight back. Echoing the value you were given
        is a no-op; changing one is a **422** `read_only_field`. `visibility`,
        `owner` and `access` are accepted the same way, but a changed value is a
        **422** `unsupported_field`: visibility changes go through `POST
        /api/v3/surveys/{surveyId}/visibility`. A round-tripped `updatedAt`
        additionally acts as an optimistic-concurrency precondition, and a stale
        one is rejected with **409**. `questions`, analytics, styling and
        scripts remain rejected outright. App surveys (stored `type: app`)
        additionally accept `distribution` and `targeting`; both are rejected
        for link surveys, and changing `targeting` filters requires the Contacts
        entitlement.


        `metadata`, `languages`, `welcomeCard`, `blocks`, `endings`,
        `hiddenFields`, and `variables`

        replace their full top-level value when provided. Missing ids in
        replaced arrays are deletions.

        Non-default locale keys in translatable maps must be declared in
        `languages`; undeclared locale

        keys are rejected with `unsupported_locale`. To remove a language,
        remove it from both

        `languages` and every translatable map that contains that locale key.

        Use `/api/v3/surveys/validate` with `operation: patch` before committing
        automated or agentic

        changes.
      properties:
        id:
          type: string
          description: Read-only echo. Must match the survey being patched.
        workspaceId:
          type: string
          description: Read-only echo. A survey cannot be moved between workspaces.
        type:
          type: string
          enum:
            - link
            - app
          description: Read-only echo. Immutable after creation.
        createdAt:
          type: string
          format: date-time
          description: Read-only echo of the creation timestamp.
        updatedAt:
          type: string
          format: date-time
          description: >-
            Read-only echo, and an optimistic-concurrency precondition: the
            write is applied only if the survey has not changed since the read
            this value came from, and is otherwise a **409**.
        archivedAt:
          type:
            - string
            - 'null'
          format: date-time
          description: Read-only echo. Use `/archive` and `/restore` to change it.
        visibility:
          allOf:
            - $ref: '#/components/schemas/SurveyVisibility'
          description: >-
            Read-only echo. Accepted only unchanged, so a survey fetched with
            `GET` can be sent back; any other value is a **422**
            `unsupported_field`. Change it with `POST
            /api/v3/surveys/{surveyId}/visibility`.
        owner:
          anyOf:
            - $ref: '#/components/schemas/SurveyOwner'
            - type: 'null'
          description: >-
            Read-only echo, accepted only unchanged (otherwise **422**
            `unsupported_field`).
        access:
          allOf:
            - $ref: '#/components/schemas/SurveyAccess'
          description: >-
            Read-only echo, accepted only unchanged (otherwise **422**
            `unsupported_field`).
        defaultLanguage:
          type: string
          description: >-
            Read-only echo of the survey default language, in whatever shape
            `GET` returned — legacy surveys carry language-only codes, so this
            is not constrained to `LocaleCode`.
        name:
          type: string
          minLength: 1
        status:
          type: string
          enum:
            - draft
            - inProgress
            - paused
            - completed
        metadata:
          allOf:
            - $ref: '#/components/schemas/SurveyMetadata'
        languages:
          type: array
          maxItems: 50
          items:
            $ref: '#/components/schemas/PatchSurveyLanguage'
          description: >
            Replaces the survey language configuration. The default language
            cannot be changed through

            PATCH; any `default: true` entry must match the existing default
            language. Languages referenced

            by `metadata`, `welcomeCard`, `blocks`, or `endings` translatable
            maps must be explicitly

            declared here, so remove the locale key from those maps as well when
            intentionally removing

            a language.
        showLanguageSwitch:
          type:
            - boolean
            - 'null'
          description: >-
            Show respondents a language switch on multi-language surveys. `null`
            and `false` both mean off. Omit to keep the stored value.
        autoSelectLanguage:
          type:
            - boolean
            - 'null'
          description: >-
            Open link and app surveys in the respondent's browser language when
            it matches an enabled survey language; otherwise the default
            language is used. `null` and `false` both mean off. Omit to keep the
            stored value.
        welcomeCard:
          $ref: '#/components/schemas/SurveyWelcomeCard'
        blocks:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            $ref: '#/components/schemas/SurveyBlock'
        endings:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/SurveyEnding'
        hiddenFields:
          $ref: '#/components/schemas/SurveyHiddenFields'
        variables:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/SurveyVariable'
        distribution:
          allOf:
            - $ref: '#/components/schemas/SurveyDistribution'
          description: >
            App-survey runtime/display settings. Only valid for stored `type:
            app`. Replaces the full distribution; omitted scalars reset to their
            defaults.
        targeting:
          allOf:
            - $ref: '#/components/schemas/SurveyTargeting'
          description: App-survey contact targeting. Only valid for stored app surveys.
        customCss:
          anyOf:
            - $ref: '#/components/schemas/CustomCssInput'
            - type: 'null'
          description: >-
            Survey custom CSS source. Omitted: unchanged. `null`: clears both
            fields. An object replaces both fields and needs both keys. Change
            is judged on normalized source, so echoing the value `GET` returned
            is a no-op that needs no plan. Adding or editing needs the Scale
            plan on Formbricks Cloud (403 `custom_css_plan_required`); clearing
            never does. A failed write keeps the saved CSS. Only an addition or
            edit is processed, so only it counts against the custom CSS budget
            of 60 per minute per user or API key, shared with custom CSS
            validation; beyond it the patch is a 429.
      additionalProperties: false
    SurveyResource:
      allOf:
        - type: object
          required:
            - id
            - workspaceId
            - createdAt
            - updatedAt
            - archivedAt
            - name
            - type
            - status
            - metadata
            - defaultLanguage
            - languages
            - showLanguageSwitch
            - autoSelectLanguage
            - welcomeCard
            - blocks
            - endings
            - hiddenFields
            - variables
            - customCss
          properties:
            id:
              type: string
            workspaceId:
              type: string
            createdAt:
              type: string
              format: date-time
            updatedAt:
              type: string
              format: date-time
            archivedAt:
              type:
                - string
                - 'null'
              format: date-time
              description: >-
                Soft-delete/archive marker (ISO 8601); `null` when the survey is
                active.
            name:
              type: string
            type:
              type: string
              enum:
                - link
                - app
            status:
              type: string
              enum:
                - draft
                - inProgress
                - paused
                - completed
            metadata:
              anyOf:
                - $ref: '#/components/schemas/SurveyMetadata'
                - type: 'null'
            defaultLanguage:
              type: string
              description: >-
                Emitted language code/tag for the survey default language. The
                internal `default` translation key is never exposed.
            languages:
              type: array
              items:
                $ref: '#/components/schemas/SurveyLanguage'
            showLanguageSwitch:
              type:
                - boolean
                - 'null'
              description: >-
                Show respondents a language switch on multi-language surveys.
                `null` and `false` both mean off.
            autoSelectLanguage:
              type:
                - boolean
                - 'null'
              description: >-
                Open link and app surveys in the respondent's browser language
                when it matches an enabled survey language; otherwise the
                default language is used. `null` and `false` both mean off.
            welcomeCard:
              $ref: '#/components/schemas/SurveyWelcomeCard'
            blocks:
              type: array
              items:
                $ref: '#/components/schemas/SurveyBlock'
            endings:
              type: array
              items:
                $ref: '#/components/schemas/SurveyEnding'
            hiddenFields:
              $ref: '#/components/schemas/SurveyHiddenFields'
            variables:
              type: array
              items:
                $ref: '#/components/schemas/SurveyVariable'
            distribution:
              allOf:
                - $ref: '#/components/schemas/SurveyDistribution'
              description: >-
                App-survey runtime/display settings. Present only for app
                surveys; omitted for link surveys.
            targeting:
              allOf:
                - $ref: '#/components/schemas/SurveyTargeting'
              description: >-
                App-survey contact targeting. Present only for app surveys;
                omitted for link surveys.
            customCss:
              anyOf:
                - $ref: '#/components/schemas/CustomCssInput'
                - type: 'null'
              description: >-
                The survey's custom CSS **source** (ENG-3641), `null` when it
                has none. Compiled output and the processor version are internal
                and never exposed here. The full resource may be sent back with
                PATCH: unchanged source is not reprocessed and needs no plan.
        - $ref: '#/components/schemas/SurveyVisibilityFields'
    CustomCssWarnings:
      type: array
      description: >-
        Processing warnings for this write: constructs the processor removed.
        Additive, returned beside `data` (never inside it, so `data` can be sent
        back unchanged); present only when the write processed custom CSS.
      maxItems: 50
      items:
        $ref: '#/components/schemas/CustomCssWarning'
    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'
    SurveyVisibility:
      type: string
      enum:
        - restricted
        - workspace
      description: >-
        `restricted`: readable by the owner and the organisation's owners and
        managers only. The owner's access is conditional on their workspace
        access, level for level: they read the survey only while they can read
        the workspace, and edit it only while they can write to it — being the
        owner grants nothing the workspace does not. `workspace`: readable by
        everyone with access to the workspace — today's behaviour.
    SurveyOwner:
      type: object
      description: >-
        The authorization owner — the person whose restricted surveys these are.
        Distinct from `creator`, which is attribution only and is never
        rewritten. Display only, like `creator`: no user id is exposed, because
        `access.via === "owner"` already answers "is this mine" and
        `filter[owner][in]=me` filters server-side. Ownership transfer can add
        an identifier later, additively. Durable source: `Survey.ownerId` in
        PostgreSQL, projected to `survey#owner@user`; the graph never holds
        ownership alone, so a rebuild from an empty graph restores it.
      required:
        - name
      properties:
        name:
          type: string
      additionalProperties: false
    SurveyAccess:
      type: object
      description: >-
        Why **this caller** can see the survey, and whether they may change its
        visibility. Derived by the server per request; a rendering hint only —
        every mutation is re-authorised on the server.
      required:
        - via
        - canManageVisibility
      properties:
        via:
          type: string
          description: >-
            Why this caller can see the survey. Known values today: `workspace`
            (the survey is workspace-visible and the caller has workspace read —
            always the value for API keys), `owner` (restricted, the caller owns
            it), `organizationRole` (restricted, the caller reaches it only as
            an organisation owner or manager — the case that shows the admin
            banner). **Open set, deliberately not an enum**: named person and
            team sharing will add values, so clients treat an unknown string as
            "some other access path" and never branch exhaustively. Explanatory
            metadata, not authorization truth — the server re-authorises every
            call.
          examples:
            - workspace
            - owner
            - organizationRole
        canManageVisibility:
          type: boolean
          description: >-
            Permission only: `true` when this caller holds
            `survey.change_visibility` — owner or organisation owner/manager,
            feature enabled for the organisation and deployment. It can be
            `true` while `allowedTargets` is empty (blockers, or no owner): it
            answers "may this person manage visibility at all", which is what
            shows or hides the controls; `allowedTargets` on `GET …/visibility`
            answers which transitions would succeed now. Stays `true` for an
            organisation manager on an ownerless restricted survey.
      additionalProperties: false
    SurveyMetadata:
      type: object
      description: >
        Arbitrary JSON survey context for customer- or operation-specific
        metadata. v3 preserves

        arbitrary metadata values as-is. If present, `title` and `description`
        are treated as

        translatable text maps and returned with real locale-code keys.
      properties:
        title:
          $ref: '#/components/schemas/TranslatableText'
        description:
          $ref: '#/components/schemas/TranslatableText'
      additionalProperties: true
    PatchSurveyLanguage:
      type: object
      description: >-
        Language entry accepted by `PATCH /api/v3/surveys/{surveyId}`. Identical
        to the create shape plus `alias`, which `GET` returns so a round-tripped
        body carries it. `alias` belongs to the workspace language rather than
        the survey, so it is verified rather than applied: echoing the value
        `GET` returned is a no-op, and a different one is rejected with
        `read_only_field`.
      required:
        - code
      properties:
        code:
          type: string
          description: >-
            Server-emitted survey language code/tag, echoed from `GET`.
            Deliberately not `LocaleCode`: existing surveys carry language-only
            and script-only codes (`vi`, `gu`), and a read-only echo has to
            accept whatever the read returned. New languages are still created
            through the strict shape.
          example: en-US
        default:
          type: boolean
          description: >-
            Optional marker for readability; only the `defaultLanguage` entry
            may set this to true.
        enabled:
          type: boolean
          default: true
          description: Whether this language is enabled for respondent-facing delivery.
        alias:
          type:
            - string
            - 'null'
          description: >-
            Workspace-level alias for the language. Read-only; echo the value
            `GET` returned.
      additionalProperties: false
    SurveyWelcomeCard:
      type: object
      description: Optional card shown before the first survey block.
      required:
        - enabled
      properties:
        enabled:
          type: boolean
        headline:
          $ref: '#/components/schemas/TranslatableText'
        subheader:
          $ref: '#/components/schemas/TranslatableText'
        buttonLabel:
          $ref: '#/components/schemas/TranslatableText'
        fileUrl:
          type: string
        videoUrl:
          type: string
        timeToFinish:
          type: boolean
        showResponseCount:
          type: boolean
      additionalProperties: false
    SurveyBlock:
      type: object
      description: >
        Block-based survey section. Block ids are stable public identifiers.
        Logic and fallbacks can

        jump to block ids or ending ids, so clients and agents should preserve
        ids unless intentionally

        creating/deleting a block.
      required:
        - id
        - name
        - elements
      properties:
        id:
          type: string
          format: cuid2
          description: Stable block id.
        name:
          type: string
          minLength: 1
        elements:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/SurveyElement'
        logic:
          type: array
          items:
            $ref: '#/components/schemas/SurveyBlockLogic'
        logicFallback:
          type: string
          format: cuid2
          description: >
            Block or ending id used when no logic condition matches. Only valid
            when this same block

            has at least one `logic` rule; omit it for normal sequential flow.
        buttonLabel:
          $ref: '#/components/schemas/TranslatableText'
        backButtonLabel:
          $ref: '#/components/schemas/TranslatableText'
      additionalProperties: false
    SurveyEnding:
      oneOf:
        - $ref: '#/components/schemas/SurveyEndScreenEnding'
        - $ref: '#/components/schemas/SurveyRedirectEnding'
      discriminator:
        propertyName: type
        mapping:
          endScreen: '#/components/schemas/SurveyEndScreenEnding'
          redirectToUrl: '#/components/schemas/SurveyRedirectEnding'
      description: >
        Ending reached after the last block or a jump action. `type` selects the
        allowed shape;

        unsupported fields are rejected instead of ignored.
    SurveyHiddenFields:
      type: object
      description: >
        Hidden fields, sometimes called embedded data in other survey products.
        Field ids are stable public identifiers and may be referenced by logic,
        recall, quotas, integrations, and response data.


        **Naming rule for new field ids** — the same rule the survey editor
        applies (ENG-2539): a new id must start with a lowercase letter and then
        contain only lowercase letters, numbers and underscores
        (`^[a-z][a-z0-9_]*$`), and must not take a reserved name. Reserved are
        the link-survey system params and internal ids — `userId`, `source`,
        `suid`, `end`, `start`, `welcomeCard`, `hidden`, `verifiedEmail`,
        `multiLanguage`, `embed`, `verify`, `suToken`, `lang`, `preview`,
        `startAt`, `skipPrefilled`, `offlineSupport` (matched
        case-insensitively) — and the names of auto-captured system fields,
        which every survey can already read without declaring them: `source`,
        `url`, `country`, `action`, `browser`, `os`, `deviceType`, `ipAddress`,
        `finished`, `language`, `locale`, `responseId`, `surveyId`,
        `durationSeconds`, `startedAt`, `finishedAt`, `pagePath`,
        `pageReferrer`, `utmSource`, `utmMedium`, `utmCampaign`, `utmTerm`,
        `utmContent`, `screenWidth`, `screenHeight`, `viewportWidth`,
        `viewportHeight`, `timezone`.


        **Ids a survey already declares are grandfathered**: they always load,
        and re-sending the survey unchanged never fails validation — only
        *adding* such a name as a new field is refused. Deleting a grandfathered
        field spends the reprieve: once it is gone from the saved survey, the
        name can no longer be re-added. Note this means re-creating a survey
        from an exported document can fail with a 400 when the export declares a
        name the rule refuses; rename the field in the payload before importing.
        The `pattern` below describes what stored surveys may hold (the
        pre-existing lenient charset), not what a new id may look like.
      required:
        - enabled
      properties:
        enabled:
          type: boolean
          description: >
            Derived, not stored: `true` whenever the survey declares at least
            one hidden field. A value sent on create or update is ignored; to
            turn hidden fields off, remove them from `fieldIds`.
        fieldIds:
          type: array
          maxItems: 200
          items:
            type: string
            pattern: ^[a-zA-Z0-9_-]+$
          uniqueItems: true
      additionalProperties: false
    SurveyVariable:
      oneOf:
        - $ref: '#/components/schemas/SurveyNumberVariable'
        - $ref: '#/components/schemas/SurveyTextVariable'
      description: >
        Survey variable. Variable ids are stable references used by logic and
        calculation actions.

        Variable names are human-readable labels and must be unique within the
        survey.
    SurveyDistribution:
      type: object
      description: >
        App-survey runtime/display settings. Only valid for `type: app`;
        rejected for link surveys.

        **Replacement semantics (destructive):** when `distribution` is provided
        on PATCH it fully replaces the stored object — any sub-field you omit is
        reset to its default, NOT preserved. In particular, omitting `triggers`
        removes ALL existing triggers, and omitting `displayOption` resets it to
        `displayOnce`. To change one setting, send the complete desired
        distribution. To leave distribution untouched, omit the whole
        `distribution` key.
      properties:
        displayOption:
          type: string
          enum:
            - displayOnce
            - displayMultiple
            - respondMultiple
            - displaySome
          default: displayOnce
          description: >
            How often the survey may be shown to a contact. `displaySome` shows
            it up to `displayLimit` times (or until the contact responds,
            whichever comes first).
        displayPercentage:
          type:
            - number
            - 'null'
          minimum: 0.01
          maximum: 100
          default: null
          description: >
            Independent throttle: show the survey to only this percentage of
            triggered contacts. Optional and valid with any `displayOption`;
            null/omitted means no throttle (shown to everyone).
        displayLimit:
          type:
            - integer
            - 'null'
          minimum: 0
          default: null
          description: >
            Maximum number of times the survey is shown to a single contact.
            Required (must be >= 1) when `displayOption` is `displaySome`;
            otherwise optional.
        recontactDays:
          type:
            - integer
            - 'null'
          minimum: 0
          default: null
          description: >-
            Days to wait before showing this survey again to the same contact.
            Null uses the workspace default.
        autoClose:
          type:
            - integer
            - 'null'
          minimum: 0
          default: null
          description: >-
            Seconds of inactivity after which the survey widget auto-closes.
            Null disables auto-close.
        autoComplete:
          type:
            - integer
            - 'null'
          minimum: 1
          default: null
          description: >-
            Automatically stop collecting responses after this many completed
            responses. Null disables it.
        delay:
          type: integer
          minimum: 0
          default: 0
          description: Seconds to wait after a trigger fires before showing the survey.
        triggers:
          type: array
          default: []
          maxItems: 50
          items:
            $ref: '#/components/schemas/SurveyTrigger'
          description: >
            Action classes that trigger the survey. Every id must reference an
            existing workspace action class. On PATCH this list fully replaces
            the survey's triggers — omitting it (or sending `[]`) removes all
            existing triggers.
      additionalProperties: false
      example:
        displayOption: displaySome
        displayLimit: 3
        displayPercentage: null
        recontactDays: 7
        autoClose: null
        autoComplete: null
        delay: 0
        triggers:
          - actionClassId: clyy1234567890123456789012
    SurveyTargeting:
      type: object
      description: >
        App-survey contact targeting. Only valid for `type: app`. `filters: []`
        targets everyone. Setting or changing non-empty filters requires the
        Contacts entitlement; otherwise create/patch returns 403.


        `attribute` filters reference workspace contact-attribute keys (discover
        them via `GET /api/v3/contact-attribute-keys`) and `segment` filters
        reference other segment ids — both are workspace-scoped resources you
        must obtain before constructing filters (an invalid key/id targets no
        one rather than erroring). See `SegmentFilter` for the per-`root.type`
        operator sets.
      required:
        - filters
      properties:
        filters:
          $ref: '#/components/schemas/SegmentFilters'
      additionalProperties: false
    CustomCssInput:
      type: object
      description: >-
        Custom CSS **source** as a caller writes it (ENG-3641). Both keys are
        required. `light` is base CSS that applies in both appearances; `dark`
        adds overrides on top of it when the survey renders dark. `null`, an
        empty string or whitespace means "no CSS" for that field. Source only:
        compiled output and the processor version are computed by the server on
        every write, and a request that supplies either is rejected. The
        combined source is limited to 100 000 UTF-8 bytes for a workspace and 20
        000 for a survey; larger CSS is a **422** with error code
        `source_too_large`.
      required:
        - light
        - dark
      properties:
        light:
          type:
            - string
            - 'null'
          description: Base CSS, applied in light and dark appearance.
        dark:
          type:
            - string
            - 'null'
          description: Additional CSS applied only in dark appearance.
      additionalProperties: false
    SurveyLanguage:
      type: object
      description: >
        Language configured for this survey. GET responses expose the
        server-emitted code/tag used as the

        translatable map key. Existing surveys can use region-qualified,
        language-only, or script-only codes.

        Disabled languages can still be read by the management API so unfinished
        translations can be completed.
      required:
        - code
        - default
        - enabled
      properties:
        code:
          type: string
          description: >-
            Server-emitted survey language code/tag used as the translatable map
            key.
          example: en-US
        alias:
          type:
            - string
            - 'null'
          description: >-
            Optional configured alias accepted by `?lang` for compatibility and
            agent discovery.
          example: english
        default:
          type: boolean
          description: Whether this is the default authoring language.
        enabled:
          type: boolean
          description: Whether this language is enabled for respondent-facing delivery.
    SurveyVisibilityFields:
      type: object
      description: >-
        The three fields every `SurveyListItem` and `SurveyResource` carries,
        merged into both via `allOf`. All required, always present.
      required:
        - visibility
        - owner
        - access
      properties:
        visibility:
          allOf:
            - $ref: '#/components/schemas/SurveyVisibility'
          description: >-
            The **enforced** visibility on this request. `restricted` whenever a
            change is pending, in either direction (fail closed). `workspace`
            for every survey while the deployment's readiness marker is unset,
            whatever the stored flag, and always `workspace` for API keys.
        owner:
          anyOf:
            - $ref: '#/components/schemas/SurveyOwner'
            - type: 'null'
          description: >-
            `null` when the author's account no longer exists (`Survey.ownerId`
            is set null on user deletion) or the survey was created with an API
            key. A survey with `owner: null` cannot be restricted; an
            organisation owner/manager can still make it workspace-visible.
        access:
          $ref: '#/components/schemas/SurveyAccess'
    CustomCssWarning:
      type: object
      description: >-
        A construct the custom CSS processor removed; the rest of the CSS still
        applies. `unknown_property` and `invalid_value` are notes instead: the
        declaration stays, but browsers ignore it. The `code` is stable — branch
        on it, not on `reason`, which never echoes customer source beyond a
        short construct name.
      required:
        - code
        - scope
        - appearance
        - line
        - column
        - reason
      properties:
        code:
          type: string
          enum:
            - import_removed
            - font_face_removed
            - external_resource_removed
            - unsupported_at_rule_removed
            - unsafe_property_removed
            - unsafe_value_removed
            - fixed_position_removed
            - unsafe_selector_removed
            - unknown_property
            - invalid_value
        scope:
          type: string
          enum:
            - workspace
            - survey
        appearance:
          type:
            - string
            - 'null'
          enum:
            - light
            - dark
            - null
          description: >-
            Source field the issue is in; `null` when it concerns the scope as a
            whole.
        line:
          type:
            - integer
            - 'null'
          minimum: 1
          description: >-
            1-based line in that field's source; `null` when there is no single
            location.
        column:
          type:
            - integer
            - 'null'
          minimum: 1
        reason:
          type: string
      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
    TranslatableText:
      allOf:
        - $ref: '#/components/schemas/TranslatableTextMap'
      description: >
        Survey authoring text. `GET /api/v3/surveys/{surveyId}` always returns
        maps keyed by the emitted

        `languages[].code` values for this survey. Use `?lang=` to filter which
        language keys are included.

        The internal storage key `default` is never exposed by v3.
      examples:
        - en-US: What should we improve?
          de-DE: Was sollten wir verbessern?
    SurveyElement:
      oneOf:
        - $ref: '#/components/schemas/SurveyOpenTextElement'
        - $ref: '#/components/schemas/SurveyConsentElement'
        - $ref: '#/components/schemas/SurveyMultipleChoiceSingleElement'
        - $ref: '#/components/schemas/SurveyMultipleChoiceMultiElement'
        - $ref: '#/components/schemas/SurveyNpsElement'
        - $ref: '#/components/schemas/SurveyCtaElement'
        - $ref: '#/components/schemas/SurveyRatingElement'
        - $ref: '#/components/schemas/SurveyPictureSelectionElement'
        - $ref: '#/components/schemas/SurveyDateElement'
        - $ref: '#/components/schemas/SurveyFileUploadElement'
        - $ref: '#/components/schemas/SurveyCalElement'
        - $ref: '#/components/schemas/SurveyMatrixElement'
        - $ref: '#/components/schemas/SurveyAddressElement'
        - $ref: '#/components/schemas/SurveyRankingElement'
        - $ref: '#/components/schemas/SurveyContactInfoElement'
        - $ref: '#/components/schemas/SurveyCsatElement'
        - $ref: '#/components/schemas/SurveyCesElement'
      discriminator:
        propertyName: type
        mapping:
          openText: '#/components/schemas/SurveyOpenTextElement'
          consent: '#/components/schemas/SurveyConsentElement'
          multipleChoiceSingle: '#/components/schemas/SurveyMultipleChoiceSingleElement'
          multipleChoiceMulti: '#/components/schemas/SurveyMultipleChoiceMultiElement'
          nps: '#/components/schemas/SurveyNpsElement'
          cta: '#/components/schemas/SurveyCtaElement'
          rating: '#/components/schemas/SurveyRatingElement'
          pictureSelection: '#/components/schemas/SurveyPictureSelectionElement'
          date: '#/components/schemas/SurveyDateElement'
          fileUpload: '#/components/schemas/SurveyFileUploadElement'
          cal: '#/components/schemas/SurveyCalElement'
          matrix: '#/components/schemas/SurveyMatrixElement'
          address: '#/components/schemas/SurveyAddressElement'
          ranking: '#/components/schemas/SurveyRankingElement'
          contactInfo: '#/components/schemas/SurveyContactInfoElement'
          csat: '#/components/schemas/SurveyCsatElement'
          ces: '#/components/schemas/SurveyCesElement'
      description: >
        Survey element/question inside a block. Element ids are stable public
        identifiers used by

        logic, recall strings, response data, quotas, integrations, and
        analysis. `type` selects the

        allowed shape; unsupported fields are rejected instead of ignored.
    SurveyBlockLogic:
      type: object
      description: Conditional logic rule evaluated at block level.
      required:
        - id
        - conditions
        - actions
      properties:
        id:
          type: string
          format: cuid2
        conditions:
          $ref: '#/components/schemas/SurveyConditionGroup'
        actions:
          type: array
          items:
            $ref: '#/components/schemas/SurveyLogicAction'
      additionalProperties: false
    SurveyEndScreenEnding:
      type: object
      description: Visual end screen displayed after survey completion.
      required:
        - id
        - type
      properties:
        id:
          type: string
          format: cuid2
          description: Stable ending id. `jumpToBlock.target` may point to this id.
        type:
          type: string
          enum:
            - endScreen
        headline:
          $ref: '#/components/schemas/TranslatableText'
        subheader:
          $ref: '#/components/schemas/TranslatableText'
        buttonLabel:
          $ref: '#/components/schemas/TranslatableText'
        buttonLink:
          type: string
        imageUrl:
          type: string
        imageAltText:
          allOf:
            - $ref: '#/components/schemas/TranslatableText'
          description: >-
            Text alternative for `imageUrl`, read by screen readers. Omit or
            leave empty for a decorative image (rendered as `alt=""`).
        videoUrl:
          type: string
        hideDefaultIcon:
          type: boolean
          description: >-
            Hides the default checkmark icon rendered above the headline when
            the ending carries no image or video. Omit to keep the icon.
      additionalProperties: false
    SurveyRedirectEnding:
      type: object
      description: >
        Redirects the respondent to a URL after survey completion. External
        redirects require the

        organization's external URL permission; otherwise write endpoints return
        `403 Forbidden`.
      required:
        - id
        - type
        - url
      properties:
        id:
          type: string
          format: cuid2
          description: Stable ending id. `jumpToBlock.target` may point to this id.
        type:
          type: string
          enum:
            - redirectToUrl
        url:
          type: string
          format: uri
          description: >-
            External redirect URL. Requires the organization's external URL
            permission.
        label:
          type: string
          description: Optional internal label for redirect endings.
      additionalProperties: false
    SurveyNumberVariable:
      type: object
      description: >
        Number variable. Used by `calculate` logic actions with numeric
        operators such as `add`,

        `subtract`, `multiply`, `divide`, or `assign`.
      required:
        - id
        - name
        - type
        - value
      properties:
        id:
          type: string
          format: cuid2
          description: Stable variable id referenced from logic.
        name:
          type: string
          pattern: ^[a-z0-9_]+$
          description: >
            Unique variable name. `pattern` is the charset of a *stored* name; a
            newly declared one is held to more than that, and the extra rules
            split into two groups that behave differently.


            **400 `forbidden_identifier`, grandfathered.** A new name must match
            `^[a-z][a-z0-9_]*$` (no leading digit or underscore) and must not
            take a reserved name — the link-survey system params and internal
            ids (`userId`, `source`, `suid`, `end`, `start`, `welcomeCard`,
            `hidden`, `verifiedEmail`, `multiLanguage`, `embed`, `verify`,
            `suToken`, `lang`, `preview`, `startAt`, `skipPrefilled`,
            `offlineSupport`) or an auto-captured system field name (`country`,
            `browser`, `deviceType`, …; `SurveyHiddenFields` carries the full
            list, and the two sets are the same there). Names the survey already
            declares are grandfathered against these, so re-sending an existing
            document never trips them.


            **400 `duplicate_identifier`, not grandfathered.** A variable name
            that collides case-insensitively with a hidden field id on the same
            survey. This is checked by reference validation over the whole
            merged document rather than against the incoming names, so a survey
            that already holds such a clash is refused on **every** patch —
            including one that touches neither collection — until one side is
            renamed.
        type:
          type: string
          enum:
            - number
        value:
          type: number
          description: Default numeric value.
      additionalProperties: false
    SurveyTextVariable:
      type: object
      description: >
        Text variable. Used by `calculate` logic actions with text operators
        such as `assign` or `concat`.
      required:
        - id
        - name
        - type
        - value
      properties:
        id:
          type: string
          format: cuid2
          description: Stable variable id referenced from logic.
        name:
          type: string
          pattern: ^[a-z0-9_]+$
          description: >
            Unique variable name. `pattern` is the charset of a *stored* name; a
            newly declared one is held to more than that, and the extra rules
            split into two groups that behave differently.


            **400 `forbidden_identifier`, grandfathered.** A new name must match
            `^[a-z][a-z0-9_]*$` (no leading digit or underscore) and must not
            take a reserved name — the link-survey system params and internal
            ids (`userId`, `source`, `suid`, `end`, `start`, `welcomeCard`,
            `hidden`, `verifiedEmail`, `multiLanguage`, `embed`, `verify`,
            `suToken`, `lang`, `preview`, `startAt`, `skipPrefilled`,
            `offlineSupport`) or an auto-captured system field name (`country`,
            `browser`, `deviceType`, …; `SurveyHiddenFields` carries the full
            list, and the two sets are the same there). Names the survey already
            declares are grandfathered against these, so re-sending an existing
            document never trips them.


            **400 `duplicate_identifier`, not grandfathered.** A variable name
            that collides case-insensitively with a hidden field id on the same
            survey. This is checked by reference validation over the whole
            merged document rather than against the incoming names, so a survey
            that already holds such a clash is refused on **every** patch —
            including one that touches neither collection — until one side is
            renamed.
        type:
          type: string
          enum:
            - text
        value:
          type: string
          description: Default text value.
      additionalProperties: false
    SurveyTrigger:
      type: object
      description: >
        Reference to an existing workspace action class that triggers this app
        survey. Discover available action-class ids with `GET
        /api/v3/action-classes`.
      required:
        - actionClassId
      properties:
        actionClassId:
          type: string
          format: cuid2
          description: Id of an action class in the same workspace.
      additionalProperties: false
    SegmentFilters:
      type: array
      description: >
        Contact targeting filter tree. An empty array targets everyone. Each
        node joins the previous one via its `connector` (`and`/`or`; the first
        node's connector is null). A node's `resource` is either a single filter
        condition or a nested filter group (enabling parenthesized `and`/`or`
        logic).
      items:
        type: object
        required:
          - id
          - connector
          - resource
        properties:
          id:
            type: string
            format: cuid2
          connector:
            type:
              - string
              - 'null'
            enum:
              - and
              - or
              - null
            description: >-
              Logical connector joining this node to the previous one. Null for
              the first node.
          resource:
            oneOf:
              - $ref: '#/components/schemas/SegmentFilter'
              - $ref: '#/components/schemas/SegmentFilters'
            description: >-
              A single filter condition, or a nested filter group for grouped
              logic.
        additionalProperties: false
    TranslatableTextMap:
      type: object
      description: >-
        Multilingual text map keyed by the emitted `languages[].code` values for
        this survey.
      propertyNames:
        type: string
        description: >-
          Survey language code/tag, for example `en-US`, `de-DE`, or
          `zh-Hans-CN`.
      additionalProperties:
        type: string
    SurveyOpenTextElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          properties:
            type:
              type: string
              enum:
                - openText
            placeholder:
              $ref: '#/components/schemas/TranslatableText'
            longAnswer:
              type: boolean
            inputType:
              type: string
              enum:
                - text
                - email
                - url
                - number
                - phone
              default: text
            insightsEnabled:
              type: boolean
              default: false
            charLimit:
              $ref: '#/components/schemas/SurveyCharLimit'
            validation:
              $ref: '#/components/schemas/SurveyValidation'
      unevaluatedProperties: false
    SurveyConsentElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - label
          properties:
            type:
              type: string
              enum:
                - consent
            label:
              $ref: '#/components/schemas/TranslatableText'
            validation:
              $ref: '#/components/schemas/SurveyValidation'
      unevaluatedProperties: false
    SurveyMultipleChoiceSingleElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - choices
          properties:
            type:
              type: string
              enum:
                - multipleChoiceSingle
            choices:
              type: array
              minItems: 2
              items:
                $ref: '#/components/schemas/SurveyChoice'
            shuffleOption:
              $ref: '#/components/schemas/SurveyShuffleOption'
            otherOptionPlaceholder:
              $ref: '#/components/schemas/TranslatableText'
            displayType:
              $ref: '#/components/schemas/SurveyMultipleChoiceDisplayType'
      unevaluatedProperties: false
    SurveyMultipleChoiceMultiElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - choices
          properties:
            type:
              type: string
              enum:
                - multipleChoiceMulti
            choices:
              type: array
              minItems: 2
              items:
                $ref: '#/components/schemas/SurveyChoice'
            shuffleOption:
              $ref: '#/components/schemas/SurveyShuffleOption'
            otherOptionPlaceholder:
              $ref: '#/components/schemas/TranslatableText'
            validation:
              $ref: '#/components/schemas/SurveyValidation'
            displayType:
              $ref: '#/components/schemas/SurveyMultipleChoiceDisplayType'
      unevaluatedProperties: false
    SurveyNpsElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          properties:
            type:
              type: string
              enum:
                - nps
            lowerLabel:
              $ref: '#/components/schemas/TranslatableText'
            upperLabel:
              $ref: '#/components/schemas/TranslatableText'
            isColorCodingEnabled:
              type: boolean
              default: false
      unevaluatedProperties: false
    SurveyCtaElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          description: >-
            If `buttonExternal` is true, `buttonUrl` and `ctaButtonLabel` are
            required.
          properties:
            type:
              type: string
              enum:
                - cta
            buttonExternal:
              type: boolean
              default: false
            buttonUrl:
              type: string
            ctaButtonLabel:
              $ref: '#/components/schemas/TranslatableText'
        - if:
            required:
              - buttonExternal
            properties:
              buttonExternal:
                const: true
          then:
            required:
              - buttonUrl
              - ctaButtonLabel
      unevaluatedProperties: false
    SurveyRatingElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - scale
            - range
          properties:
            type:
              type: string
              enum:
                - rating
            scale:
              type: string
              enum:
                - number
                - smiley
                - star
            range:
              type: integer
              enum:
                - 3
                - 4
                - 5
                - 6
                - 7
                - 10
            lowerLabel:
              $ref: '#/components/schemas/TranslatableText'
            upperLabel:
              $ref: '#/components/schemas/TranslatableText'
            isColorCodingEnabled:
              type: boolean
              default: false
      unevaluatedProperties: false
    SurveyPictureSelectionElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - choices
          properties:
            type:
              type: string
              enum:
                - pictureSelection
            allowMulti:
              type: boolean
              default: false
            choices:
              type: array
              minItems: 2
              items:
                $ref: '#/components/schemas/SurveyPictureChoice'
            validation:
              $ref: '#/components/schemas/SurveyValidation'
      unevaluatedProperties: false
    SurveyDateElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - format
          properties:
            type:
              type: string
              enum:
                - date
            html:
              $ref: '#/components/schemas/TranslatableText'
            format:
              type: string
              enum:
                - M-d-y
                - d-M-y
                - y-M-d
            validation:
              $ref: '#/components/schemas/SurveyValidation'
      unevaluatedProperties: false
    SurveyFileUploadElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - allowMultipleFiles
          properties:
            type:
              type: string
              enum:
                - fileUpload
            allowMultipleFiles:
              type: boolean
            maxSizeInMB:
              type: number
            allowedFileExtensions:
              type: array
              items:
                type: string
            validation:
              $ref: '#/components/schemas/SurveyValidation'
      unevaluatedProperties: false
    SurveyCalElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - calUserName
          properties:
            type:
              type: string
              enum:
                - cal
            calUserName:
              type: string
              minLength: 1
            calHost:
              type: string
      unevaluatedProperties: false
    SurveyMatrixElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - rows
            - columns
          properties:
            type:
              type: string
              enum:
                - matrix
            rows:
              type: array
              items:
                $ref: '#/components/schemas/SurveyChoice'
            columns:
              type: array
              items:
                $ref: '#/components/schemas/SurveyChoice'
            shuffleOption:
              $ref: '#/components/schemas/SurveyShuffleOption'
            validation:
              $ref: '#/components/schemas/SurveyValidation'
      unevaluatedProperties: false
    SurveyAddressElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - addressLine1
            - addressLine2
            - city
            - state
            - zip
            - country
          properties:
            type:
              type: string
              enum:
                - address
            addressLine1:
              $ref: '#/components/schemas/SurveyToggleInputConfig'
            addressLine2:
              $ref: '#/components/schemas/SurveyToggleInputConfig'
            city:
              $ref: '#/components/schemas/SurveyToggleInputConfig'
            state:
              $ref: '#/components/schemas/SurveyToggleInputConfig'
            zip:
              $ref: '#/components/schemas/SurveyToggleInputConfig'
            country:
              $ref: '#/components/schemas/SurveyToggleInputConfig'
            validation:
              $ref: '#/components/schemas/SurveyValidation'
      unevaluatedProperties: false
    SurveyRankingElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - choices
          properties:
            type:
              type: string
              enum:
                - ranking
            choices:
              type: array
              minItems: 2
              maxItems: 25
              items:
                $ref: '#/components/schemas/SurveyChoice'
            otherOptionPlaceholder:
              $ref: '#/components/schemas/TranslatableText'
            shuffleOption:
              $ref: '#/components/schemas/SurveyShuffleOption'
            validation:
              $ref: '#/components/schemas/SurveyValidation'
      unevaluatedProperties: false
    SurveyContactInfoElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - firstName
            - lastName
            - email
            - phone
            - company
          properties:
            type:
              type: string
              enum:
                - contactInfo
            firstName:
              $ref: '#/components/schemas/SurveyToggleInputConfig'
            lastName:
              $ref: '#/components/schemas/SurveyToggleInputConfig'
            email:
              $ref: '#/components/schemas/SurveyToggleInputConfig'
            phone:
              $ref: '#/components/schemas/SurveyToggleInputConfig'
            company:
              $ref: '#/components/schemas/SurveyToggleInputConfig'
            validation:
              $ref: '#/components/schemas/SurveyValidation'
      unevaluatedProperties: false
    SurveyCsatElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - scale
            - range
          properties:
            type:
              type: string
              enum:
                - csat
            scale:
              type: string
              enum:
                - number
                - smiley
                - star
            range:
              type: integer
              enum:
                - 5
            lowerLabel:
              $ref: '#/components/schemas/TranslatableText'
            upperLabel:
              $ref: '#/components/schemas/TranslatableText'
            isColorCodingEnabled:
              type: boolean
              default: false
      unevaluatedProperties: false
    SurveyCesElement:
      allOf:
        - $ref: '#/components/schemas/SurveyElementBase'
        - type: object
          required:
            - scale
            - range
          properties:
            type:
              type: string
              enum:
                - ces
            scale:
              type: string
              enum:
                - number
                - smiley
                - star
            range:
              type: integer
              enum:
                - 5
                - 7
            lowerLabel:
              $ref: '#/components/schemas/TranslatableText'
            upperLabel:
              $ref: '#/components/schemas/TranslatableText'
            isColorCodingEnabled:
              type: boolean
              default: false
      unevaluatedProperties: false
    SurveyConditionGroup:
      type: object
      required:
        - id
        - connector
        - conditions
      properties:
        id:
          type: string
          format: cuid2
        connector:
          type: string
          enum:
            - and
            - or
        conditions:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/SurveyCondition'
              - $ref: '#/components/schemas/SurveyConditionGroup'
      additionalProperties: false
    SurveyLogicAction:
      oneOf:
        - $ref: '#/components/schemas/SurveyCalculateAction'
        - $ref: '#/components/schemas/SurveyRequireAnswerAction'
        - $ref: '#/components/schemas/SurveyJumpToBlockAction'
      description: >
        Logic action. Keep referenced ids stable: `calculate.variableId` points
        to a variable id,

        `requireAnswer.target` points to an element id, and `jumpToBlock.target`
        points to a block id

        or ending id.
    SegmentFilter:
      description: >
        A single targeting condition. `root.type` selects the subject and
        determines the valid `qualifier.operator` set: `attribute` (a workspace
        contact-attribute key), `person` (a built-in person identifier such as
        `userId`/`email`), `segment` (membership in another segment), or
        `device`. Each variant is one member of this union; the matching member
        is chosen by `root.type`.
      oneOf:
        - title: AttributeFilter
          description: Matches on a workspace contact-attribute value.
          type: object
          required:
            - id
            - root
            - qualifier
            - value
          properties:
            id:
              type: string
              format: cuid2
            root:
              type: object
              required:
                - type
                - contactAttributeKey
              properties:
                type:
                  type: string
                  enum:
                    - attribute
                contactAttributeKey:
                  type: string
                  description: >-
                    A contact-attribute key defined in the workspace (e.g.
                    `plan`, `role`).
              additionalProperties: false
            qualifier:
              type: object
              required:
                - operator
              properties:
                operator:
                  type: string
                  enum:
                    - lessThan
                    - lessEqual
                    - greaterThan
                    - greaterEqual
                    - equals
                    - notEquals
                    - isSet
                    - isNotSet
                    - contains
                    - doesNotContain
                    - startsWith
                    - endsWith
                    - isOlderThan
                    - isNewerThan
                    - isBefore
                    - isAfter
                    - isBetween
                    - isSameDay
              additionalProperties: false
            value:
              $ref: '#/components/schemas/SegmentFilterValue'
          additionalProperties: false
        - title: PersonFilter
          description: Matches on a built-in person identifier.
          type: object
          required:
            - id
            - root
            - qualifier
            - value
          properties:
            id:
              type: string
              format: cuid2
            root:
              type: object
              required:
                - type
                - personIdentifier
              properties:
                type:
                  type: string
                  enum:
                    - person
                personIdentifier:
                  type: string
                  description: A built-in person identifier (e.g. `userId`).
              additionalProperties: false
            qualifier:
              type: object
              required:
                - operator
              properties:
                operator:
                  type: string
                  enum:
                    - equals
                    - notEquals
                    - isSet
                    - isNotSet
                    - contains
                    - doesNotContain
                    - startsWith
                    - endsWith
              additionalProperties: false
            value:
              $ref: '#/components/schemas/SegmentFilterValue'
          additionalProperties: false
        - title: SegmentFilter
          description: Matches on membership in another segment.
          type: object
          required:
            - id
            - root
            - qualifier
            - value
          properties:
            id:
              type: string
              format: cuid2
            root:
              type: object
              required:
                - type
                - segmentId
              properties:
                type:
                  type: string
                  enum:
                    - segment
                segmentId:
                  type: string
                  description: Id of the segment to test membership against.
              additionalProperties: false
            qualifier:
              type: object
              required:
                - operator
              properties:
                operator:
                  type: string
                  enum:
                    - userIsIn
                    - userIsNotIn
              additionalProperties: false
            value:
              $ref: '#/components/schemas/SegmentFilterValue'
          additionalProperties: false
        - title: DeviceFilter
          description: Matches on the contact's device type.
          type: object
          required:
            - id
            - root
            - qualifier
            - value
          properties:
            id:
              type: string
              format: cuid2
            root:
              type: object
              required:
                - type
                - deviceType
              properties:
                type:
                  type: string
                  enum:
                    - device
                deviceType:
                  type: string
                  description: Device type (e.g. `desktop`, `phone`).
              additionalProperties: false
            qualifier:
              type: object
              required:
                - operator
              properties:
                operator:
                  type: string
                  enum:
                    - equals
                    - notEquals
              additionalProperties: false
            value:
              $ref: '#/components/schemas/SegmentFilterValue'
          additionalProperties: false
    SurveyElementBase:
      type: object
      required:
        - id
        - type
        - headline
        - required
      properties:
        id:
          type: string
          pattern: ^[a-zA-Z0-9_-]+$
          description: Stable element id. Avoid spaces and reserved ids.
        type:
          type: string
          enum:
            - openText
            - multipleChoiceSingle
            - multipleChoiceMulti
            - nps
            - rating
            - csat
            - ces
            - consent
            - pictureSelection
            - cta
            - date
            - fileUpload
            - cal
            - matrix
            - address
            - ranking
            - contactInfo
        headline:
          $ref: '#/components/schemas/TranslatableText'
        subheader:
          $ref: '#/components/schemas/TranslatableText'
        required:
          type: boolean
        imageUrl:
          type: string
        imageAltText:
          allOf:
            - $ref: '#/components/schemas/TranslatableText'
          description: >-
            Text alternative for `imageUrl`, read by screen readers. Omit or
            leave empty for a decorative image (rendered as `alt=""`).
        videoUrl:
          type: string
        isDraft:
          type: boolean
          description: Draft marker used by the editor and future update rules.
    SurveyCharLimit:
      type: object
      description: Optional `openText` character limit configuration.
      properties:
        enabled:
          type: boolean
          default: false
        min:
          type: number
        max:
          type: number
      additionalProperties: false
    SurveyValidation:
      type: object
      description: Optional element-level validation rules.
      required:
        - rules
      properties:
        logic:
          type: string
          enum:
            - and
            - or
          default: and
        rules:
          type: array
          items:
            $ref: '#/components/schemas/SurveyValidationRule'
      additionalProperties: false
    SurveyChoice:
      type: object
      required:
        - id
        - label
      properties:
        id:
          type: string
          description: Stable choice id.
        label:
          $ref: '#/components/schemas/TranslatableText'
      additionalProperties: false
    SurveyShuffleOption:
      type: string
      enum:
        - none
        - all
        - exceptLast
        - reverseOrderOccasionally
        - reverseOrderExceptLast
    SurveyMultipleChoiceDisplayType:
      type: string
      enum:
        - list
        - dropdown
    SurveyPictureChoice:
      type: object
      required:
        - id
        - imageUrl
      properties:
        id:
          type: string
          description: Stable picture choice id.
        imageUrl:
          type: string
        imageAltText:
          allOf:
            - $ref: '#/components/schemas/TranslatableText'
          description: >-
            Text alternative for `imageUrl`, read by screen readers. Omit or
            leave empty to fall back to the positional label ("Option 1",
            "Option 2", …); a choice image is never decorative, since the
            respondent selects it.
      additionalProperties: false
    SurveyToggleInputConfig:
      type: object
      description: Field config for address and contact info elements.
      required:
        - show
        - required
        - placeholder
      properties:
        show:
          type: boolean
        required:
          type: boolean
        placeholder:
          $ref: '#/components/schemas/TranslatableText'
      additionalProperties: false
    SurveyCondition:
      type: object
      description: >
        Single condition. Operators such as `isSubmitted`, `isSkipped`,
        `isClicked`, `isAccepted`,

        `isBooked`, `isSet`, and `isEmpty` do not use `rightOperand`; comparison
        operators do.
      required:
        - id
        - leftOperand
        - operator
      properties:
        id:
          type: string
          format: cuid2
        leftOperand:
          $ref: '#/components/schemas/SurveyDynamicReference'
        operator:
          type: string
          enum:
            - equals
            - doesNotEqual
            - contains
            - doesNotContain
            - startsWith
            - doesNotStartWith
            - endsWith
            - doesNotEndWith
            - isSubmitted
            - isSkipped
            - isGreaterThan
            - isLessThan
            - isGreaterThanOrEqual
            - isLessThanOrEqual
            - equalsOneOf
            - includesAllOf
            - includesOneOf
            - doesNotIncludeOneOf
            - doesNotIncludeAllOf
            - isClicked
            - isNotClicked
            - isAccepted
            - isBefore
            - isAfter
            - isBooked
            - isPartiallySubmitted
            - isCompletelySubmitted
            - isSet
            - isNotSet
            - isEmpty
            - isNotEmpty
            - isAnyOf
        rightOperand:
          $ref: '#/components/schemas/SurveyLogicOperand'
      additionalProperties: false
    SurveyCalculateAction:
      type: object
      description: Updates a survey variable when the logic rule matches.
      required:
        - id
        - objective
        - variableId
        - operator
        - value
      properties:
        id:
          type: string
          format: cuid2
        objective:
          type: string
          enum:
            - calculate
        variableId:
          type: string
          format: cuid2
          description: Variable id for `calculate`.
        operator:
          type: string
          enum:
            - assign
            - concat
            - add
            - subtract
            - multiply
            - divide
        value:
          $ref: '#/components/schemas/SurveyLogicOperand'
      additionalProperties: false
    SurveyRequireAnswerAction:
      type: object
      description: Requires an element/question to be answered before continuing.
      required:
        - id
        - objective
        - target
      properties:
        id:
          type: string
          format: cuid2
        objective:
          type: string
          enum:
            - requireAnswer
        target:
          type: string
          description: Target element id.
      additionalProperties: false
    SurveyJumpToBlockAction:
      type: object
      description: Jumps to another block or ending when the logic rule matches.
      required:
        - id
        - objective
        - target
      properties:
        id:
          type: string
          format: cuid2
        objective:
          type: string
          enum:
            - jumpToBlock
        target:
          type: string
          format: cuid2
          description: Target block id or ending id.
      additionalProperties: false
    SegmentFilterValue:
      description: >
        Comparison value for a segment filter condition. A string or number for
        most operators; a relative-date object (`{ amount, unit }`) for
        `isOlderThan` / `isNewerThan`; a two-element `[from, to]` string array
        for `isBetween`. Ignored for `isSet` / `isNotSet` but still required.
      oneOf:
        - type: string
        - type: number
        - type: object
          required:
            - amount
            - unit
          properties:
            amount:
              type: number
            unit:
              type: string
              enum:
                - days
                - weeks
                - months
                - years
          additionalProperties: false
        - type: array
          items:
            type: string
          minItems: 2
          maxItems: 2
    SurveyValidationRule:
      type: object
      required:
        - id
        - type
        - params
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - minLength
            - maxLength
            - pattern
            - email
            - url
            - phone
            - equals
            - doesNotEqual
            - contains
            - doesNotContain
            - minValue
            - maxValue
            - isGreaterThan
            - isLessThan
            - minSelections
            - maxSelections
            - minRanked
            - rankAll
            - minRowsAnswered
            - answerAllRows
            - isLaterThan
            - isEarlierThan
            - isBetween
            - isNotBetween
            - fileExtensionIs
            - fileExtensionIsNot
        params:
          type: object
          additionalProperties: true
        field:
          type: string
          enum:
            - addressLine1
            - addressLine2
            - city
            - state
            - zip
            - country
            - firstName
            - lastName
            - email
            - phone
            - company
      additionalProperties: false
    SurveyDynamicReference:
      type: object
      description: Dynamic reference to another value in the survey document.
      required:
        - type
        - value
      properties:
        type:
          type: string
          enum:
            - element
            - variable
            - hiddenField
            - reserved
        value:
          type: string
          description: >-
            Element id, variable id, hidden field id, or — for `reserved` — the
            name of an Embedded Data field-catalog entry (`country`,
            `deviceType`, `utmSource`, …). Reserved names are checked for
            non-emptiness only, deliberately not against the catalog, so a
            survey authored against a newer catalog still parses on an older
            self-hosted deployment.
        meta:
          type: object
          additionalProperties:
            type: string
      additionalProperties: false
    SurveyLogicOperand:
      oneOf:
        - type: object
          required:
            - type
            - value
          properties:
            type:
              type: string
              enum:
                - static
            value:
              oneOf:
                - type: string
                - type: number
                - type: array
                  items:
                    type: string
          additionalProperties: false
        - $ref: '#/components/schemas/SurveyDynamicReference'
  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'
    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.