/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
{ "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 toworkflow.* 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.
POST /v1/surveys/{surveyId}/files— create a pending file (visibilityoptional, defaultprivate) and receive an upload URLPUTbytes to the upload URL (local or R2)PATCH /v1/surveys/{surveyId}/files/{fileId}— markstatus: "ready"withsize_bytes- Private:
POST .../download-urlfor a signed read URL. Public: usepublic_urlon the file record (or the samedownload-urlendpoint, which returns the permanent URL) POST /v1/surveys/{surveyId}/files/move— move selected files (orselect_all) betweenquestraandplatformhosting. Response includes asurvey_content_update_requiredwarning; Program does not rewrite the survey document.
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 asurvey.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.
POST .../questionnaire/clarifications→ 202 with{ workflow_id, workflow, session, stream_url }(actor from auth)- Subscribe:
GETthe returnedstream_url(NDJSON). Handlestarted/clarification/completed(reconnect withstartIndex). See Questionnaire clarifications and Live updates. GET .../questionnaire/clarifications→ tip session (does not advance the probe)GET .../questionnaire/clarifications/{clarificationId}→ clarification detailsPATCH .../questionnaire/clarifications/{clarificationId}→{ "kind": "answer", "option_id"?: "...", "text"?: "..." }or{ "kind": "skip" }- When open clarifications are resolved,
POST .../clarificationsagain for follow-ups; a pass that adds nothing new completes the session
GET .../revisions/{revision}/clarifications— prior clarifications for that revisionGET .../revisions/{revision}/clarifications/{clarificationId}— a single historical clarification
Survey programming
Programming converts the survey’s questionnaire into the target platform deliverable (includingquestra as a first-class target). Intermediate IR is not exposed as a separate CRUD resource. Progress lives on a survey.programming workflow.
Typical flow:
- Upload questionnaire file and mark it
ready PUT .../questionnairewith thatfile_id(required before programming)- Optionally run Clarifications on that revision and submit answers
- Optionally
PUT .../programming/target(or passplatformon start). You may also start without a target and resume later via the target-selection hook. POST .../programmingwith optionalquestionnaire_revision(defaults to tip) → 202 with{ run_id, workflow, stream_url, platform, ... }- Follow progress via the returned
stream_url(?startIndex=N), pollGET .../programming/runs/{runId}(steps/hooks), or subscribe toworkflow.*webhooks. Statuses:pending→running→completed|failed|cancelled. - On success,
GET .../contentfor the tip revision written by programming; use/content/revisionsfor history. ManualPUT .../contentis also supported when no programming/content-edit workflow is active.
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 theqpk_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:
- Authenticate (local/dev:
X-Questra-Workspace-IdorQUESTRA_PROGRAM_PLACEHOLDER_WORKSPACE_IDuntil OIDC) POST /v1/api_keyswithname+scopes— store the returnedkey- Send
Authorization: Bearer <key>on subsequent requests POST /v1/api_keys/{apiKeyId}/rotatewhen rotating credentialsDELETE /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:
POST /v1/integrationswithplatform,config, andcredentials(store nothing from the response secrets — there are none)POST /v1/integrations/{integrationId}/testto confirm the credentials still workPATCH ...with a newcredentialsmap when rotating secretsDELETE /v1/integrations/{integrationId}to revoke
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:
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 sameContentRevision 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 underdata.
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:
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-storage → repositories/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:
POST /v1/webhook_endpoints— register URL +event_types(store the returnedsecret)GET /v1/events— see what Program emittedGET /v1/webhook_endpoints/{endpointId}/deliveries— inspect attempts and response codesPOST .../deliveries/{deliveryId}/redeliver— manually retry a failed delivery
Regenerating the spec
Fromrepos/program:
pnpm --filter @questra-ai/program-docs dev) watches the API and regenerates the spec automatically.