---
title: "Change a survey's visibility"
method: POST
path: "/api/v3/surveys/{surveyId}/visibility"
tags: ["V3 Surveys"]
---

# Change a survey's visibility

`POST /api/v3/surveys/{surveyId}/visibility`

## Request

- Base URL: `https://app.formbricks.com`
- URL: `https://app.formbricks.com/api/v3/surveys/{surveyId}/visibility`
- Auth: one of:
  - API key in cookie `next-auth.session-token`
  - API key in header `x-api-key`
  - HTTP bearer

Sets the survey to `restricted` or `workspace`.

**Restrictions take effect at once.** A `workspace → restricted` request stores the flag, a new `version` and a pending marker in one transaction. From that instant the survey is restricted on every path — single reads, lists and counts, responses, exports — for everyone but the owner and the organisation's owners and managers, whether or not the graph has caught up (fail closed). The owner keeps only the access their workspace role gives them — read to view, write to edit; an owner who loses workspace access loses the survey too. The relationships are then projected; the answer is **200** either way, with `pending: null` when the graph acknowledged this `version` inside the request and `pending: "restricted"` when the outbox will finish it.

**Grants take effect when enforced.** A `restricted → workspace` request answers **200** only once the graph has acknowledged this exact `version`; until then the survey stays restricted. If the in-request projection fails, the answer is **503** `projection_pending`: the change is stored and queued, `GET …/visibility` shows `pending: "workspace"`, and retrying is safe.

**Fencing.** Every accepted change increments the survey's `version`. Projection work carries the version it was issued for, and a projector holding an older version never writes, so a worker leased before a newer change cannot overtake it — including one that crashed before its verification pass. A **200** for a grant means the graph holds this exact version.

**No-ops and cancellation.** Requesting the value the survey already enforces, with nothing pending, is a **200** no-op: nothing is written or audited and `changedAt` / `changedBy` are unchanged. Requesting the enforced value while the *opposite* is pending **cancels** the pending change: a new version is stored, the queued work is superseded, and the answer is **200** with `pending: null`. Requesting the pending value again re-attempts the projection (200 or 503).

When `restricted` is refused for two reasons at once — no owner **and** outbound connections — the **422** wins: the missing owner cannot be fixed, so listing connections to remove would send the caller down a dead end.

Allowed for the survey's owner (while they still hold read access to the workspace) and for the organisation's owners and managers. **Not** for workspace members with write or team-level manage rights, and never for API keys. Every successful change is audit-logged with the actor.

Going `workspace → restricted` is refused with **409** while any outbound connection depends on the survey; resolve them first. Archived creatives don't block it: a restriction unlinks them (each unlink is audited as an update of that creative), so restoring one later needs a new Destination. It is refused with **422** when the survey has no owner, because "restricted" would then mean "administrators only" — duplicate the survey for a restricted copy. Going `restricted → workspace` has no preconditions.

This is the only way to change visibility: `PATCH /api/v3/surveys/{surveyId}` accepts `visibility`, `owner` and `access` only as the exact values `GET` returned (so a fetched survey round-trips), and rejects any other value as an unsupported field. Publishing does not change visibility; the publish dialogue chains this call before the status change.

## Path parameters

- `surveyId` string, cuid2, required

## Request body

- SurveyVisibilityChangeRequest
  - `visibility` 'restricted' | 'workspace', required: `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.

## Response `200`

Visibility changed, or already at the requested value

- object
  - `data` SurveyVisibilityChangeResult, required: The three fields every `SurveyListItem` and `SurveyResource` carries, merged into both via `allOf`. All required, always present.
    - `id` string, required: Survey identifier.
    - `visibility` 'restricted' | 'workspace', required: `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.
    - `owner` SurveyOwner, required: 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.
      - `name` string, required
    - `access` SurveyAccess, required: 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.
      - `via` string, required: 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.
      - `canManageVisibility` boolean, required: 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.
    - `version` integer, required: The survey's visibility version after this call. Unchanged by a no-op.
    - `pending` 'restricted' | 'workspace', required: `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.
    - `changedAt` string, date-time, nullable, required: When the current visibility was last set through this endpoint. `null` when it never was — the survey still has its creation visibility, which is the case for a no-op on a survey nobody has touched. Unchanged by a no-op otherwise.
    - `changedBy` SurveyVisibilityActor, required: Who made the change. Always a user, since API keys cannot change visibility.
      - `type` 'user', required
      - `id` string, required
      - `name` string, required

## Other responses

- `400`: Bad Request — malformed JSON, invalid query/body/params, duplicate name, or unsupported field.
- `401`: Not authenticated (no valid session or API key).
- `403`: 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.
- `409`: Conflict — `restricted` was requested while outbound connections depend on the survey. Remove or re-point them, then retry. `details.blockers` matches `blockers[]` from the `GET`.
- `422`: Unprocessable — `restricted` was requested on a survey with no owner (`owner: null`). Restricted means "the owner and the organisation's administrators"; with no owner it would mean administrators only, which is not a state the product offers. Duplicate the survey to get a restricted copy you own.
- `429`: Rate limit exceeded.
- `500`: Internal Server Error.
- `503`: Service Unavailable — only for a **grant** (`restricted → workspace`): the flag and a new `version` were stored, but the graph did not acknowledge the projection within the request. The survey stays restricted meanwhile (fail closed); the change is queued durably and lands within 60 seconds or the deployment fails closed. `GET …/visibility` shows `pending: "workspace"`. Retrying the `POST` is safe: it re-attempts the projection and answers **200** only when the graph holds this exact version. A `POST` for `restricted` cancels the pending grant. A restriction never answers 503 — it is enforced the moment it is stored. Chosen over **202 Accepted** so the pending state stays an exception, not the normal path every client must poll.

## Changes

- **2026-10-28** `84820a01e550`: 24 warning
  - added the new `chart_in_use` enum value to the `allOf[#/components/schemas/Problem]/code` response property for the response status `409`
  - added the new `chart_in_use` enum value to the `code` response property for the response status `400`
  - added the new `chart_in_use` enum value to the `code` response property for the response status `401`
  - added the new `chart_in_use` enum value to the `code` response property for the response status `403`
  - …20 more
- **2026-10-27** `aad5dc1d8236`: 17 warning
  - added the new `creative` enum value to the `allOf[subschema #2]/details/blockers/items/type` response property for the response status `409`
  - added the new `creatives_not_entitled` enum value to the `allOf[#/components/schemas/Problem]/code` response property for the response status `409`
  - added the new `creatives_not_entitled` enum value to the `code` response property for the response status `400`
  - added the new `creatives_not_entitled` enum value to the `code` response property for the response status `401`
  - …13 more
- …earlier changes not shown

[Operation history](https://skmtc.dev/formbricks/apis/formbricks-api-v3/changes/api/v3/surveys/:surveyId/visibility/post.md)

---

[API](https://skmtc.dev/formbricks/apis/formbricks-api-v3.md) - [All operations](https://skmtc.dev/formbricks/apis/formbricks-api-v3/llms.txt) - [OpenAPI document](https://skmtc.dev/formbricks/apis/formbricks-api-v3/revisions/25335c959a13?raw)
