Skip to main content
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 to create a key and Quickstart for a first request.
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.

Base URL

Health check

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

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. 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. 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/clarifications202 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 and 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. 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: pendingrunningcompleted | 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 and 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. 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.

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. 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. 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:
Pass pagination.next_cursor back as cursor for the next page. Adapters encode provider-native paging into that opaque value.

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). Revision history varies by platform: 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. 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/integrationsresponse_data_access: 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: Outbound payloads use the Standard Webhooks 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-storagerepositories/webhooks/events/). See Webhook 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. 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:
Docs dev (pnpm --filter @questra-ai/program-docs dev) watches the API and regenerates the spec automatically.