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

# API overview

> REST API for Questra Program.

The Program API is a REST service at `/v1/*`. Endpoint reference is generated from the OpenAPI spec.

Authenticate automation with a workspace API key (`Authorization: Bearer qpk_live_…`). See [Authentication](/authentication) to create a key and [Quickstart](/quickstart) for a first request.

<Note>
  Survey **root** CRUD, API keys, webhooks, survey **files**, **questionnaire revisions**, and workflow **bindings** are workspace-scoped and persist via `@questra-ai/program-storage` (memory or Postgres). Other nested survey resources (Clarifications, conversations, content) may still use in-memory stores when running without Postgres and reset on process restart.
</Note>

## Base URL

| Environment | URL                                      |
| ----------- | ---------------------------------------- |
| Production  | `https://api.program.questra.ai`         |
| Staging     | `https://api.program.staging.questra.ai` |
| Local       | `http://localhost:3310`                  |

## Health check

```
GET /health
```

Returns `{ "status": "ok" }`. This route is outside `/v1` and is not included in the OpenAPI reference.

## Surveys

Program-managed surveys are **workspace-scoped** (same Auth workspace id as API keys and webhooks). Slugs are unique per workspace.

| Method   | Path                     | Scope           |
| -------- | ------------------------ | --------------- |
| `GET`    | `/v1/surveys`            | `surveys:read`  |
| `POST`   | `/v1/surveys`            | `surveys:write` |
| `GET`    | `/v1/surveys/{surveyId}` | `surveys:read`  |
| `PATCH`  | `/v1/surveys/{surveyId}` | `surveys:write` |
| `DELETE` | `/v1/surveys/{surveyId}` | `surveys:write` |

Create body: `slug` (lowercase kebab), `name`, optional `description`. Duplicate slug in the same workspace returns `409` with `duplicate_slug`. Nested resources (files, questionnaire, programming, content, conversations) require the survey to exist in the caller's workspace.

## Workflows

Async execution is modeled as first-class **workflows**. Domain endpoints (programming, content editing, questionnaire clarifications, audit) **create** typed workflows and own subscribe/status/hooks; the Workflow SDK advances durable steps. Clients poll domain run status or subscribe to `workflow.*` webhooks — GET endpoints are side-effect free.

The public workflow id is the SDK run id (`wrun_...`). Program Postgres stores bindings for authz and newest-wins concurrency — it is not a second scheduler. See [Workflows overview](/workflows/overview).

| Kind                                  | Created by                              | Output (on success)                                       |
| ------------------------------------- | --------------------------------------- | --------------------------------------------------------- |
| `survey.programming`                  | `POST .../programming`                  | `{ content_revision }` (domain pipeline still stubbed)    |
| `survey.content_edit`                 | `POST .../conversations/.../messages`   | `{ content_revision }` via WorkflowAgent draft finalize   |
| `survey.questionnaire_clarifications` | `POST .../questionnaire/clarifications` | `{ questionnaire_revision, clarification_count, status }` |

## Survey files and object storage

Large files are uploaded directly to S3-compatible object storage — **Cloudflare R2** in production (private + public buckets) and a **filesystem-backed local store** in development (`/v1/_dev/objects/{private|public}`). The API mints upload URLs; clients never send file bytes through Program except when hitting the local object routes in development.

| Visibility | Default | Upload     | Download                                                             |
| ---------- | ------- | ---------- | -------------------------------------------------------------------- |
| `private`  | yes     | Signed PUT | Signed GET via `POST .../download-url`                               |
| `public`   | no      | Signed PUT | Stable `public_url` once `ready` (also returned from `download-url`) |

Each file also tracks **`hosting`**: `questra` (live surveys should use the Questra CDN URL) or `platform` (upstream file manager / native handle). Workspace **`default_file_hosting`** on `/v1/settings/programming` (`questra_cdn` | `platform_native`) decides the default during programming. Platform-reserved sidecars (for example Decipher `quota.xls`) always prefer upstream when the platform can push files.

1. `POST /v1/surveys/{surveyId}/files` — create a pending file (`visibility` optional, default `private`) and receive an upload URL
2. `PUT` bytes to the upload URL (local or R2)
3. `PATCH /v1/surveys/{surveyId}/files/{fileId}` — mark `status: "ready"` with `size_bytes`
4. Private: `POST .../download-url` for a signed read URL. Public: use `public_url` on the file record (or the same `download-url` endpoint, which returns the permanent URL)
5. `POST /v1/surveys/{surveyId}/files/move` — move selected files (or `select_all`) between `questra` and `platform` hosting. Response includes a `survey_content_update_required` warning; Program does **not** rewrite the survey document.

Env (API): `QUESTRA_PROGRAM_OBJECT_STORAGE=local|s3`, dual local roots under `QUESTRA_PROGRAM_LOCAL_OBJECTS_DIR`, or dual R2 buckets via `QUESTRA_PROGRAM_S3_PRIVATE_BUCKET` / `QUESTRA_PROGRAM_S3_PUBLIC_BUCKET` + `QUESTRA_PROGRAM_S3_PUBLIC_BASE_URL`.

## Questionnaire

The **questionnaire** is not a separate upload channel — it is a survey file that has been asserted as the survey’s current questionnaire. Revisions are append-only pointers (`file_id` + monotonic `revision`), persisted per workspace (same tenancy as surveys/files), so you can compare the tip against historical copies.

| Resource                 | Path                                                                       | Purpose                                       |
| ------------------------ | -------------------------------------------------------------------------- | --------------------------------------------- |
| Current tip              | `/v1/surveys/{surveyId}/questionnaire`                                     | Get or assert the latest questionnaire file   |
| History                  | `/v1/surveys/{surveyId}/questionnaire/revisions`                           | List / fetch past file references             |
| Clarifications (tip)     | `/v1/surveys/{surveyId}/questionnaire/clarifications`                      | Probe + session + per-question answer/skip    |
| Clarifications (history) | `/v1/surveys/{surveyId}/questionnaire/revisions/{revision}/clarifications` | Read-only prior Clarifications for a revision |

Assert a tip with `PUT .../questionnaire` `{ "file_id": "..." }` (file must be `ready`). Programming reads from these revisions and does not accept a questionnaire file directly.

Deleting a survey file that is referenced by any questionnaire revision returns **409** `{ error: "file_in_use", revisions: [...] }`. Pass `?force=true` to remove those revisions, then delete the file.

### Questionnaire clarifications

Before programming, probe the **tip** questionnaire for clarifications. Each probe creates a `survey.questionnaire_clarifications` workflow. Identified items stream on the run NDJSON stream while probing; reconcile with tip `GET` (side-effect free) or `questionnaire.clarifications.updated` webhooks. Each clarification has an id, optional shortcut `options`, and is answered or skipped via tip PATCH only.

1. `POST .../questionnaire/clarifications` → **202** with `{ workflow_id, workflow, session, stream_url }` (actor from auth)
2. **Subscribe:** `GET` the returned `stream_url` (NDJSON). Handle `started` / `clarification` / `completed` (reconnect with `startIndex`). See [Questionnaire clarifications](/workflows/questionnaire-clarifications) and [Live updates](/workflows/live-updates).
3. `GET .../questionnaire/clarifications` → tip session (does not advance the probe)
4. `GET .../questionnaire/clarifications/{clarificationId}` → clarification details
5. `PATCH .../questionnaire/clarifications/{clarificationId}` → `{ "kind": "answer", "option_id"?: "...", "text"?: "..." }` or `{ "kind": "skip" }`
6. When open clarifications are resolved, `POST .../clarifications` again for follow-ups; a pass that adds nothing new completes the session

Historical revisions are **read-only**:

* `GET .../revisions/{revision}/clarifications` — prior clarifications for that revision
* `GET .../revisions/{revision}/clarifications/{clarificationId}` — a single historical clarification

You cannot start a probe or submit answers on an old revision.

## Survey programming

Programming converts the survey’s questionnaire into the **target platform** deliverable (including `questra` as a first-class target). Intermediate IR is not exposed as a separate CRUD resource. Progress lives on a `survey.programming` **workflow**.

| Resource              | Path                                                   | Purpose                                                                                                                                                                                                                                       |
| --------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Workspace defaults    | `/v1/settings/programming`                             | Per-platform default instructions + `default_file_hosting` (`questra_cdn` \| `platform_native`)                                                                                                                                               |
| Integration policy    | `/v1/settings/integrations`                            | Response data access (`none` / `test` / `production`)                                                                                                                                                                                         |
| Programming           | `/v1/surveys/{surveyId}/programming`                   | Start / get latest programming workflow (target optional)                                                                                                                                                                                     |
| Programming target    | `/v1/surveys/{surveyId}/programming/target`            | Platform + optional instruction override                                                                                                                                                                                                      |
| Content               | `/v1/surveys/{surveyId}/content`                       | Append-only revision history (Postgres, workspace-scoped). Each revision stores actor `user_id` (from auth) + optional `message`. PUT emits `content.updated` webhook. Blocked while `survey.programming` or `survey.content_edit` is active. |
| Programming run       | `.../programming/runs/{runId}`                         | Product steps, hooks, progress stream URL                                                                                                                                                                                                     |
| Programming stream    | `.../programming/runs/{runId}/stream`                  | NDJSON `StreamEnvelope`; reconnect with `startIndex`                                                                                                                                                                                          |
| Resolve hook          | `.../programming/runs/{runId}/hooks/{hookId}/resolve`  | Resume a waiting hook from run detail (`resume_path`); body is kind-specific                                                                                                                                                                  |
| Clarifications stream | `.../questionnaire/clarifications/runs/{runId}/stream` | NDJSON `QuestionnaireClarificationsStreamChunk`                                                                                                                                                                                               |
| Audit stream          | `GET /v1/workflows/{workflowId}/stream`                | NDJSON anatomy + `audit.issue` chunks                                                                                                                                                                                                         |

Typical flow:

1. Upload questionnaire file and mark it `ready`
2. `PUT .../questionnaire` with that `file_id` (required before programming)
3. Optionally run Clarifications on that revision and submit answers
4. Optionally `PUT .../programming/target` (or pass `platform` on start). You may also start **without** a target and resume later via the target-selection hook.
5. `POST .../programming` with optional `questionnaire_revision` (defaults to tip) → **202** with `{ run_id, workflow, stream_url, platform, ... }`
6. Follow progress via the returned `stream_url` (`?startIndex=N`), poll `GET .../programming/runs/{runId}` (steps/hooks), or subscribe to `workflow.*` webhooks. Statuses: `pending` → `running` → `completed` | `failed` | `cancelled`.
7. On success, `GET .../content` for the tip revision written by programming; use `/content/revisions` for history. Manual `PUT .../content` is also supported when no programming/content-edit workflow is active.

Effective instructions: job `target.instructions` if non-null, otherwise workspace defaults for that platform. Use `""` on the target to force no instructions. See [Programming](/workflows/programming) and [Live updates](/workflows/live-updates).

## Conversations (agentic content editor)

Conversations are durable threads for editing programmed content with a **WorkflowAgent** (`survey.content_edit`). Chat uses the AI SDK `UIMessage` protocol and `WorkflowChatTransport` (SSE). See [Content edit](/workflows/content-edit).

| Resource                          | Purpose                                                                     |
| --------------------------------- | --------------------------------------------------------------------------- |
| `.../conversations`               | Create / list conversations                                                 |
| `POST .../messages`               | Send a user `UIMessage`; returns AI SDK SSE + `x-workflow-run-id`           |
| `GET .../messages`                | Persisted `UIMessage` history for reload                                    |
| `GET .../messages/{runId}/stream` | Reconnect stream for `WorkflowChatTransport`                                |
| `.../cancel`                      | Cancel the in-flight edit workflow (optional partial assistant `UIMessage`) |

One active turn per conversation — a second `POST .../messages` while a turn is active returns **409**. Client disconnect does **not** cancel; use `.../cancel` to stop. Tools edit a workflow-scoped draft; successful completion writes exactly one content revision and emits `content.updated`.

For tip reconciliation when you are not attached to a run, use `content.updated` webhooks and `GET .../content`. Per-kind live channels: [Live updates](/workflows/live-updates).

## API keys

API keys are **workspace-scoped** credentials for authenticating to the Program API. Secrets use the `qpk_live_` prefix, are hashed at rest, and are returned only on create and rotate — list and get expose `key_hint` only.

Each key has an explicit **scopes** list (e.g. `surveys:read`, `webhooks:write`, or `*` for full access). The auth layer enforces required scopes before handlers run (`401` unauthenticated, `403` missing scope). Cross-product requirements live in the workspace `docs/api-key-standards.md`.

| Resource   | Purpose                                       |
| ---------- | --------------------------------------------- |
| `api_keys` | Create, list, update, revoke, and rotate keys |

Typical flow:

1. Authenticate (local/dev: `X-Questra-Workspace-Id` or `QUESTRA_PROGRAM_PLACEHOLDER_WORKSPACE_ID` until OIDC)
2. `POST /v1/api_keys` with `name` + `scopes` — store the returned `key`
3. Send `Authorization: Bearer <key>` on subsequent requests
4. `POST /v1/api_keys/{apiKeyId}/rotate` when rotating credentials
5. `DELETE /v1/api_keys/{apiKeyId}` to revoke

## Integrations

Integrations are **workspace-scoped** connections to survey platforms (`questra`, `decipher`, `confirmit`, `qualtrics`, `alchemer`), including first-party Questra Publish. Clients supply platform credentials on create or update; the API **never** returns credential values — only field names via `credential_fields` and a `credentials_configured` flag. Secrets are encrypted at rest. Create and update are local writes (status stays `untested`); use the test endpoint to probe the upstream platform.

| Resource                    | Purpose                                                 |
| --------------------------- | ------------------------------------------------------- |
| `integrations`              | Create, list, update, and delete platform credentials   |
| `integrations/{id}/test`    | Verify stored credentials against the upstream platform |
| `integrations/{id}/surveys` | Metadata CRUD for surveys on that platform              |

Typical flow:

1. `POST /v1/integrations` with `platform`, `config`, and `credentials` (store nothing from the response secrets — there are none)
2. `POST /v1/integrations/{integrationId}/test` to confirm the credentials still work
3. `PATCH ...` with a new `credentials` map when rotating secrets
4. `DELETE /v1/integrations/{integrationId}` to revoke

Non-secret settings live in `config` (e.g. `base_url`, `datacenter`) and are safe to read back. Use `last_tested_at` / `last_test_status` to show health in the UI.

### Integration surveys

Upstream surveys are nested under an integration and expose **metadata only** — common fields across platforms (`id`, `name`, `status`, `created_at`, `updated_at`). The `id` is the provider's opaque survey identifier (not a Program `/surveys` UUID).

List responses use a **normalized cursor** contract so clients do not need to know whether the upstream API is offset- or cursor-based:

```
GET /v1/integrations/{integrationId}/surveys?limit=25&cursor=...&q=brand&status=active
→ { "data": [...], "pagination": { "next_cursor": "...", "has_more": true } }
```

Pass `pagination.next_cursor` back as `cursor` for the next page. Adapters encode provider-native paging into that opaque value.

| Query    | Purpose                                                                                         |
| -------- | ----------------------------------------------------------------------------------------------- |
| `limit`  | Page size (default 25, max 100)                                                                 |
| `cursor` | Opaque continuation token                                                                       |
| `q`      | Case-insensitive name search                                                                    |
| `status` | Filter by normalized status (`draft`, `test`, `live`, `active`, `paused`, `closed`, `archived`) |

### Integration survey content

Read upstream questionnaire/definition source via the same `ContentRevision` payload as Program `GET /surveys/{id}/content` (`survey_id`, `revision`, `platform`, `source`, `user_id`, `message`, `created_at`).

| Method | Path                               | Purpose                                   |
| ------ | ---------------------------------- | ----------------------------------------- |
| `GET`  | `.../content`                      | Tip (latest) revision                     |
| `GET`  | `.../content/revisions`            | History (newest first; `?include=source`) |
| `GET`  | `.../content/revisions/{revision}` | Specific revision                         |

**Revision history varies by platform:**

| Platform        | Queryable history?                       | List behavior                   |
| --------------- | ---------------------------------------- | ------------------------------- |
| Questra Publish | Yes (monotonic revisions + digests)      | Multiple revisions              |
| Qualtrics       | Yes (`/survey-definitions/.../versions`) | Multiple revisions              |
| Decipher        | UI version control only — not on REST    | Single tip item (`revision: 1`) |
| ConfirmIt       | Tip export only                          | Single tip item                 |
| Alchemer        | Tip only (`modified_on`)                 | Single tip item                 |

Clients can always call `/content/revisions`; tip-only adapters return one element.

### Integration survey responses

List and fetch respondent responses from the upstream survey. Metadata is normalized; the platform-native payload is opaque under `data`.

| Method | Path                         | `data`                         |
| ------ | ---------------------------- | ------------------------------ |
| `GET`  | `.../responses`              | Omitted unless `?include=data` |
| `GET`  | `.../responses/{responseId}` | Omitted unless `?include=data` |

Normalized metadata: `id`, `survey_id`, `status` (`in_progress` | `complete` | `partial` | `terminated`), `mode` (`test` | `production`), `link_id`, `content_revision`, `started_at`, `completed_at`, `created_at`, `updated_at`.

List and get are gated by workspace **`GET/PUT/PATCH /v1/settings/integrations`** → `response_data_access`:

| Value            | Effect                                                                                                 |
| ---------------- | ------------------------------------------------------------------------------------------------------ |
| `none`           | No response records may be queried                                                                     |
| `test` (default) | Only `mode: "test"` (enough for simulations)                                                           |
| `production`     | Production **and** test — **not available yet**; setting it returns `response_data_access_unavailable` |

Query (list): `limit`, `cursor`, `status`, `mode`, `since`, `until`, `include=data` (cursor pagination like integration surveys).

Query (get): `include=data`.

## Webhooks

Webhooks are **workspace-scoped** (Auth workspace id — same tenancy as API keys). The design follows the Stripe / Svix resource model:

| Resource            | Purpose                                                           |
| ------------------- | ----------------------------------------------------------------- |
| `webhook_endpoints` | Subscribe a URL to event types; signing secret on create / rotate |
| `events`            | Immutable event log for reconciliation and backfill               |
| `deliveries`        | Per-endpoint delivery attempts, HTTP responses, and retries       |

Outbound payloads use the [Standard Webhooks](https://www.standardwebhooks.com/) envelope and signing headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`). Delivery is **at-least-once** — consumers should treat `event.id` / `webhook-id` as an idempotency key and return HTTP 2xx to acknowledge.

Program persists events, fans out pending deliveries to subscribed endpoints, and delivers asynchronously in-process (with retries and backoff). `POST .../redeliver` resets a delivery to pending and re-queues it.

Event names and payload schemas live in one registry (`@questra-ai/program-storage` → `repositories/webhooks/events/`). See [Webhook event types](/webhooks/event-types) for the generated catalog (do not maintain a separate list).

Required scopes: `webhooks:read` / `webhooks:write` for `/webhook_endpoints`, `/events`, and `/event_types`.

Workflow-related events: `workflow.started`, `workflow.updated`, `workflow.finished` (read `status`). Also: `questionnaire.updated`, `questionnaire.clarifications.updated`, `content.updated`. Live UI during a run uses Workflow SDK streams (NDJSON or conversation UIMessage SSE); webhooks + REST reconcile tip state for clients not attached to a run. See [Live updates](/workflows/live-updates).

Typical debug flow:

1. `POST /v1/webhook_endpoints` — register URL + `event_types` (store the returned `secret`)
2. `GET /v1/events` — see what Program emitted
3. `GET /v1/webhook_endpoints/{endpointId}/deliveries` — inspect attempts and response codes
4. `POST .../deliveries/{deliveryId}/redeliver` — manually retry a failed delivery

## Regenerating the spec

From `repos/program`:

```bash theme={null}
pnpm --filter @questra-ai/program-api generate:openapi
```

Docs dev (`pnpm --filter @questra-ai/program-docs dev`) watches the API and regenerates the spec automatically.
