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

# Read a survey's visibility and the impact of changing it

> Returns the survey's enforced visibility, its owner, the caller's access, and what a change would do: `blockers[]` lists the outbound connections that currently make `restricted` refusable (webhooks, integrations, active feedback sources, and draft or enabled workflows), `impact` carries the numbers the confirmation dialogues show, `allowedTargets[]` the values a `POST` would act on right now, and `pending` / `version` describe a change still in flight. Read this before offering "Make restricted"; the `POST` answers **409** while `blockers[]` is non-empty.

Requires `survey.change_visibility`, the same permission as the `POST`: the survey's owner while they still hold read access to the workspace, or an organisation owner/manager, with the feature enabled. Everyone else — read or write members, team-level managers, every API key, and anyone who cannot see the survey at all — answers **403** with the same body as an unknown id. Connection names and ids are configuration, not survey content, and only the people who may act on them get to read them.

While `pending` is non-null the survey is restricted for everyone but its owner (within their workspace access: read to view, write to edit) and the organisation's owners and managers, whichever direction is queued — the application fails closed until the graph acknowledges the change — so `visibility` reads `restricted` in that state and `allowedTargets` includes the value that cancels the pending change.




## OpenAPI

````yaml /api-v3-reference/openapi.yml get /api/v3/surveys/{surveyId}/visibility
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}/visibility:
    get:
      tags:
        - V3 Surveys
      summary: Read a survey's visibility and the impact of changing it
      description: >
        Returns the survey's enforced visibility, its owner, the caller's
        access, and what a change would do: `blockers[]` lists the outbound
        connections that currently make `restricted` refusable (webhooks,
        integrations, active feedback sources, and draft or enabled workflows),
        `impact` carries the numbers the confirmation dialogues show,
        `allowedTargets[]` the values a `POST` would act on right now, and
        `pending` / `version` describe a change still in flight. Read this
        before offering "Make restricted"; the `POST` answers **409** while
        `blockers[]` is non-empty.


        Requires `survey.change_visibility`, the same permission as the `POST`:
        the survey's owner while they still hold read access to the workspace,
        or an organisation owner/manager, with the feature enabled. Everyone
        else — read or write members, team-level managers, every API key, and
        anyone who cannot see the survey at all — answers **403** with the same
        body as an unknown id. Connection names and ids are configuration, not
        survey content, and only the people who may act on them get to read
        them.


        While `pending` is non-null the survey is restricted for everyone but
        its owner (within their workspace access: read to view, write to edit)
        and the organisation's owners and managers, whichever direction is
        queued — the application fails closed until the graph acknowledges the
        change — so `visibility` reads `restricted` in that state and
        `allowedTargets` includes the value that cancels the pending change.
      operationId: getSurveyVisibilityV3
      parameters:
        - in: path
          name: surveyId
          required: true
          schema:
            type: string
            format: cuid2
          description: Survey identifier.
      responses:
        '200':
          description: Visibility state retrieved
          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/SurveyVisibilityState'
              examples:
                visibleWithBlockers:
                  summary: >-
                    Workspace-visible survey that a webhook and a workflow
                    depend on
                  value:
                    data:
                      id: clsv1234567890123456789012
                      visibility: workspace
                      owner:
                        name: Alice Example
                      access:
                        via: workspace
                        canManageVisibility: true
                      blockers:
                        - type: webhook
                          id: clwh1234567890123456789012
                          name: Zapier — new responses
                        - type: workflow
                          id: clwf1234567890123456789012
                          name: 'Notify #product on completion'
                      impact:
                        memberCount: 7
                        responseCount: 212
                      pending: null
                      version: 2
                      allowedTargets: []
                privateDraft:
                  summary: Restricted draft seen by its owner
                  value:
                    data:
                      id: clsv2234567890123456789012
                      visibility: restricted
                      owner:
                        name: Bob Example
                      access:
                        via: owner
                        canManageVisibility: true
                      blockers: []
                      impact:
                        memberCount: 7
                        responseCount: 0
                      pending: null
                      version: 2
                      allowedTargets:
                        - workspace
                pendingGrant:
                  summary: >-
                    Owner asked for workspace; the in-request projection failed,
                    the outbox will finish it
                  value:
                    data:
                      id: clsv2234567890123456789012
                      visibility: restricted
                      owner:
                        name: Bob Example
                      access:
                        via: owner
                        canManageVisibility: true
                      blockers: []
                      impact:
                        memberCount: 7
                        responseCount: 0
                      pending: workspace
                      version: 3
                      allowedTargets:
                        - workspace
                        - restricted
                ownerlessPrivateSeenByManager:
                  summary: >-
                    Departed author's restricted survey, read by an organisation
                    manager
                  value:
                    data:
                      id: clsv3234567890123456789012
                      visibility: restricted
                      owner: null
                      access:
                        via: organizationRole
                        canManageVisibility: true
                      blockers: []
                      impact:
                        memberCount: 7
                        responseCount: 41
                      pending: null
                      version: 2
                      allowedTargets:
                        - workspace
        '400':
          $ref: '#/components/responses/V3BadRequest'
        '401':
          $ref: '#/components/responses/V3Unauthorized'
        '403':
          $ref: '#/components/responses/V3VisibilityForbidden'
        '429':
          $ref: '#/components/responses/V3TooManyRequests'
        '500':
          $ref: '#/components/responses/V3InternalServerError'
      security:
        - sessionAuth: []
        - apiKeyAuth: []
        - bearerAuth: []
components:
  schemas:
    SurveyVisibilityState:
      allOf:
        - type: object
          required:
            - id
          properties:
            id:
              type: string
              description: Survey identifier.
        - $ref: '#/components/schemas/SurveyVisibilityFields'
        - type: object
          required:
            - blockers
            - impact
          properties:
            blockers:
              type: array
              items:
                $ref: '#/components/schemas/SurveyVisibilityBlocker'
              description: >-
                Connections that make `restricted` refusable right now. Empty
                when being restricted is allowed, and always empty for the
                `workspace` direction.
            impact:
              $ref: '#/components/schemas/SurveyVisibilityImpact'
        - type: object
          required:
            - pending
            - version
            - allowedTargets
          properties:
            pending:
              anyOf:
                - $ref: '#/components/schemas/SurveyVisibility'
                - type: 'null'
              description: >-
                The value a `POST` stored that the graph has not yet
                acknowledged. While non-null the survey is restricted for
                everyone but the owner (for as long as they keep the workspace
                access the action needs — read to view, write to edit) and the
                organisation's owners and managers, whichever value is pending —
                `visibility` reads `restricted`. Always `null` for a survey just
                created or copied whose visibility has not been changed yet —
                its stored value is in effect at once. `"restricted"` means the
                restriction is already enforced and only the graph is catching
                up; `"workspace"` means the grant is not yet in effect. `null`
                normally. Clears when the outbox delivers (within 60 seconds, or
                the deployment fails closed), or when a later `POST` cancels or
                supersedes it.
            version:
              type: integer
              minimum: 0
              description: >-
                Monotonic per survey; incremented by every accepted `POST`,
                including cancellations. Projection work is fenced on it: a
                projector holding an older version never writes. `0` for a
                survey whose visibility was never changed through this endpoint.
            allowedTargets:
              type: array
              items:
                $ref: '#/components/schemas/SurveyVisibility'
              uniqueItems: true
              description: >-
                Values a `POST …/visibility` by this caller would act on right
                now. Never the enforced value — **except** when a different
                value is pending, in which case the enforced value is listed
                because requesting it cancels the pending change. Never
                `restricted` while `blockers[]` is non-empty or `owner` is
                `null`. Empty when nothing would happen. Drives the row menu and
                the editor control.
    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'
    SurveyVisibilityBlocker:
      type: object
      description: >-
        An outbound connection that must be removed before the survey can become
        restricted.
      required:
        - type
        - id
        - name
      properties:
        type:
          type: string
          enum:
            - webhook
            - integration
            - feedbackSource
            - dashboard
            - workflow
        id:
          type: string
        name:
          type: string
          description: >-
            Display name of the connection, for the UI to list. Never empty;
            falls back to the type.
      additionalProperties: false
    SurveyVisibilityImpact:
      type: object
      description: The numbers the confirmation dialogues show.
      required:
        - memberCount
        - responseCount
      properties:
        memberCount:
          type: integer
          minimum: 0
          description: >-
            People other than the owner and the organisation's owners/managers
            who can see the survey now (if workspace-visible) or would gain
            access (if restricted).
        responseCount:
          type: integer
          minimum: 0
          description: Responses on the survey, partial ones included.
      additionalProperties: false
    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.
    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'
    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
    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
  responses:
    V3BadRequest:
      description: >-
        Bad Request — malformed JSON, invalid query/body/params, duplicate name,
        or unsupported field.
      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'
    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'
    V3VisibilityForbidden:
      description: >-
        Forbidden, one of two codes. `forbidden`: the caller cannot see the
        survey, **or** sees it but may not change its visibility — workspace
        members without ownership, team-level managers, and every API key — with
        the same body as any unreadable survey. `visibility_not_enabled`: the
        organisation lacks the RBAC / access-control entitlement, or the
        deployment's readiness marker for survey visibility is not set; this
        answer does not depend on the survey.
      headers:
        X-Request-Id:
          schema:
            type: string
        Cache-Control:
          schema:
            type: string
          example: private, no-store
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            notAllowed:
              summary: Team-level manager, or an API key
              value:
                title: Forbidden
                status: 403
                detail: You are not authorized to access this resource
                code: forbidden
                requestId: req_01j8z3
            notEnabled:
              summary: >-
                Organisation is not entitled, or the deployment has not enabled
                visibility
              value:
                title: Forbidden
                status: 403
                detail: Survey visibility is not enabled for this organization.
                code: visibility_not_enabled
                requestId: req_01j8z4
    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.