How to embed Outset in your product with OIDC single sign-on, and the full v2 Platform API surface — existing, planned, and proposed — for an end-to-end study lifecycle: creation, fielding, analysis, and export.
0.1 (draft for internal review)2026-08-11Draft — proposed endpoints are not committed
Outset is an AI-moderated research platform: researchers design studies, recruit participants, and AI conducts and analyzes the interviews. This document describes how a partner platform integrates Outset end to end — embedding the product for their users, and driving the full study lifecycle programmatically.
Two integration surfaces, one trust relationship
Both surfaces build on the same foundation: your identity provider signs JWTs, Outset verifies them against your JWKS endpoint. You register one OIDC provider configuration with Outset (issuer, JWKS URL, client ID) and it powers everything below. There is no shared secret at rest, and key rotation is transparent — publish a new key to your JWKS before signing with it.
Surface
What it is for
Credential
Chapter
Embedded UI
Your users work inside the Outset product, framed in your own. Full researcher experience with zero login friction.
Per-session OIDC ID token, handed to the iframe over postMessage.
2
Platform API (/v2/)
Your backend drives Outset programmatically: provision organizations, create and launch studies, read results, export data.
Per-request signed JWT assertion (partner principal, RFC 7523) or a user-delegated OAuth 2.1 token.
3 onward
The two compose. A common pattern: your backend creates and configures a study via the API, your users refine and monitor it in the embedded UI, your backend pulls insights and exports when fielding completes. Users and organizations are provisioned just-in-time on first SSO login. Organizations are addressed by your identifiers throughout the API — the same org_id your tokens carry; users carry Outset-issued IDs, returned by GET /v2/users/.
Outset also exposes an MCP (Model Context Protocol) server for AI-agent integrations, secured with the same OAuth 2.1 flow — one token works on both surfaces. If your integration is agent-driven, ask your Outset contact for the Outset MCP — Integration Guide for Developers.
How to read the endpoint reference
Every endpoint in chapters 4–15 carries one of three status badges:
EXISTING — live on /v2/ today. You can build against it now.
PLANNED — designed and scheduled on the Outset roadmap. Shapes may shift in detail but not in substance.
PROPOSED — a concrete proposal for discussion with integration partners, not yet committed.
Panel-monitoring endpoints (a separate surface for recruitment-panel partners).
Report authoring — creating a report stays in the product; the API exposes every generated report and insight read-only, plus a rerun of an existing report.
Human-moderated interviews — the API covers AI-moderated studies.
You embed Outset in an <iframe>, hand it a signed OIDC ID token for the current user over postMessage, and Outset starts an authenticated session. No OAuth server, no redirect flow, no Outset login page. Users and organizations are created just-in-time on first login.
This chapter condenses the full partner guide (Outset OIDC Embed SSO — Integration Guide for Partners); the mechanics below are complete enough to implement against.
Two generic embed-lifecycle messages also apply: URL_CHANGED (Outset → you; deep-linking and navigation tracking) and LOGOUT (you → Outset; ends the embedded session).
The ID token
A standard JWT, signed RS256 only, verified against the JWKS endpoint you host (a static HTTPS JSON file is fine). Key rotation is transparent: publish the new key before signing with it.
Claim
Required
Notes
iss, aud, exp, iat
✓
Your issuer URL; the client ID Outset assigns you; tokens with iat older than 10 minutes (configurable) are rejected.
sub
✓
Stable unique user ID on your side. The identity anchor — returning users match on this, never on email. Never reuse across users.
email
✓
Rejected if email_verified is present and false.
org_id
✓
Your workspace/account ID. Each distinct value maps to its own Outset organization.
nonce
✓
Random one-time value; replayed tokens are rejected.
given_name, family_name, org_name
optional
Display names; org_name names a newly provisioned organization.
If your IdP already issues these under different claim names, Outset maps to your names during setup — you do not change how you sign tokens.
Provisioning
First login: an Outset organization is created for the org_id (if none exists) and a user account for the sub, joined with the default role configured for your integration (User or Observer).
Returning users match by sub; email and name changes flow in from the token; roles assigned inside Outset are kept.
Email conflict: if a new sub presents an email that already belongs to an unlinked Outset account, login is blocked (ACCOUNT_CONFLICT) rather than silently linking — for migrations, Outset backfills the links before go-live.
Security model
You: HTTPS everywhere, fresh token per session, explicit postMessage target origin, stable sub values. An optional shared secret on the token exchange is available per-partner.
Outset: RS256-only verification against your JWKS, full claim validation, single-use replay cache (fails closed), frame-ancestors CSP scoped to exactly your registered origins (fails closed), origin check on every inbound message, rate-limited token exchange.
You provide: JWKS URL, issuer URL, every embed origin (including dev), custom claim names if any, and any existing-user migration list before go-live.
Outset provides: your embed slug and URL, your client ID (aud), the default role for provisioned users, and a staging environment for end-to-end testing.
Everything in chapters 4–15 lives under one base path — /v2/ on the API host for your region (communicated during onboarding; US and EU stacks are separate deployments). The API publishes its own machine-readable contract: GET /v2/openapi.json (no auth required) and a hosted reference viewer at GET /v2/docs/. The OpenAPI document is generated from the deployed code on every request, so it cannot drift.
Credentials
Every request carries Authorization: Bearer <credential>. Two credential types reach the same endpoints; each endpoint in this document lists which it accepts.
User-delegated OAuth 2.1 token — existing. Standard authorization-code flow with PKCE (S256, required), discovered via GET /.well-known/oauth-authorization-server. Clients register through RFC 7591 Dynamic Client Registration or by presenting an https client-ID metadata URL. Access tokens live 1 hour; refresh tokens rotate on every use and live 30 days. At consent time the user pins the token to one organization and a workspace selection; the token can never widen that grant, and refreshes re-check that the user is still a member. One token works on both the Platform API and the MCP server.
Partner assertion — planned. Your backend signs a short-lived JWT (≤ 5 minutes, single-use jti) with the same keys your OIDC embed tokens use, and sends it directly as the bearer credential — no token exchange, no long-lived secret at rest. The target organization travels inside the signed assertion (outset_org, carrying your own external org identifier), so nothing between your backend and Outset can retarget a request. One credential reaches every organization mapped to your provider; scopes are configured per-provider and fail closed. This is the credential for estate-level automation: provisioning client organizations, managing users, reading cross-org usage, funding budgets.
Requests without a valid credential get 401 with WWW-Authenticate: Bearer. Organizations must be entitled to the Platform API (subscription flag); tokens for non-entitled organizations are rejected at authentication time.
Scopes
Scopes use a resource:verb vocabulary. Mutating scopes end in :write or :launch — :launch is deliberately separate because publish/close and recruitment operations hit production panels and can incur real fees.
The proposed endpoints in this document deliberately reuse the existing scope vocabulary — a new scope forces every existing integration to re-authorize, so one is added only when a genuinely new authority appears (the planned partner-estate scopes above).
Two role-based guardrails apply to user-delegated tokens on top of scopes, enforced at consent time and again on every request: :write/:launch scopes require an editing organization role, and analytics:read (participant output — transcripts, answers, reports) is denied to administrative non-researcher roles. Partner assertions are bounded by their per-provider scope configuration instead; role belts do not apply.
Request and response conventions
IDs are UUIDs. Timestamps are ISO 8601, UTC.
Single objects come wrapped: {"data": { ... }}.
Lists are cursor-paginated: {"data": [ ... ], "next_cursor": "<opaque|null>", "has_more": bool} with page_size (default 50, max 200) and cursor query params. Cursors are opaque bare tokens; ordering is newest-first with a stable tiebreaker.
Three usage-endpoint responses (400 result_too_large, 502, 503) are raw {"detail": …} rather than the envelope.
404, never 403, for invisible resources. When a credential cannot see a resource by ID — wrong organization, workspace outside the grant — the API answers as if it does not exist. A 403 is reserved for scope, role, and organization-policy denials; a scope denial names the missing scope explicitly.
Workspace scoping. Every workspace-scoped read and write intersects the token's granted workspaces with the caller's current membership. The intersection can only narrow over time. Partner assertions scope to the non-archived workspaces of the organization named in the assertion.
Writes are strict. Unknown fields in a request body are rejected, not ignored — a misspelled optional field fails loudly instead of silently doing nothing.
Files move through short-lived presigned URLs, in both directions. Uploads: the API issues a presigned PUT URL and a server-chosen key, you upload the bytes, then confirm — the API validates the real bytes at commit. Downloads (recordings, exports): the API returns a presigned URL or 302-redirects to one; URLs expire within minutes and must not be persisted. Two surfaces are deliberately durable and unsigned — content images embedded in question text, and highlight-reel share links — and both are documented as publicly readable by whoever holds the URL.
Long-running work is asynchronous. Operations that render files or fan out processing (exports, highlight reels, translations, synthetic responses) return 202 with a job resource to poll. Terminal states are COMPLETED / FAILED; job results carry presigned URLs.
Rate limits: authenticated callers get 600 requests/min (burst); a few expensive analytics endpoints carry tighter per-organization limits and answer 429 (or 503 + Retry-After while a shared cache warms).
PII protection is enforced below the API. Interviews whose PII scan has not completed are withheld (404); messages under review are masked ("[PII Under Review]"); redacted content never leaves the platform, on any surface.
Versioning
The /v2/ prefix is stable across additive changes; the OpenAPI document carries a semantic version that is bumped whenever the surface changes shape. A breaking change would ship as /v3/ — additive evolution (new endpoints, new optional fields) is the strong default, and this whole document is additive on /v2/.
These are the endpoints that describe who you are and what your estate looks like, rather than any single study. GET /v2/me/ confirms a credential and shows exactly which organization and workspaces it reaches — the first call to make in any integration. The usage endpoints report per-user, per-project and per-study activity for invoicing and seat review, and the recruitment wallet reports the organization's settled credit balance. The remaining routes are the partner estate: a partner credential that authenticates without a user can list and pre-provision client organizations, read a cross-organization usage aggregate, and set per-organization spend caps against one central funding balance.
Returns the user, organization, OAuth client, consented scopes, and effective workspaces behind the bearer token. Every valid token can call it, so a client can verify its credential before attempting a scope-gated endpoint.
Response
Single-object envelope.
Field
Type
Description
data.user.id
uuid
The authenticated user.
data.user.email
email
Email address of the authenticated user.
data.user.name
string
Full display name of the authenticated user.
data.organization.id
uuid
The organization the token is pinned to.
data.organization.name
string
Name of that organization.
data.application.name
string
Display name of the OAuth client the token was issued to.
data.application.client_id
string
OAuth client identifier the token was issued to.
data.scopes
array[string]
The scopes the user consented to, verbatim — the authoritative list of what this token may do.
data.workspaces.all_workspaces_granted
boolean
Whether consent covered current and future workspaces rather than a fixed selection.
data.workspaces.items[].id
uuid
A workspace this token can actually reach — the token's grant intersected with the user's current membership.
A partner assertion is presented. A partner credential has no authenticated person, so this resource has no meaning for it and the rejection names the required credential type rather than a missing scope.
Archived workspaces, and the hidden simulation workspace used for digital twins, never appear in items. The list is recomputed on every call, so a workspace the user has since lost access to disappears without the token changing.
Returns the organization the credential is acting on. Singular by design: it always resolves from the credential and takes no identifier, so a caller can never read another organization through it.
Response
Single-object envelope.
Field
Type
Description
data.id
uuid
Outset identifier of the organization the credential is acting on.
Do not confuse this with /v2/organizations/ (plural). This route is the caller's own organization; the plural collection is the partner estate — the set of client organizations mapped to a partner's identity provider — and is not reachable with a user-delegated token today.
One row per user in the organization for a date range: their logins, engagement sessions, and the projects, studies, and completed interviews they produced. This is the endpoint behind seat review and monthly invoicing.
Query parameters
Name
Type
Description
start_daterequired
date (YYYY-MM-DD)
First day of the reporting window, inclusive, in UTC.
end_daterequired
date (YYYY-MM-DD)
Last day of the reporting window, inclusive, in UTC; must be on or after start_date and no more than 366 days later.
page_size
integer · default 50
Rows per page, capped at 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list, one row per user, ordered by email ascending.
Field
Type
Description
data[].user_id
uuid, nullable
Outset user ID, or null when no current user maps to this email (for example a removed account).
data[].email
email
Email identifying the user. Rows are keyed and ordered by this value.
data[].logins[].login_id
uuid
Unique ID of this login event in the auth audit log.
data[].logins[].login_at
datetime
When the login occurred (ISO 8601, UTC).
data[].logins[].auth_method
string
How the user authenticated — for example credentials, google, saml, or oidc.
data[].engagement_session_count
integer
Count of distinct product-usage sessions (30-minute inactivity buckets) — actual active usage, which a single login can span several of, or none.
data[].projects_created
integer
Projects this user created in the date range (excludes demo and template projects).
data[].studies_created
integer
Studies this user created in the date range (excludes templates; includes soft-deleted studies).
data[].interview_completes_raw
integer
Completed interviews on this user's studies in the date range, including low-quality ones (excludes fraudulent and archived interviews).
data[].interview_completes_valid
integer
The high-quality subset of interview_completes_raw — low-quality interviews excluded.
data[].total_interview_duration_seconds
float
Total recorded duration, in seconds, of the valid (non-low-quality) completed interviews.
The window produces more rows than the usage data source returns. Request a shorter window.
502
None
The usage data source is temporarily unavailable. The endpoint fails loudly rather than serving a misleadingly low count. This response is outside the standard error envelope — the body carries only `detail`.
503
None
Another request is already filling the cache for this organization. Retry after the seconds given in `Retry-After`. This response is outside the standard error envelope — the body carries only `detail`.
Three access requirements apply on top of the usage:read scope, and all three must hold: the Usage API must be enabled for your organization (Outset enables it during onboarding); the caller's organization role must be Admin or Non-researcher admin — an organization-wide role, not a workspace one; and the token must carry an organization-wide workspace grant (all workspaces, not a selection). Each failure is a 403 naming the specific requirement. This endpoint is throttled per organization (30 requests/hour by default) because it queries a shared analytics backend. A user who created work but never logged in still appears, with an empty logins list; simulation (digital-twin) projects and studies are excluded throughout. The 400 result_too_large, 502 and 503 bodies from the usage routes use {"detail": …} rather than the standard error envelope.
One row per product-engagement session in the date range — a 30-minute inactivity bucket of real product usage, not a login. Use it when the rolled-up engagement_session_count on /v2/usage/ needs breaking down.
Query parameters
Name
Type
Description
start_daterequired
date (YYYY-MM-DD)
First day of the reporting window, inclusive, in UTC.
end_daterequired
date (YYYY-MM-DD)
Last day of the reporting window, inclusive, in UTC; the window may not exceed 366 days.
page_size
integer · default 50
Rows per page, capped at 200.
cursor
string
Opaque cursor from the previous page's next_cursor; a malformed value is a 400.
Response
Cursor-paginated list, newest session first.
Field
Type
Description
data[].user_id
uuid
Outset user ID the engagement session belongs to.
data[].session_id
string
Analytics session identifier — a 30-minute inactivity bucket, not a login event.
data[].started_at
datetime
Start of the session: its first event (ISO 8601, UTC).
data[].ended_at
datetime
End of the session: its last recorded event (ISO 8601, UTC).
data[].duration_seconds
integer
Elapsed time from the first to the last event in the session, in seconds.
Same three access requirements and the same per-organization throttle as /v2/usage/. Results are cached per organization and date range for five minutes behind a single-flight lock, so a repeated call within that window is served from cache; a window whose result exceeds the upstream row cap returns 400 result_too_large.
One row per successful server-side sign-in in the date range — the flat counterpart of the logins array on /v2/usage/. A login is an auth event and a session is a window of product engagement; the two do not map one-to-one.
Query parameters
Name
Type
Description
start_daterequired
date (YYYY-MM-DD)
First day of the reporting window, inclusive, in UTC.
end_daterequired
date (YYYY-MM-DD)
Last day of the reporting window, inclusive, in UTC; the window may not exceed 366 days.
page_size
integer · default 50
Rows per page, capped at 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list, newest login first.
Field
Type
Description
data[].login_id
uuid
Unique ID of this login event in the auth audit log.
data[].user_id
uuid, nullable
Outset user ID that logged in, or null if no current user maps to the email.
data[].user_email
email
Email of the user that logged in.
data[].login_at
datetime
When the login occurred (ISO 8601, UTC).
data[].auth_method
string
How the user authenticated — for example credentials, google, saml, or oidc.
Same three access requirements as /v2/usage/ (Usage API enabled, organization-wide role, organization-wide workspace grant). No endpoint-specific throttle applies — this one reads Outset's own audit log rather than the shared analytics backend.
Same three access requirements as /v2/usage/. Studies that are not assigned to a project are omitted entirely, so these rows can total less than /v2/usage/studies/ for the same window — reconcile against the study-level route, not against this one.
Completed-interview volume and recorded duration for the date range, grouped by study. Same window rules and pagination as the project rollup, keyed on study instead.
Query parameters
Name
Type
Description
start_daterequired
date (YYYY-MM-DD)
First day of the reporting window, inclusive, in UTC.
end_daterequired
date (YYYY-MM-DD)
Last day of the reporting window, inclusive, in UTC; the window may not exceed 366 days.
page_size
integer · default 50
Rows per page, capped at 200.
cursor
string
Opaque cursor from the previous page's next_cursor; a malformed value is a 400.
Response
Cursor-paginated list, one row per study, ordered by study ID ascending.
Field
Type
Description
data[].study_id
uuid
ID of the study (interview definition).
data[].study_name
string
Name of the study.
data[].project_id
uuid, nullable
ID of the project this study belongs to, or null if it is not in a project.
data[].interview_completes_raw
integer
Completed interviews on this study in the date range, including low-quality ones (excludes fraudulent and archived interviews).
data[].interview_completes_valid
integer
The high-quality subset of interview_completes_raw — low-quality interviews excluded.
data[].total_interview_duration_seconds
float
Total recorded duration, in seconds, of the valid (non-low-quality) completed interviews.
Returns the organization's settled recruitment credit balance at read time. Check it before launching or expanding recruitment, which draws against this balance.
Response
Single-object envelope.
Field
Type
Description
data.organization_id
uuid
The organization the credential is acting on.
data.wallet_balance_usd
string (decimal)
Settled wallet balance in USD, serialized as a string so no precision is lost in JSON parsing.
The balance is settled, not unallocated — recruitment already launched but not yet fully fielded is not deducted here, so a launch can still be blocked on funds while this reads positive. The wallet is organization-level and deliberately ignores the token's workspace grant; there is no per-workspace wallet. The organization always comes from the credential, never from a parameter.
Enumerates every organization mapped to your identity provider, with the identifier you provision them under and the read-only list of workspaces inside each. This is how a partner takes inventory of its estate.
Query parameters
Name
Type
Description
page_size
integer · default 50
Rows per page, capped at 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list, newest organization first.
Field
Type
Description
data[].external_org_id
string
Your own identifier for this organization — the value you address it by everywhere in the API.
data[].organization_id
uuid
Outset's identifier for the organization.
data[].name
string
Display name of the organization.
data[].status
enum
Whether the organization is active or has been deactivated. Deactivation semantics are still being designed, so the exact set of values and their effects may change before general availability.
data[].created_at
datetime
When the organization was created in Outset (ISO 8601, UTC).
data[].workspaces[].id
uuid
A workspace inside this organization.
data[].workspaces[].name
string
Name of that workspace.
data[].workspaces[].is_default
boolean
Whether this is the organization's default workspace, created automatically with it.
Estate-scoped: the partner assertion must not carry an organization claim on this route. A claim present here is a 400, so a request meant for one organization can never be answered with the whole estate. The workspace list is read-only; workspace creation and archival stay in the product UI for now, and a dedicated /v2/workspaces/ route is reserved for when write access lands.
Creates an Outset organization for one of your clients and maps it to your own identifier. Use it to pre-provision an organization — set a spend cap, share templates — before any of that client's users has logged in.
Request body
The external identifier to map, plus the organization's display name.
Field
Type
Description
external_org_idrequired
string
Your own identifier for this organization; it becomes the value you address the organization by on every other route.
namerequired
string
Display name for the organization as researchers will see it.
Single-object envelope carrying the same shape as a row from the list endpoint.
Field
Type
Description
data.external_org_id
string
Your identifier for the organization, as supplied.
data.organization_id
uuid
Outset's identifier for the newly created (or already existing) organization.
data.name
string
Display name of the organization.
data.status
enum
Whether the organization is active or has been deactivated. Deactivation semantics are still being designed, so the exact set of values and their effects may change before general availability.
data.created_at
datetime
When the organization was created in Outset (ISO 8601, UTC).
data.workspaces[]
array[object]
Workspaces inside the organization; a freshly created organization has exactly one, its default workspace.
Idempotent on external_org_id. Posting an identifier that is already mapped returns the existing organization instead of creating a second one, so a retried or replayed provisioning call is safe. A default workspace is created with the organization automatically. Estate-scoped: the assertion must not carry an organization claim.
Reads a single organization by your own identifier for it, with its workspace list.
Path parameters
Name
Type
Description
external_org_id
string
Your identifier for the organization. For clients migrated from a single shared organization this is the legacy workspace UUID, so identifiers you already store keep resolving.
Response
Single-object envelope; same shape as a row from the list endpoint.
Field
Type
Description
data.external_org_id
string
Your own identifier for this organization — the value you address it by everywhere in the API.
data.organization_id
uuid
Outset's identifier for the organization.
data.name
string
Display name of the organization.
data.status
enum
Whether the organization is active or has been deactivated. Deactivation semantics are still being designed, so the exact set of values and their effects may change before general availability.
data.created_at
datetime
When the organization was created in Outset (ISO 8601, UTC).
data.workspaces[].id
uuid
A workspace inside this organization. The list is read-only.
data.workspaces[].name
string
Name of that workspace.
data.workspaces[].is_default
boolean
Whether this is the organization's default workspace, created automatically with it.
The identifier is unknown, or is mapped to a different provider. Foreign mappings are never distinguishable from missing ones, so the route cannot be used to probe another partner's estate.
400
validation_error
The organization claim in the assertion does not match the `{external_org_id}` in the path. The path segment is never authoritative on its own.
Updates an organization's display name, or deactivates it when a client relationship ends. Only the fields you send are changed.
Path parameters
Name
Type
Description
external_org_id
string
Your identifier for the organization to update.
Request body
Any subset of the mutable fields.
Field
Type
Description
name
string
New display name for the organization.
status
enum
Set to DEACTIVATED to shut the organization down, or ACTIVE to restore it.
{
"status": "DEACTIVATED"
}
Response
Single-object envelope carrying the organization after the update, in the same shape the detail endpoint returns.
Field
Type
Description
data.external_org_id
string
Your own identifier for this organization — the value you address it by everywhere in the API.
data.organization_id
uuid
Outset's identifier for the organization.
data.name
string
Display name of the organization.
data.status
enum
Whether the organization is active or has been deactivated. Deactivation semantics are still being designed, so the exact set of values and their effects may change before general availability.
data.created_at
datetime
When the organization was created in Outset (ISO 8601, UTC).
data.workspaces[].id
uuid
A workspace inside this organization. The list is read-only.
data.workspaces[].name
string
Name of that workspace.
data.workspaces[].is_default
boolean
Whether this is the organization's default workspace, created automatically with it.
Deactivation semantics are still being designed — what happens to the organization's in-flight studies, active recruitment, and its users' sessions is not yet settled, and this field ships with that decision. Treat it as ending the relationship rather than as a soft delete, and expect the exact behavior to be pinned down before general availability.
One row per organization in your estate for a date range — studies, completed interviews, and recruitment spend. This is the invoicing aggregate: it answers "what did each client cost this month" in one call instead of 337.
Query parameters
Name
Type
Description
start_daterequired
date (YYYY-MM-DD)
First day of the reporting window, inclusive, in UTC.
end_daterequired
date (YYYY-MM-DD)
Last day of the reporting window, inclusive, in UTC.
page_size
integer · default 50
Rows per page, capped at 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list, one row per organization.
Field
Type
Description
data[].external_org_id
string
Your identifier for the organization the row covers.
data[].organization_id
uuid
Outset's identifier for that organization.
data[].studies_created
integer
Studies created in the organization during the date range (excludes templates).
data[].interview_completes_raw
integer
Completed interviews in the date range, including low-quality ones (excludes fraudulent and archived interviews).
data[].interview_completes_valid
integer
The high-quality subset of interview_completes_raw — low-quality interviews excluded.
data[].total_interview_duration_seconds
float
Total recorded duration, in seconds, of the valid completed interviews.
data[].recruitment_spend_usd
string (decimal)
Recruitment spend charged against the funding balance for this organization in the date range, in USD.
Partner-only and estate-scoped — the assertion must carry no organization claim, and the route is meaningless to a single-organization caller. Deliberately narrower than /v2/usage/: engagement-session and login detail are not here, because they come from an analytics backend that cannot be fanned out across a whole estate. For those, call the per-organization usage routes with an organization claim set.
Lists the members of the organization your credential is pinned to — the token's organization, or the organization named in your assertion — with the role each holds.
Query parameters
Name
Type
Description
page_size
integer · default 50
Rows per page, capped at 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list of memberships, newest first.
Field
Type
Description
data[].user_id
uuid
Outset identifier of the user.
data[].email
email
Email address of the member.
data[].name
string
Full display name of the member.
data[].role
enum
The member's role in this organization: ADMIN, MANAGER, USER, PWDR_USER, OBSERVER, or NON_RESEARCHER_ADMIN. PWDR_USER is a User whose study spend and launches are subject to per-study budget caps and approval configured by the organization.
data[].is_active
boolean
Whether the member currently has access to the organization.
Organization-scoped: a user-delegated token resolves the organization it is pinned to, and a partner assertion must carry an organization claim — a missing claim is a 400 rather than a listing of every user in the estate. Outset staff accounts attached to a customer organization for support are excluded from this listing and from every write below — a partner credential can neither see nor modify them.
There is deliberately no create-user endpoint. Members are provisioned just-in-time at first OIDC login, so creating a user here would race that flow and create a second account.
Reads one member of the organization your credential is pinned to — the token's organization, or the organization named in your assertion.
Path parameters
Name
Type
Description
user_id
uuid
Outset identifier of the user.
Response
Single-object envelope; same shape as a row from the list endpoint.
Field
Type
Description
data.user_id
uuid
Outset identifier of the user.
data.email
email
Email address of the member.
data.name
string
Full display name of the member.
data.role
enum
The member's role in this organization: ADMIN, MANAGER, USER, PWDR_USER, OBSERVER, or NON_RESEARCHER_ADMIN. PWDR_USER is a User whose study spend and launches are subject to per-study budget caps and approval configured by the organization.
data.is_active
boolean
Whether the member currently has access to the organization.
Changes a member's role within the organization your credential is pinned to — the token's organization, or the organization named in your assertion.
Path parameters
Name
Type
Description
user_id
uuid
Outset identifier of the user.
Request body
The role to set.
Field
Type
Description
rolerequired
enum
New role for this member: ADMIN, MANAGER, USER, PWDR_USER, OBSERVER, or NON_RESEARCHER_ADMIN.
{
"role": "ADMIN"
}
Response
Single-object envelope carrying the membership after the change, in the same shape the detail endpoint returns.
Field
Type
Description
data.user_id
uuid
Outset identifier of the user.
data.email
email
Email address of the member.
data.name
string
Full display name of the member.
data.role
enum
The member's role in this organization: ADMIN, MANAGER, USER, PWDR_USER, OBSERVER, or NON_RESEARCHER_ADMIN. PWDR_USER is a User whose study spend and launches are subject to per-study budget caps and approval configured by the organization.
data.is_active
boolean
Whether the member currently has access to the organization.
Roles carry real authority: ADMIN, MANAGER, and USER can edit studies and launch recruitment against the organization's spend cap; PWDR_USER does the same under per-study budget caps and the approval the organization configures; OBSERVER is read-only; NON_RESEARCHER_ADMIN administers the organization but is walled off from participant output such as transcripts and reports. The panel-partner role is not settable here — it belongs to external recruitment vendors, not to a client organization's members.
Removes a member's access to the organization your credential is pinned to — the token's organization, or the organization named in your assertion.
Path parameters
Name
Type
Description
user_id
uuid
Outset identifier of the user to deprovision from this organization.
Response
204 — empty body. The membership is gone; reading the user on this organization afterwards returns 404.
Organization-scoped, not a global account deletion: the person keeps their Outset account and any membership in your other organizations, and the studies and interviews they created stay where they are. A subsequent OIDC login carrying this organization would provision them again — revoke the assignment in your identity provider too, or the removal is temporary.
A cap is not a balance. One funding organization holds the actual recruitment credit — readable at /v2/recruitment-wallet/ by naming that organization — and every client organization draws against it up to its own cap. There is no transfer between organizations and no per-organization balance to strand. A launch is therefore blocked when either the cap is exhausted or the funding balance is, and a FUNDS_REQUIRED webhook fires naming the organization, the study, and the amount needed.
Raising a cap commits real money — spend resolves against the shared funding balance, so a higher cap in one organization can exhaust the balance the whole estate draws on. Lowering a cap below what has already been spent does not claw anything back; it only blocks further recruitment. Every change is recorded against the credential that made it.
Workspaces and projects are the two containers above a study: a workspace partitions an organization's work and governs who can see it, and a project groups the studies that answer one research question. Every study, interview, and report you reach elsewhere in this document hangs off a project, so these endpoints are usually the first call an integration makes — resolve a workspace, create or find a project, then create studies inside it. Reads need projects:read; because there is no projects:write scope, project writes reuse studies:write.
Lists the workspaces the credential can reach — for a user-delegated token, the intersection of the workspaces granted at consent time with the caller's current membership; for a partner assertion, the non-archived workspaces of the organization named in the assertion. Use this to pick the workspace a new project should live in.
Query parameters
Name
Type
Description
page_size
integer · default 50
Number of workspaces per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list of workspaces, newest-first.
Field
Type
Description
data[].id
uuid
Workspace identifier, used as workspace_id when creating a project.
data[].name
string
Display name of the workspace.
data[].is_default
boolean
Whether this is the organization's default workspace, which cannot be renamed or archived.
Read-only on purpose. Archived workspaces are omitted, and so are internal storage workspaces the product creates for itself — a workspace that does not appear here cannot be named in any other call. An empty list is a real answer, not an error: it means the credential's grant no longer intersects any live workspace, and every workspace-scoped read will be empty until that is fixed.
Lists projects the credential can reach, most-recently-modified first, each with rolled-up study and interview counts. Filter by workspace to enumerate one part of an organization.
Query parameters
Name
Type
Description
workspace_id
uuid
Return only projects in this workspace; must be a workspace the credential can reach.
archived
boolean · default False
Whether to return archived projects instead of active ones.
page_size
integer · default 50
Number of projects per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list of projects, ordered by last modification, newest first.
Field
Type
Description
data[].id
uuid
Project identifier.
data[].name
string
Display name of the project.
data[].workspace_id
uuid
Workspace the project belongs to.
data[].workspace_name
string
Display name of that workspace.
data[].project_type
string
How the project's studies are moderated: AI_MODERATED or HUMAN_MODERATED.
data[].archived
boolean
Whether the project has been archived.
data[].study_count
integer
Number of studies in the project, of any status.
data[].completed_interview_count
integer
Number of completed, valid interviews across the project's studies.
data[].participant_response_count
integer
Number of analyzed participant responses across the project's reports.
data[].created_at
datetime
When the project was created.
data[].modified_at
datetime
When the project or anything the counts summarize last changed.
The three counts are aggregated per page, which is why page_size is capped like every other list. Pulling a whole large estate one 200-project page at a time is expected and cheap; asking for the counts across thousands of projects in one call is not offered.
201 with the created project, in the same shape the detail read returns. Projects created through the API are always `AI_MODERATED`, and the rolled-up counts start at zero.
Field
Type
Description
data.id
uuid
Project identifier.
data.name
string
Display name of the project.
data.workspace_id
uuid
Workspace the project belongs to.
data.workspace_name
string
Display name of that workspace.
data.project_type
string
How the project's studies are moderated: AI_MODERATED or HUMAN_MODERATED.
data.archived
boolean
Whether the project has been archived.
data.research_goals
string
Free-text research goals for the project, used to steer analysis; empty when never set.
data.study_count
integer
Number of studies in the project, of any status.
data.completed_interview_count
integer
Number of completed, valid interviews across the project's studies.
data.participant_response_count
integer
Number of analyzed participant responses across the project's reports.
data.first_interview_date
datetime
When the project's first valid completed interview finished; null before any interview completes.
`workspace_id` names a workspace outside the credential's reach, or one that does not exist. Unreachable resources are reported as missing wherever they are named, including in a request body.
409
project_quota_exceeded
The organization has reached the project limit for its subscription tier. Retrying will not help until the tier changes or a project is archived.
Project writes use studies:write rather than a projects:write scope, which does not exist — the same authority that lets you build studies lets you create the container they live in.
Archiving is reversible and does not delete anything: the project's studies, interviews, and reports stay readable by ID and keep counting toward usage. There is no delete endpoint — archive is the intended way to retire a project.
A study is one interview definition — its guide, its screener, its interviewer settings — and it lives inside a project. This group covers the whole lifecycle: creating a study and editing its settings, duplicating or copying content from an existing one, moving it between projects, running the pre-publish checks, and taking it live. Reading and editing a study needs studies:read / studies:write; anything that opens or closes the doors to participants — publish, unpublish, close — needs the separate studies:launch scope, because those operations reach production panels and can incur real fees. Study content (sections, questions, screener, stimuli) has its own chapters; the endpoints here treat the guide as a whole.
Creates a study in a project. The study starts in DRAFT state with no questions — add content through the study-content endpoints, then publish it. A default interviewer voice is seeded so a programmatically created study is publishable without a further call.
Request body
The study's project and its initial overview fields. Everything except `name` and `project_id` is optional and can be set later with `PATCH /v2/studies/{study_id}/`.
Field
Type
Description
namerequired
string
Display name for the study, shown to researchers in the project list.
project_idrequired
uuid
Project the study is created in; it must be reachable by your credential's workspace grant.
interview_method
enum
How participants take the interview: `CHAT` (text), `VOICE` (audio), `INTERACTIVE` (voice-to-voice), `VIDEO`, `SCREENSHARE`, `MOB_UX_WEB` (mobile UX on mobile web), or `MOB_UX_APP` (mobile UX in an app); an organization may have some methods restricted.
context
string
Background the AI interviewer should know about the study's purpose and audience (10,000 characters, or 50,000 for organizations with the expanded context limit); participants may infer it, so keep confidential details out.
goals
array[string]
Research objectives, one concise sentence per entry; the combined text of all goals must stay under 1,000 characters.
language_code
string
ISO 639-1 code of the language the interview is conducted in.
welcome_message
string
Message shown to participants before the interview starts.
end_message
string
Thank-you message shown after participants finish.
{
"name": "Q3 Checkout Friction",
"project_id": "3f7c1a90-52d8-4c11-9a6e-8b0d4f2e77a1",
"interview_method": "VOICE",
"context": "We redesigned the checkout flow in June. We want to understand where shoppers hesitate.",
"goals": [
"Find the steps where shoppers abandon checkout",
"Understand how shoppers feel about the new address form"
],
"language_code": "en",
"welcome_message": "Thanks for joining — this should take about 15 minutes."
}
Response
The created study, in the single-object envelope.
Field
Type
Description
data.id
uuid
ID of the new study; use it on every other endpoint in this group.
data.name
string
Display name of the study.
data.project_id
uuid
Project the study was created in.
data.project_name
string
Name of that project.
data.interview_method
enum
Interview method the study was created with.
data.moderation_type
enum
Always `AI_MODERATED` for studies created through the API.
data.language_code
string
ISO 639-1 code of the interview language.
data.goals
array[string]
Research objectives as stored, or `null` when none were supplied.
data.state
enum
Lifecycle state — always `DRAFT` on a freshly created study.
data.url
url
Deep link to the study in the Outset web app, for handing a researcher the editor.
{
"data": {
"id": "9c4f1e2a-6d3b-4f18-b2c7-5a0e8d1f3b42",
"name": "Q3 Checkout Friction",
"project_id": "3f7c1a90-52d8-4c11-9a6e-8b0d4f2e77a1",
"project_name": "Checkout Research",
"interview_method": "VOICE",
"moderation_type": "AI_MODERATED",
"language_code": "en",
"goals": [
"Find the steps where shoppers abandon checkout",
"Understand how shoppers feel about the new address form"
],
"state": "DRAFT",
"url": "https://app.outset.ai/project/3f7c1a90-52d8-4c11-9a6e-8b0d4f2e77a1/survey/9c4f1e2a-6d3b-4f18-b2c7-5a0e8d1f3b42"
}
}
Errors
Status
Code
When
403
feature_not_enabled
The organization is not entitled to the requested `interview_method` (commonly `INTERACTIVE` or `VIDEO`).
400
validation_error
The subscription's study quota is exhausted, or `context` exceeds the organization's character limit.
Studies created through the API are always AI-moderated. Human-moderated studies need researcher-uploaded interview recordings and are authored in the web app.
Lists the studies your credential can reach, newest first, with optional filters by project, workspace, active state, name, and creation window. This is the audit read: with include_content=true each row carries the study's full question and screener definition.
Query parameters
Name
Type
Description
project_id
uuid
Return only studies in this project.
workspace_id
uuid
Return only studies whose project belongs to this workspace.
active
boolean
Filter on whether the study is currently accepting participants.
state
string
Filter by lifecycle state: DRAFT, LIVE, PAUSED, or CLOSED. The boolean `active` filter is the LIVE shorthand; use one or the other.
search
string
Case-insensitive substring match on the study name.
created_after
datetime
Only studies created at or after this ISO 8601 timestamp.
created_before
datetime
Only studies created strictly before this ISO 8601 timestamp.
include_content
boolean · default False
Inline each study's questions and screener definition in the row, as the study-audit read does; off by default because it makes pages substantially larger.
page_size
integer · default 50
Rows per page, maximum 200.
cursor
string
Opaque cursor from the previous page's `next_cursor`.
Response
Cursor-paginated list of studies.
Field
Type
Description
data[].id
uuid
Study ID.
data[].name
string
Display name of the study.
data[].project_id
uuid
Project the study belongs to.
data[].state
enum
Lifecycle state: `DRAFT` (never published), `LIVE` (accepting participants), `PAUSED` (published before, currently not accepting), or `CLOSED` (terminally closed).
data[].active
boolean
Whether the study is accepting new participants right now.
data[].interview_method
enum
How participants take the interview.
data[].language_code
string
ISO 639-1 code of the interview language.
data[].question_count
integer
Number of guide questions in the study.
data[].has_screener
boolean
Whether the study has a screener attached.
data[].visual_intelligence_enabled
boolean
Whether AI analysis of participant recordings has been approved for this study.
data[].closed_at
datetime
When the study was closed, or `null` if it is not closed.
data[].closed_method
enum
How it closed — `MANUAL`, `AUTO_INACTIVITY_30D`, or `AUTO_HARD_LIMIT_90D` — or `null` if it is not closed.
data[].auto_close_at
datetime
When the organization's auto-close policy will close this study, or `null` when no auto-close is pending.
data[].auto_close_method
enum
Which policy would trigger that auto-close (inactivity or hard limit), or `null`.
data[].auto_close_enabled
boolean
Per-study opt-out: when false the study is never auto-closed and `auto_close_at` is always `null`.
data[].created
datetime
When the study was created (ISO 8601, UTC).
data[].modified
datetime
When the study was last edited (ISO 8601, UTC).
data[].questions
array[object]
Full guide-question definitions; present only when `include_content=true`.
data[].screener
object
Full screener definition, or `null` when the study has none; present only when `include_content=true`.
Returns the complete definition of one study: its overview and interviewer settings, its sections and questions in order, and its screener. This is the canonical read to ground any edit — fetch it before changing content, reordering, or editing conditions.
Path parameters
Name
Type
Description
study_id
uuid
ID of the study to read.
Response
The study definition, in the single-object envelope.
Field
Type
Description
data.id
uuid
Study ID.
data.name
string
Display name of the study.
data.project_id
uuid
Project the study belongs to.
data.state
enum
Lifecycle state: `DRAFT`, `LIVE`, `PAUSED`, or `CLOSED`.
data.active
boolean
Whether the study is accepting new participants right now.
data.interview_method
enum
How participants take the interview.
data.moderation_type
enum
Whether the interview is AI-moderated or human-moderated.
data.context
string
Background the AI interviewer is given about the study.
data.goals
array[string]
Research objectives for the study, or `null` when none are set.
data.language_code
string
ISO 639-1 code of the interview language.
data.base_language_code
string
The participant-facing default language and the language recruitment copy is written in.
data.welcome_message
string
Message shown to participants before the interview starts.
data.end_message
string
Message shown after participants finish.
data.completion_url
url
Where participants are redirected after finishing, for self-managed external panels; empty when Outset handles the redirect.
data.max_interviews
integer
Cap on completed interviews for this study.
data.interviewer_persona
enum
Built-in interviewer style: `HIGH_TWO_FOLLOW_UP` ("Standard"), `LOW_TWO_FOLLOW_UP` ("Neutral"), or `NONE`.
data.custom_interviewer_persona_id
uuid
Trained custom persona assigned to the study, or `null` when a built-in persona is used.
data.interviewer_voice_engine
enum
`SIMPLE` uses the platform default voice for the language; `ADVANCED` uses a voice picked from the voice catalog.
data.interviewer_voice_id
string
Catalog voice in use, or empty when the engine is `SIMPLE`.
data.flag_pii
boolean
Whether participant messages are scanned for personal identifiers and flagged for review.
data.pii_flag_settings
array[object]
Effective per-category PII settings, each with `category`, `enabled`, `review_mode`, and `locked` (true when the organization enforces the value).
data.visual_intelligence_enabled
boolean
Whether AI analysis of participant recordings has been approved for this study.
data.auto_close_enabled
boolean
Whether the organization's auto-close policies may close this study.
data.total_questions
integer
Number of guide questions, excluding matrix sub-rows.
data.created
datetime
When the study was created (ISO 8601, UTC).
data.modified
datetime
When the study was last edited (ISO 8601, UTC).
data.sections
array[object]
Sections in display order, each with `id`, `name`, `type`, `position`, and its questions with full configuration.
data.screener
object
The screener definition with its questions, options, and qualification rules, or `null` when the study has none.
{
"data": {
"id": "9c4f1e2a-6d3b-4f18-b2c7-5a0e8d1f3b42",
"name": "Q3 Checkout Friction",
"project_id": "3f7c1a90-52d8-4c11-9a6e-8b0d4f2e77a1",
"state": "LIVE",
"active": true,
"interview_method": "VOICE",
"language_code": "en",
"max_interviews": 120,
"interviewer_voice_engine": "ADVANCED",
"interviewer_voice_id": "b1d9f0c4-77a2-4e6b-9a31-2c5d8e4f1a09",
"total_questions": 12,
"…": "remaining overview and interviewer settings omitted — see the field table above",
"sections": [
{
"id": "5b2e7d10-9a44-4c8f-8e21-7d3f6a0b9c55",
"name": "Warm-up",
"type": "STANDARD",
"position": 0,
"questions": [
{
"id": "a7c30f18-4d92-4b6a-9f57-0e1b8c2d4a63",
"position": 0,
"text": "Walk me through the last time you checked out on our site.",
"question_type": "TEXT"
}
]
}
],
"screener": {
"id": "d4a81c07-2f36-4c95-b8e0-6a9f3d7b1e28",
"questions": []
},
"created": "2026-08-01T14:22:03Z",
"modified": "2026-08-11T08:40:55Z"
}
}
Updates the study's overview and interviewer settings. Only the fields you send are changed; sending null for a nullable field clears it. Study content (sections, questions, screener) is edited through the study-content endpoints, not here.
Path parameters
Name
Type
Description
study_id
uuid
ID of the study to update.
Request body
Any subset of the study's settings. Unknown fields are rejected.
Field
Type
Description
name
string
Display name of the study.
interview_method
enum
`CHAT`, `VOICE`, `INTERACTIVE`, `VIDEO`, `SCREENSHARE`, `MOB_UX_WEB`, or `MOB_UX_APP`; switching to a method the organization has restricted is rejected.
context
string
Background for the AI interviewer (10,000 characters, or 50,000 with the expanded limit); per-section and per-question detail belongs on those objects instead.
goals
array[string]
Research objectives, combined text under 1,000 characters; send `null` to remove all goals.
language_code
string
ISO 639-1 code of the interview language; send `null` to fall back to the default.
base_language_code
string
Participant-facing default language, which must be one of the study's offered languages; send `null` to clear it.
welcome_message
string
Message shown to participants before the interview starts.
end_message
string
Message shown after participants finish.
completion_url
url
Redirect target after completion for a self-managed external panel; may contain `{placeholder}` tokens filled from the participant's start parameters. Leave unset when recruiting through Outset.
max_interviews
integer
Cap on completed interviews; must be at least 1, at least the active recruitment target, and at least the number of interviews already completed or in progress.
is_template
boolean
Mark the study as a reusable template, or convert it back to a normal study.
auto_close_enabled
boolean
Whether the organization's inactivity and maximum-duration policies may close this study automatically.
flag_pii
boolean
Scan participant messages for personal identifiers and flag them for review; shorthand for the `PERSONAL_IDENTIFIERS` entry of `pii_flag_settings` and not changeable once interviews have started.
pii_flag_settings
array[object]
Per-category PII configuration; each entry sets `category` (`PERSONAL_IDENTIFIERS` or `SENSITIVE_PERSONAL_DETAILS`) plus `enabled` and/or `review_mode` (`MANUAL_REVIEW` or `DELETE_ONLY`).
pass_metadata_to_llm
boolean
Let the AI interviewer reference the participant's start parameters and screener answers during the conversation.
skip_walkthrough
boolean
Skip the participant onboarding screens at the start of the interview.
skip_app_download
boolean
For mobile-app studies, assume the app is already installed and skip the download step.
mobile_only
boolean
Restrict text, voice, and video interviews to mobile devices; desktop participants get a hand-off screen with a link and QR code.
allow_fallback_to_chat
boolean
Let voice and voice-to-voice interviews fall back to text chat when the participant's device or network can't sustain the method.
enforce_no_video_for_screenshare
boolean
For screenshare studies, force participant video off even where the organization would allow it.
interviewer_voice_output_enabled
boolean
Speak the interviewer's messages aloud; required for the `INTERACTIVE` method.
interviewer_voice_defaults_on
boolean
Start the interviewer's voice unmuted instead of muted-until-clicked; has no effect unless voice output is enabled.
interviewer_persona
enum
Built-in interviewer style — `HIGH_TWO_FOLLOW_UP` ("Standard"), `LOW_TWO_FOLLOW_UP` ("Neutral", no upbeat acknowledgements), or `NONE`; setting a non-`NONE` value clears any custom persona.
custom_interviewer_persona_id
uuid
Trained custom persona to assign, from `GET /v2/interviewer-personas/`; send `null` to fall back to the built-in persona.
interviewer_voice_engine
enum
`SIMPLE` for the platform default voice per language, or `ADVANCED` to pick a voice from the catalog; voice-to-voice studies are always coerced to `SIMPLE`.
interviewer_voice_id
string
Voice for the `ADVANCED` engine, from `GET /v2/interviewer-voices/`; send `null` to fall back to the engine's default voice.
filter_low_quality
boolean
End interviews whose answers fall below the platform's standard quality threshold.
medical_transcription
boolean
Transcribe with a clinical speech-to-text engine; available for English-language studies only.
self_recruit_incentive_enabled
boolean
Pay completed participants of a self-recruited study a gift card; enabling it ensures an email-collecting screener question so payouts have a recipient.
self_recruit_incentive_amount_usd
number
Per-participant incentive amount in USD; publishing is blocked while incentives are enabled with a zero amount.
`max_interviews` drops below the completed-plus-in-progress interview count or the active recruitment target, `flag_pii` is toggled after interviews have started, or `base_language_code` names a language the study does not offer.
interviewer_voice_id is cleared when you flip interviewer_voice_engine without setting a new voice in the same request — voice IDs are not portable between engines.
Creates a full copy of the study — sections, questions, screener, and routing logic — in the same project. The copy is created inactive with recruitment links cleared, so duplicating can never start a parallel recruitment stream.
Path parameters
Name
Type
Description
study_id
uuid
ID of the study to copy.
Request body
Optional overrides for the copy.
Field
Type
Description
name
string
Name for the copy; defaults to the original's name followed by "(copy)".
duplicate_skip_conditions
boolean
Copy the study's skip and routing logic along with the questions; set false to copy the questions and screener without it.
Replaces this study's sections, questions, and screener with those of another study you can reach. The target study's own overview — name, context, goals, interviewer settings — is preserved.
Path parameters
Name
Type
Description
study_id
uuid
ID of the study that receives the content.
Request body
The study to copy from.
Field
Type
Description
source_study_idrequired
uuid
ID of the study whose sections, questions, and screener are copied; it must be in the same organization and reachable by your workspace grant.
Destructive and not undoable through the API: the target study's existing sections, questions, screener, and monadic configuration are deleted before the copy is written. Duplicate the study first if you need a fallback.
Reparents the study, and the stimuli it uses, under a different project. Both the study and the destination project must be reachable by your credential.
Path parameters
Name
Type
Description
study_id
uuid
ID of the study to move.
Request body
The destination.
Field
Type
Description
project_idrequired
uuid
Project to move the study into; it must have the same type and format as the study's current project.
Takes the study live so it can accept participants, running the same pre-publish validation as the web app. Publishing does not recruit anyone — pair it with the recruitment endpoints to bring participants in.
Path parameters
Name
Type
Description
study_id
uuid
ID of the study to publish.
Response
The study's new launch state.
Field
Type
Description
data.id
uuid
Study ID.
data.state
enum
Lifecycle state after the call — always `LIVE` on success.
data.already_active
boolean
True when the study was already live and this call changed nothing.
data.start_url
url
Participant-facing link to hand out; present only for self-recruited studies, since panel-recruited participants arrive through the provider.
The study fails a pre-publish check — no questions, invalid screener rules, or a role that requires admin approval. Call `POST /v2/studies/{study_id}/validate/` first to get the issues as a structured list.
402
insufficient_credit_balance
The workspace budget or recruitment wallet cannot cover the study's committed spend.
409
study_closed
The study has been closed; a closed study can never be published again.
Idempotent — publishing a live study returns already_active: true with no further effect.
The study is already terminally closed, so there is nothing to pause.
Unpublishing does not cancel a recruitment already launched with a third-party panel — those slots keep their state. Interviews already in progress also keep running and can still complete.
Irreversible. A closed study can never be published or reopened — use unpublish if you only want to pause. Closing stops arrivals, not interviews: sessions already open keep running and can still complete for up to 24 hours, so unfilled recruitment spend is settled and the completion email sent about a day later, not at the moment you close.
Runs the full pre-publish validation chain without changing anything, and returns the issues as a structured list. Use it before publish so you can fix problems by code instead of parsing an error string.
Path parameters
Name
Type
Description
study_id
uuid
ID of the study to check.
Response
The validation result.
Field
Type
Description
data.id
uuid
Study ID.
data.ready_to_publish
boolean
True only when `issues` is empty.
data.active
boolean
Whether the study is already live.
data.issues
array[object]
One entry per blocking problem, each with a stable `code`, a human-readable `message`, and context fields such as `question_id` or `question_study_order` where they apply.
{
"data": {
"id": "9c4f1e2a-6d3b-4f18-b2c7-5a0e8d1f3b42",
"ready_to_publish": false,
"active": false,
"issues": [
{
"code": "approval_required",
"message": "Publishing this study requires approval from an organization admin. Reasons: workspace_budget_exceeded.",
"reasons": [
"workspace_budget_exceeded"
]
}
]
}
}
A snapshot, not a reservation: workspace budget and interview counts can move between this call and publish, which stays the authoritative gate. POST rather than GET because the check fans out to external services, but it never writes.
Returns the duration breakdown the study editor shows, as ready-to-quote display strings. Use these verbatim wherever your copy mentions how long the interview takes, so participants see the same number the researcher does.
Path parameters
Name
Type
Description
study_id
uuid
ID of the study to estimate.
Response
Display labels for the whole session and each part of it. Labels use a mixed-unit format: `45 sec`, `1 min 20 sec`, `21 mins`, `1 hour 23 min`.
Field
Type
Description
data.total_label
string
The full participant session — welcome, screener, and all sections.
data.welcome_label
string
The welcome and closing steps, or `null` when the study has no questions yet.
data.screener_label
string
The screener portion only, or `null` when the study has no screener.
data.sections
array[object]
One entry per section in display order, each with `name` and `duration_label`.
Grades the study against Outset's research-quality rubric and returns 202 with a run to poll. A GUIDE check grades the questions (yes/no stems, double-barreled or leading phrasing, missing escape hatches, matrix misuse, and more); a CONTEXT check grades the free-text background given to the AI interviewer.
Path parameters
Name
Type
Description
study_id
uuid
ID of the study to grade.
Request body
Which check to run.
Field
Type
Description
typerequired
enum
`GUIDE` to grade the study's questions, or `CONTEXT` to grade its background text.
{
"type": "GUIDE"
}
Response
The newly started run, with `status` set to `QUEUED`.
Field
Type
Description
data.id
uuid
Run ID; poll it at `GET /v2/studies/{study_id}/quality-checks/{run_id}/`.
data.type
enum
`GUIDE` or `CONTEXT`, echoing the request.
data.status
enum
`QUEUED` on creation; it moves to `RUNNING`, then `COMPLETED` or `FAILED`.
Each call spends model credits, so it is not safe to retry blindly. The check never edits the study — apply the suggestions yourself through the study-content endpoints. Takes studies:write rather than studies:read because each run is persisted and spends model credits; checks that write nothing (validate, the recruitment estimates) stay on the read scope.
Returns the run's current state. The shape is the same on every call: while grading, status is QUEUED or RUNNING and findings is empty; on completion it flips to COMPLETED with findings, or FAILED. Past runs stay readable, so you can compare results across edits.
Path parameters
Name
Type
Description
study_id
uuid
ID of the study the run belongs to.
run_id
uuid
ID returned when the check was started.
Response
The run and its findings.
Field
Type
Description
data.id
uuid
Run ID.
data.type
enum
`GUIDE` or `CONTEXT`; it determines which finding fields are populated.
data.status
enum
`QUEUED`, `RUNNING`, `COMPLETED`, or `FAILED`.
data.summary
string
One-paragraph verdict on the study, empty until the run completes.
data.questions_checked
integer
How many questions were graded; `GUIDE` runs only.
data.graded_internal_context
boolean
Whether the study's internal context — the private research context visible to the AI interviewer but never shown to participants — was graded as well; `CONTEXT` runs only.
data.findings
array[object]
Live findings, newest run state; dismissed findings are omitted.
data.findings[].id
uuid
Finding ID.
data.findings[].severity
enum
`high`, `medium`, or `low`.
data.findings[].category
string
Rubric category the finding falls under — for context runs one of `interview_questions`, `conflicting_instructions`, `leading`, `too_long`, `other`.
data.findings[].title
string
Short label for the problem.
data.findings[].issue
string
Explanation of what is wrong and why it hurts the interview.
data.findings[].suggestion
string
Proposed rewrite, or an empty string when no rewrite fits.
data.findings[].source_text
string
The exact text the finding is about.
data.findings[].target_type
enum
`GUIDE_QUESTION` or `SCREENER_QUESTION`, saying which of the two ID fields below carries the reference; `GUIDE` runs only.
data.findings[].question_id
uuid
Guide question the finding is about, or `null`; `GUIDE` runs only.
data.findings[].screener_question_id
uuid
Screener question the finding is about, or `null`; `GUIDE` runs only.
data.findings[].study_order
integer
Position of that question in the study, so you can point a researcher at it; `GUIDE` runs only.
data.findings[].source_field
enum
Which part of the question the finding is about — `stem`, `probing_instructions`, `options`, `display_logic`, or `skip_logic`; `GUIDE` runs only.
data.findings[].target_field
enum
Which context field the finding came from — `study_context` or `study_internal_context`; `CONTEXT` runs only.
{
"data": {
"id": "f81c26b4-30ad-4de9-8b57-c1e94a205d76",
"type": "GUIDE",
"status": "COMPLETED",
"summary": "The guide is solid overall; two questions are double-barreled.",
"questions_checked": 12,
"findings": [
{
"id": "7a2d5c81-9e04-4b3f-a6c8-30f19b7e2d54",
"target_type": "GUIDE_QUESTION",
"question_id": "a7c30f18-4d92-4b6a-9f57-0e1b8c2d4a63",
"screener_question_id": null,
"study_order": 4,
"severity": "medium",
"category": "double_barreled",
"title": "Two questions in one",
"issue": "Asks about both delivery speed and packaging, so answers can't be attributed to either.",
"suggestion": "How did you feel about the delivery speed?",
"source_field": "stem",
"source_text": "How did you feel about the delivery speed and the packaging?"
}
]
}
}
A failed run does not expose the underlying error text. Re-run the check; if it fails repeatedly, contact support with the run ID.
Returns how participants moved through the study — completions, screen-outs, over-quota and fraud counts — plus per-question reach and answer counts and a cost breakdown by transaction type. Built for audit and invoicing rather than analysis; interview transcripts and reports live in the analysis chapters.
Path parameters
Name
Type
Description
study_id
uuid
ID of the study to report on.
Response
Summary, per-question reach, and costs.
Field
Type
Description
data.study_id
uuid
The study these stats describe.
data.summary.total_interviews
integer
Participants who answered at least one question or were screened out, excluding archived interviews.
data.summary.total_completed
integer
Interviews that finished cleanly — not screened out, not fraudulent, not low quality, not archived.
data.summary.total_incomplete
integer
Interviews that started but did not finish, including sessions that timed out and low-quality ones.
data.summary.total_screened_out
integer
Participants the screener disqualified.
data.summary.total_over_quota
integer
Participants turned away because their quota cell was already full.
data.summary.total_fraud
integer
Interviews flagged as fraudulent.
data.summary.total_active_sessions
integer
Interviews still in progress within the 24-hour session window.
data.summary.total_archived
integer
Interviews the researcher archived.
data.summary.first_completed_at
datetime
When the first interview completed, or `null` when none have.
data.summary.last_completed_at
datetime
When the most recent interview completed, or `null` when none have.
data.summary.screen_out_limit_usd
string
Spend cap on screen-out bonuses for the active recruitment, or `null` when there is none.
data.questions[].question_id
uuid
Guide question the row counts.
data.questions[].text
string
The question as participants saw it.
data.questions[].question_type
enum
Kind of question, matching the type in the study definition.
data.questions[].position
integer
Position of the question in study order, from 0.
data.questions[].reached_count
integer
Clean interviews in which the question was presented.
data.questions[].answered_count
integer
Clean interviews in which the participant answered it, so reached minus answered is the drop-off at this question.
data.screener_questions[].question_id
uuid
Screener question the row counts.
data.screener_questions[].text
string
The screener question as participants saw it.
data.screener_questions[].question_type
enum
Kind of screener question, matching the type in the screener definition.
data.screener_questions[].position
integer
Position of the question in screener order, from 0.
data.screener_questions[].reached_count
integer
Clean interviews in which the screener question was presented.
data.screener_questions[].answered_count
integer
Clean interviews in which the participant selected an option or typed an answer; the rows are absent entirely when the study has no screener.
data.costs.recruitment_created_usd
string
Spend committed when recruitment was created.
data.costs.recruitment_participants_increased_usd
string
Spend added by raising the participant target.
data.costs.recruitment_incentive_increased_usd
string
Spend added by raising the per-participant reward.
data.costs.screened_out_bonus_usd
string
Bonuses paid to participants who were screened out.
data.costs.incentive_payout_usd
string
Incentives paid to completed participants.
data.costs.concierge_recruitment_usd
string
Charges for concierge (Outset-managed) recruitment.
data.costs.refund_usd
string
Amounts refunded for unfilled recruitment.
{
"data": {
"study_id": "9c4f1e2a-6d3b-4f18-b2c7-5a0e8d1f3b42",
"summary": {
"total_interviews": 268,
"total_completed": 204,
"total_incomplete": 41,
"total_screened_out": 19,
"total_over_quota": 4,
"total_fraud": 0,
"total_active_sessions": 3,
"total_archived": 2,
"first_completed_at": "2026-08-02T11:04:19Z",
"last_completed_at": "2026-08-11T18:52:07Z",
"screen_out_limit_usd": "150.00"
},
"questions": [
{
"question_id": "a7c30f18-4d92-4b6a-9f57-0e1b8c2d4a63",
"text": "Walk me through the last time you checked out on our site.",
"question_type": "TEXT",
"position": 0,
"reached_count": 204,
"answered_count": 201
},
{
"…": "one row per guide question, in study order"
}
],
"screener_questions": [
{
"…": "same row shape, in screener order"
}
],
"costs": {
"recruitment_created_usd": "2400.00",
"screened_out_bonus_usd": "38.00",
"incentive_payout_usd": "1020.00",
"refund_usd": "0.00",
"…": "remaining cost keys omitted — see the field table above"
}
}
}
Results are cached for an hour, so a stat can lag a just-completed interview. Costs are settled amounts only — pending transactions are excluded — and each key is a positive USD amount regardless of the transaction's sign.
Turns AI analysis of the study's participant recordings on or off. This is a compliance gate, not an edit surface: it accepts exactly one field and no other part of the study can be changed through it.
Path parameters
Name
Type
Description
study_id
uuid
ID of the study to approve.
Request body
The single whitelisted field.
Field
Type
Description
visual_intelligence_enabledrequired
boolean
Whether participant recordings for this study may be processed by Outset's vision models.
Enabling this force-enables model processing of participant recordings, so every change is recorded in the study's audit history with the credential that made it. For partner credentials this is deliberately a cross-organization write: a partner administrator can approve on a study that lives in a client organization.
Lists the voices available for the ADVANCED interviewer-voice engine, featured voices first. Pick an id here and set it as interviewer_voice_id on a study. The SIMPLE engine uses one default voice per language and ignores this catalog.
Query parameters
Name
Type
Description
language
string · default en
ISO 639-1 code to filter voices by; pass an empty value to return every voice regardless of language.
Response
The matching voices. Returned whole — the catalog is bounded, not paginated.
Field
Type
Description
data[].id
uuid
Voice ID; pass it back as `interviewer_voice_id`.
data[].name
string
Human-facing name of the voice.
data[].description
string
Short blurb about how the voice sounds; may be empty.
data[].language
string
ISO 639-1 code of the language the voice was trained for.
Lists the trained custom interviewer personas your organization can assign to a study — your own and your organization's, excluding archived ones and any still training. Assign one with custom_interviewer_persona_id on the study.
Response
The assignable personas, newest first. Returned whole — the catalog is bounded, not paginated.
Field
Type
Description
data[].id
uuid
Persona ID; pass it back as `custom_interviewer_persona_id`.
The three built-in styles (HIGH_TWO_FOLLOW_UP, LOW_TWO_FOLLOW_UP, NONE) are not listed here — they are values of the study's interviewer_persona field, and a custom persona and a built-in one are mutually exclusive.
A study's interview guide is an ordered list of sections, each holding an ordered list of questions. Sections come in three types — STANDARD, CONCEPT_TESTING (variants participants compare, with per-concept fields and stimuli), and PARTICIPANT_UPLOAD (participants submit a file the interview analyzes inline) — and a section can be converted between them in place without losing its questions. On top of that ordering sit three independent randomization mechanisms (section blocks, study-level section randomization, within-section question randomization) and two kinds of per-question logic (display conditions, which decide whether a question is asked, and skip conditions, which route the interview elsewhere once it is answered). Everything here is workspace-scoped through its study and requires studies:write to mutate; a published study's guide is frozen — every write in this chapter is refused with 409 published_study once the study is published, so edit before launch.
Question type drives which configuration fields are honored. The type-specific fields are documented on the create endpoint; this is the map:
TASK is only available on usability studies (screenshare and mobile UX), MATRIX can only be created (never converted into), and IMAGE_CODESIGN is entitlement-gated per organization — each rejects with 422 rather than silently downgrading.
Lists the study's sections in guide order, with their type, randomization membership, and — for concept-testing and participant-upload sections — their type-specific configuration. Use this to discover the section IDs the rest of this chapter addresses.
Path parameters
Name
Type
Description
study_id
uuid
The study whose guide you are reading.
Query parameters
Name
Type
Description
page_size
integer · default 50
Sections per page, up to 200.
cursor
string
Opaque cursor from a previous page's `next_cursor`.
Response
Cursor-paginated list of sections in guide order (oldest position first, unlike the newest-first default).
Field
Type
Description
data[].id
uuid
Section identifier.
data[].name
string
Section name, shown to participants and in the study editor.
data[].type
enum
One of `STANDARD`, `CONCEPT_TESTING`, `PARTICIPANT_UPLOAD`.
data[].position
integer
Zero-indexed position of the section in the guide.
data[].question_count
integer
Number of questions in the section, excluding matrix rows.
data[].section_block_id
uuid|null
Section block this section belongs to, or null if it is standalone.
data[].randomize_concepts
boolean|null
Concept-testing sections only: whether concept order is randomized per participant.
data[].randomize_concepts_count
integer|null
Concept-testing sections only: how many concepts each participant sees, or null for all of them.
data[].upload_config
object|null
Participant-upload sections only: the section's upload configuration, in the shape returned by the upload-config endpoint.
Creates a section of any of the three types. A CONCEPT_TESTING section can be seeded with concept names inline; a PARTICIPANT_UPLOAD section is created with a default upload config (image, one file, requirements validation on) that you then complete through the upload-config endpoint.
Path parameters
Name
Type
Description
study_id
uuid
The study to add the section to.
Request body
Section name plus the type and any type-specific seed values.
Field
Type
Description
namerequired
string
Section name, shown to participants and in the study editor.
type
enum
`STANDARD` (default), `CONCEPT_TESTING`, or `PARTICIPANT_UPLOAD`.
position
integer
Zero-indexed position in the guide; omit to append at the end.
concept_names
array[string]
Concept-testing only: names of the concepts to seed the section with, in order.
randomize_concepts
boolean
Concept-testing only: whether each participant sees the concepts in a random order (defaults to true).
randomize_concepts_count
integer
Concept-testing only: how many concepts each participant sees; omit to show every concept.
media_type
enum
Participant-upload only: `IMAGE` (default), `VIDEO`, or `DOCUMENT`.
`VIDEO` or `DOCUMENT` uploads are requested and the organization is not entitled to them — the request is rejected rather than downgraded to `IMAGE`.
A concept-testing section created with concept_names still needs each concept's content (field values or a stimulus) before the study will pass publish validation, and a participant-upload section needs upload instructions and at least one question.
Renames a section, changes concept randomization, or converts the section to another type in place, keeping its questions. Only the fields you send are changed.
Path parameters
Name
Type
Description
study_id
uuid
The study the section belongs to.
section_id
uuid
The section to update.
Request body
Any subset of the section's mutable fields.
Field
Type
Description
name
string
New section name.
type
enum
Convert the section to `STANDARD`, `CONCEPT_TESTING`, or `PARTICIPANT_UPLOAD`, keeping its questions.
randomize_concepts
boolean
Concept-testing only: whether each participant sees the concepts in a random order.
randomize_concepts_count
integer|null
Concept-testing only: how many concepts each participant sees; send `null` to go back to showing all of them.
Response
The updated section, plus the fields the request actually changed.
Field
Type
Description
data.id
uuid
Section identifier.
data.name
string
Section name, shown to participants and in the study editor.
data.type
enum
One of `STANDARD`, `CONCEPT_TESTING`, `PARTICIPANT_UPLOAD`.
data.position
integer
Zero-indexed position of the section in the guide.
data.question_count
integer
Number of questions in the section, excluding matrix rows.
data.section_block_id
uuid|null
Section block this section belongs to, or null if it is standalone.
data.randomize_concepts
boolean|null
Concept-testing sections only: whether concept order is randomized per participant.
data.randomize_concepts_count
integer|null
Concept-testing sections only: how many concepts each participant sees, or null for all of them.
data.upload_config
object|null
Participant-upload sections only: the section's upload configuration, in the shape returned by the upload-config endpoint.
data.created_at
timestamp
When the section was created, ISO 8601 UTC.
data.changes
array[object]
One entry per field that changed, each naming the `field` with its `old` and `new` value; empty when the section already had the requested values.
A concept-randomization field is sent for a section that is not a concept-testing section.
Converting into CONCEPT_TESTING seeds a default concept table; converting into PARTICIPANT_UPLOAD seeds an image upload config. Convert first, then set randomization or upload settings in a second call — the conversion ignores them.
Deletes a section and every question inside it. Logic on other questions that referenced the deleted questions is cascaded away.
Path parameters
Name
Type
Description
study_id
uuid
The study the section belongs to.
section_id
uuid
The section to delete.
Response
`204 No Content` — empty body. The section and its questions are gone; subsequent reads of them 404.
Irreversible through the API, and it takes the section's questions with it. To change a section's type without losing questions, use PATCH with type instead of deleting and re-creating.
Copies a section and everything in it — questions with their options, stimuli, display and skip conditions and validation rules, plus concepts and concept fields or the upload config. The copy is placed immediately after the original.
Path parameters
Name
Type
Description
study_id
uuid
The study the section belongs to.
section_id
uuid
The section to duplicate.
Response
The new section, in the shape returned by the list endpoint — its own `id`, the position right after the original, and the copied question count.
Field
Type
Description
data.id
uuid
Section identifier.
data.name
string
Section name, shown to participants and in the study editor.
data.type
enum
One of `STANDARD`, `CONCEPT_TESTING`, `PARTICIPANT_UPLOAD`.
data.position
integer
Zero-indexed position of the section in the guide.
data.question_count
integer
Number of questions in the section, excluding matrix rows.
data.section_block_id
uuid|null
Section block this section belongs to, or null if it is standalone.
data.randomize_concepts
boolean|null
Concept-testing sections only: whether concept order is randomized per participant.
data.randomize_concepts_count
integer|null
Concept-testing sections only: how many concepts each participant sees, or null for all of them.
data.upload_config
object|null
Participant-upload sections only: the section's upload configuration, in the shape returned by the upload-config endpoint.
Condition rules pointing at questions inside the section are rewired to the copies; rules pointing at questions elsewhere in the guide keep pointing at the originals.
Configures what participants upload in a participant-upload section and how their files are validated. Only the fields you send are changed.
Path parameters
Name
Type
Description
study_id
uuid
The study the section belongs to.
section_id
uuid
The participant-upload section to configure.
Request body
Any subset of the upload configuration.
Field
Type
Description
instructions
string
Prompt telling participants what to upload, e.g. "Upload a photo of your receipt".
requirements
string
What an uploaded file must show to be accepted; also used as the analysis criteria when validation is enforced.
enforce_requirements_validation
boolean
Whether uploads are checked against `requirements` before being accepted.
requirements_bypass_max_attempts
integer
How many failed validation attempts a participant may make before being allowed through anyway (minimum 1).
max_files
integer
Maximum number of files one participant may upload in this section (minimum 1).
media_type
enum
`IMAGE`, `VIDEO`, or `DOCUMENT`.
{
"instructions": "Upload a photo of your most recent grocery receipt.",
"requirements": "The total and the purchase date must be legible.",
"enforce_requirements_validation": true,
"requirements_bypass_max_attempts": 2,
"max_files": 1
}
Response
The section's complete upload configuration after the change, plus the fields the request actually changed.
Field
Type
Description
data.section_id
uuid
The participant-upload section this configuration belongs to.
data.instructions
string
Prompt telling participants what to upload.
data.requirements
string
What an uploaded file must show to be accepted.
data.enforce_requirements_validation
boolean
Whether uploads are checked against `requirements` before being accepted.
data.requirements_bypass_max_attempts
integer
How many failed validation attempts a participant may make before being allowed through anyway.
data.max_files
integer
Maximum number of files one participant may upload in this section.
data.media_type
enum
`IMAGE`, `VIDEO`, or `DOCUMENT`.
data.changes
array[object]
One entry per field that changed, each naming the `field` with its `old` and `new` value.
{
"data": {
"section_id": "c05a1d77-8b41-4a63-9e2f-b3d18a6c7e94",
"instructions": "Upload a photo of your most recent grocery receipt.",
"requirements": "The total and the purchase date must be legible.",
"enforce_requirements_validation": true,
"requirements_bypass_max_attempts": 2,
"max_files": 1,
"media_type": "IMAGE",
"changes": [
{
"field": "max_files",
"old": 3,
"new": 1
}
]
}
}
Errors
Status
Code
When
404
not_found
The section exists but is not a participant-upload section — it has no upload config to address.
403
feature_not_enabled
Switching to `VIDEO` or `DOCUMENT` without the organization being entitled to it. Keeping the current value is always allowed.
Groups sections into a named block with shared presentation rules — the mechanism for randomizing several independent sets of sections. Members need not be adjacent in the guide.
Path parameters
Name
Type
Description
study_id
uuid
The study to create the block on.
Request body
Block name, membership, and presentation rules.
Field
Type
Description
namerequired
string
Researcher-facing block name; must be unique within the study.
section_idsrequired
array[uuid]
Sections that make up the block; at least one.
mode
enum
`NONE` (default, authored order), `MONADIC` (a balanced random subset, then shuffled), or `SEQUENTIAL_MONADIC` (all members in a balanced random order).
subset_count
integer
Required when `mode` is `MONADIC`: how many members each participant sees, from 1 up to the member count.
randomize_block_position
boolean
Whether this block may trade its guide position with other blocks that also opt in (defaults to false).
Randomizes the order — or the selection — of sections across participants (monadic testing) for one set of sections. Idempotent: the body is the complete desired state, and sections attached today but absent from the body are detached.
Path parameters
Name
Type
Description
study_id
uuid
The study to configure.
Request body
The randomization mode and the complete set of participating sections.
Field
Type
Description
moderequired
enum
`SEQUENTIAL` (every participant sees every attached section, in a random order) or `SUBSET` (each participant sees `subset_count` of them).
subset_count
integer
Required when `mode` is `SUBSET`: how many sections each participant sees, from 1 up to the number attached.
section_idsrequired
array[uuid]
The complete set of sections that participate; any section attached but not listed is detached.
Randomizes the order — or the selection — of questions inside one section. There is one configuration per section, so every participating question shares the mode.
Path parameters
Name
Type
Description
study_id
uuid
The study the section belongs to.
section_id
uuid
The section whose questions are randomized.
Request body
The randomization mode and the complete set of participating questions.
Field
Type
Description
moderequired
enum
`SEQUENTIAL` (every attached question, in a random order) or `SUBSET` (each participant sees `subset_count` of them).
subset_count
integer
Required when `mode` is `SUBSET`: how many questions each participant sees.
question_idsrequired
array[uuid]
The complete set of participating questions, all belonging to this section; questions attached but not listed revert to their fixed order.
Response
The stored configuration and the questions attached to it.
Field
Type
Description
data.id
uuid
Randomization configuration identifier.
data.mode
enum
`SEQUENTIAL` (everything attached, in a random order) or `SUBSET` (each participant sees `subset_count` of them).
data.subset_count
integer|null
How many of the attached items each participant sees; set only in `SUBSET` mode.
data.section_id
uuid
The section this configuration belongs to.
data.question_ids
array[uuid]
The questions now attached to the configuration — the set you sent.
Lists the study's questions in guide order with their full per-type configuration, options, and attached logic. Matrix rows are returned inside their parent question rather than as top-level entries.
Path parameters
Name
Type
Description
study_id
uuid
The study whose questions you are reading.
Query parameters
Name
Type
Description
section_id
uuid
Return only the questions in this section.
page_size
integer · default 50
Questions per page, up to 200.
cursor
string
Opaque cursor from a previous page's `next_cursor`.
Response
Cursor-paginated list of questions in guide order. Fields that do not apply to a question's type are returned as null.
Field
Type
Description
data[].id
uuid
Question identifier.
data[].section_id
uuid
Section the question belongs to.
data[].position
integer
Zero-indexed position within the section.
data[].text
string
The question wording shown to participants.
data[].question_type
enum
The answer format — see the table in this chapter's introduction.
data[].options
array[object]
Answer choices, each with `id`, `text`, and `anchor_type` (`OTHER`, `NONE`, or null); for rating and matrix questions these are the generated scale points.
data[].optional
boolean
Whether the participant may decline to answer and move on.
data[].deep_probing
enum
How hard the AI interviewer probes after the first answer: `NONE`, `SINGLE`, `STANDARD`, `DEEP_PROBING`, or `ABYSS`.
data[].probing_instructions
string|null
A scripted conditional probe the interviewer uses on top of the probing level.
data[].ask_verbatim
boolean
Whether the interviewer must read the text exactly rather than rephrasing it.
data[].has_display_logic
boolean
Whether display conditions gate this question.
data[].condition_mode
enum
How display conditions are read: `SHOW_IF` (ask only when one matches) or `SKIP_IF` (skip when one matches).
data[].display_conditions
array[object]
Display conditions on this question, each with an `id` and its `rules`.
data[].skip_conditions
array[object]
Skip conditions on this question, each with an `id`, its `rules`, and either `target_question_id` or `end_immediately`.
data[].matrix_rows
array[object]|null
Matrix questions only: the row statements, each with `id` and `text`, in display order.
data[].stimulus
object|null
Attached stimulus, if any, with a short-lived presigned URL — see the Media & Stimuli chapter.
data[].rating_scale
integer
Number of points on a RATING question's scale; null for other question types.
data[].rating_labels
object
Optional labels for the low and high ends of a RATING scale; null when unlabeled or not a RATING question.
{
"data": [
{
"id": "7a4e1b93-6d20-4f8c-b512-0ac9e3f47d61",
"section_id": "3f1c9a02-5b3d-4e21-9a77-6c2f0c1de401",
"position": 0,
"text": "How likely are you to recommend us to a friend?",
"question_type": "RATING",
"rating_scale": "ZERO_THROUGH_TEN",
"rating_labels": [
"Not at all likely",
"Extremely likely"
],
"options": [
{
"id": "d1b0c5a4-9e77-4a12-8c31-2f6b4d9e0a83",
"text": "0",
"anchor_type": null
}
],
"optional": false,
"deep_probing": "STANDARD",
"probing_instructions": null,
"ask_verbatim": false,
"has_display_logic": false,
"condition_mode": "SHOW_IF",
"display_conditions": [],
"skip_conditions": [],
"matrix_rows": null,
"stimulus": null
}
],
"next_cursor": null,
"has_more": false
}
Adds a question to a section with its full per-type configuration. The type determines which of the fields below are honored; unknown fields are rejected, and fields that do not apply to the type are ignored.
Path parameters
Name
Type
Description
study_id
uuid
The study to add the question to.
Request body
The question's wording, type, target section, and type-specific configuration.
Field
Type
Description
section_idrequired
uuid
Section to add the question to.
textrequired
string
The question wording shown to participants.
question_typerequired
enum
One of `TEXT`, `MULTIPLE_CHOICE`, `MULTIPLE_SELECT`, `RATING`, `STACK_RANK`, `NUMBER_INPUT`, `TASK`, `INFO_ONLY`, `TYPED_RESPONSE`, `MATRIX`, `IMAGE_CODESIGN`.
position
integer
Zero-indexed position within the section; omit to append at the end.
options
array
Answer choices for `MULTIPLE_CHOICE`, `MULTIPLE_SELECT`, and `STACK_RANK`; each entry is a string or an object with `text` and `anchor_type` (`OTHER` or `NONE`).
rating_scale
enum
Required for `RATING`: `ZERO_THROUGH_TEN`, `ONE_THROUGH_FIVE`, `ONE_THROUGH_SEVEN`, or `ONE_THROUGH_TEN`; the scale points are generated for you.
rating_labels
array[string]
Endpoint labels for a rating scale, at least two, e.g. `["Not at all likely", "Extremely likely"]`.
deep_probing
enum
How hard the interviewer probes after the first answer — `NONE`, `SINGLE`, `STANDARD` (default), `DEEP_PROBING`, `ABYSS`.
probing_instructions
string
One scripted conditional probe: the shallow answer that triggers it and the single follow-up to ask.
ask_verbatim
boolean
Read the text exactly as written instead of letting the interviewer rephrase it (defaults to false).
optional
boolean
Let the participant decline to answer; mutually exclusive with skip logic on the same question.
has_display_logic
boolean
Whether display conditions gate this question; must be true for display conditions to take effect.
min_selections
integer
`MULTIPLE_SELECT` only: fewest options the participant must pick.
max_selections
integer
`MULTIPLE_SELECT` only: most options the participant may pick.
preserve_options_order
boolean
Keep the authored option order (default true); false randomizes it per participant to reduce order bias.
allow_custom_other_text
boolean
Let participants type their own text when they pick an `OTHER` anchor option.
screenshare_url
string
`TASK` only: the URL or deep link participants open to perform the task.
allow_empty_screenshare_url
boolean
`TASK` only: the task uses whatever is already on the participant's screen, so no URL is needed.
screenshare_enabled
boolean
Record the participant's screen for this question on a screenshare study.
task_completion_definition_enabled
boolean
`TASK` only: judge task completion against an explicit definition during post-interview analysis.
task_completion_definition
string
`TASK` only: what counts as completing the task, up to 255 characters.
figma_analytics_enabled
boolean
`TASK` only: capture a Figma prototype's frame geometry for the click heatmap; requires a connected Figma account and a Figma prototype URL.
matrix_preset
enum
Required for `MATRIX`: `AGREEMENT`, `DIFFICULTY`, `SATISFACTION`, `USEFULNESS`, `COMFORT`, `CLARITY`, `INTEREST`, `EASE`, `TRUST`, `PROFESSIONAL`, or `CUSTOM` (which requires your own scale points).
matrix_rows
array[string]
Required for `MATRIX`: the row statements being rated, at most 9.
matrix_label_display
enum
`MATRIX` only: `SHOW_ALL` (default) or `SHOW_FIRST_AND_LAST` to label only the endpoints.
stimulus_duration
number
Minimum seconds an attached stimulus must be shown before the participant can continue.
watermark_stimulus_enabled
boolean
Composite the interview's identifier onto the stimulus image so leaked screenshots are traceable.
codesign_max_tries
integer
`IMAGE_CODESIGN` only: how many image edits a participant may request, 1-5 (default 3).
number_input_label
string
`NUMBER_INPUT` only: label next to the numeric field; an empty string hides the label entirely.
{
"section_id": "3f1c9a02-5b3d-4e21-9a77-6c2f0c1de401",
"text": "How much do you agree with each statement about the checkout flow?",
"question_type": "MATRIX",
"matrix_preset": "AGREEMENT",
"matrix_rows": [
"It was easy to find what I needed",
"I trusted the payment step"
],
"matrix_label_display": "SHOW_FIRST_AND_LAST"
}
Response
The created question, in the shape returned by the list endpoint. Fields that do not apply to the question's type are returned as null.
Field
Type
Description
data.id
uuid
Question identifier.
data.section_id
uuid
Section the question belongs to.
data.position
integer
Zero-indexed position within the section.
data.text
string
The question wording shown to participants.
data.question_type
enum
The answer format — see the table in this chapter's introduction.
data.options
array[object]
Answer choices, each with `id`, `text`, and `anchor_type` (`OTHER`, `NONE`, or null); for rating and matrix questions these are the generated scale points.
data.optional
boolean
Whether the participant may decline to answer and move on.
data.deep_probing
enum
How hard the AI interviewer probes after the first answer: `NONE`, `SINGLE`, `STANDARD`, `DEEP_PROBING`, or `ABYSS`.
data.probing_instructions
string|null
A scripted conditional probe the interviewer uses on top of the probing level.
data.ask_verbatim
boolean
Whether the interviewer must read the text exactly rather than rephrasing it.
data.has_display_logic
boolean
Whether display conditions gate this question.
data.condition_mode
enum
How display conditions are read: `SHOW_IF` (ask only when one matches) or `SKIP_IF` (skip when one matches).
data.display_conditions
array[object]
Display conditions on this question, each with an `id` and its `rules`.
data.skip_conditions
array[object]
Skip conditions on this question, each with an `id`, its `rules`, and either `target_question_id` or `end_immediately`.
data.rating_scale
enum|null
Rating questions only: the scale the generated options span.
data.rating_labels
array[string]|null
Endpoint labels for a rating or custom-matrix scale.
data.matrix_preset
enum|null
Matrix questions only: the preset the scale columns come from.
data.matrix_label_display
enum|null
Matrix questions only: `SHOW_ALL` or `SHOW_FIRST_AND_LAST`.
data.matrix_rows
array[object]|null
Matrix questions only: the row statements, each with `id` and `text`, in display order.
data.stimulus
object|null
Attached stimulus, if any, with a short-lived presigned URL — see the Media & Stimuli chapter.
data.created_at
timestamp
When the question was created, ISO 8601 UTC.
{
"data": {
"id": "e58c2f10-9a4b-4d77-83e6-14b0d7c5a962",
"section_id": "3f1c9a02-5b3d-4e21-9a77-6c2f0c1de401",
"position": 3,
"text": "How much do you agree with each statement about the checkout flow?",
"question_type": "MATRIX",
"matrix_preset": "AGREEMENT",
"matrix_label_display": "SHOW_FIRST_AND_LAST",
"matrix_rows": [
{
"id": "1c9f7d63-0b48-4e15-a2d9-53f7c1e08a44",
"text": "It was easy to find what I needed"
},
{
"id": "4b6a0e52-77c3-49d1-8f0a-9e21b5d3c706",
"text": "I trusted the payment step"
}
],
"created_at": "2026-08-11T14:07:41Z"
}
}
Errors
Status
Code
When
422
incompatible_question_type
The type cannot exist on this study or section — `TASK` outside a usability study, `IMAGE_CODESIGN` on a voice or mobile-UX study, on a non-standard section, or without the co-design entitlement.
422
question_limit_exceeded
A matrix asks for more than 9 rows or more than 9 scale points; the participant renderer cannot draw it, so split the question instead.
Question text embedding a markdown image turns ask_verbatim on automatically — otherwise the interviewer could paraphrase the wording and the image would never be shown. MATRIX questions can only be created, never converted into.
Changes any subset of a question's wording, type, or configuration. It accepts the create body's fields plus the ones below, and a question's type can be converted in place — to any type except MATRIX.
Path parameters
Name
Type
Description
study_id
uuid
The study the question belongs to.
question_id
uuid
The question to update.
Request body
Any subset of the create fields, plus the edit-only fields below. Sending `options` replaces the whole option list; for a matrix it replaces the scale-point columns.
Field
Type
Description
condition_mode
enum
How this question's display conditions are read: `SHOW_IF` (ask only when a condition matches) or `SKIP_IF` (skip when one matches).
typed_response_validation_enabled
boolean
`TYPED_RESPONSE` only: validate the participant's answer before accepting it.
typed_response_is_url
boolean
`TYPED_RESPONSE` only: require the answer to be an absolute URL.
typed_response_validation_root_op
enum
`TYPED_RESPONSE` only: `AND` requires every validation rule group to pass, `OR` any one of them.
typed_response_max_retries
integer
`TYPED_RESPONSE` only: how many failed validation attempts are allowed before the answer is accepted anyway, 1-5.
matrix_display_mode
enum
`MATRIX` only: currently `LIKERT` is the only supported layout.
clear_stimulus_duration
boolean
Remove the minimum stimulus display time entirely, rather than changing it.
clear_number_input_label
boolean
Restore the built-in `NUMBER_INPUT` label instead of a custom one.
Response
The updated question, plus the fields the request actually changed.
Field
Type
Description
data.id
uuid
Question identifier.
data.section_id
uuid
Section the question belongs to.
data.position
integer
Zero-indexed position within the section.
data.text
string
The question wording shown to participants.
data.question_type
enum
The answer format — see the table in this chapter's introduction.
data.options
array[object]
Answer choices, each with `id`, `text`, and `anchor_type` (`OTHER`, `NONE`, or null); for rating and matrix questions these are the generated scale points.
data.optional
boolean
Whether the participant may decline to answer and move on.
data.deep_probing
enum
How hard the AI interviewer probes after the first answer: `NONE`, `SINGLE`, `STANDARD`, `DEEP_PROBING`, or `ABYSS`.
data.probing_instructions
string|null
A scripted conditional probe the interviewer uses on top of the probing level.
data.ask_verbatim
boolean
Whether the interviewer must read the text exactly rather than rephrasing it.
data.has_display_logic
boolean
Whether display conditions gate this question.
data.condition_mode
enum
How display conditions are read: `SHOW_IF` (ask only when one matches) or `SKIP_IF` (skip when one matches).
data.display_conditions
array[object]
Display conditions on this question, each with an `id` and its `rules`.
data.skip_conditions
array[object]
Skip conditions on this question, each with an `id`, its `rules`, and either `target_question_id` or `end_immediately`.
data.rating_scale
enum|null
Rating questions only: the scale the generated options span.
data.rating_labels
array[string]|null
Endpoint labels for a rating or custom-matrix scale.
data.matrix_preset
enum|null
Matrix questions only: the preset the scale columns come from.
data.matrix_label_display
enum|null
Matrix questions only: `SHOW_ALL` or `SHOW_FIRST_AND_LAST`.
data.matrix_rows
array[object]|null
Matrix questions only: the row statements, each with `id` and `text`, in display order.
data.stimulus
object|null
Attached stimulus, if any, with a short-lived presigned URL — see the Media & Stimuli chapter.
data.created_at
timestamp
When the question was created, ISO 8601 UTC.
data.changes
array[object]
One entry per field that changed, each naming the `field` with its `old` and `new` value; empty when the question already had the requested values.
{
"data": {
"id": "7a4e1b93-6d20-4f8c-b512-0ac9e3f47d61",
"section_id": "3f1c9a02-5b3d-4e21-9a77-6c2f0c1de401",
"position": 0,
"text": "How likely are you to recommend us to a colleague?",
"question_type": "RATING",
"rating_scale": "ZERO_THROUGH_TEN",
"rating_labels": [
"Not at all likely",
"Extremely likely"
],
"deep_probing": "DEEP_PROBING",
"created_at": "2026-08-11T14:05:12Z",
"changes": [
{
"field": "text",
"old": "How likely are you to recommend us to a friend?",
"new": "How likely are you to recommend us to a colleague?"
},
{
"field": "deep_probing",
"old": "STANDARD",
"new": "DEEP_PROBING"
}
]
}
}
Converting a question's type keeps its wording and, where compatible, its options — but a type change can invalidate logic elsewhere that sources this question, since operators are validated against the source question's type.
Deletes a question, its options, and its own skip and display conditions. Logic on other questions that referenced it is cascaded away, and skip conditions routing to it lose their target.
Path parameters
Name
Type
Description
study_id
uuid
The study the question belongs to.
question_id
uuid
The question to delete.
Response
`204 No Content` — empty body. The question is gone; subsequent reads of it 404.
Call the delete-preview endpoint first when other questions may depend on this one — the cascade is silent and irreversible.
Reports what deleting a question would break, without deleting anything: how much logic on other questions references it and would cascade away, and which skip conditions route to it.
Path parameters
Name
Type
Description
study_id
uuid
The study the question belongs to.
question_id
uuid
The question whose deletion you are previewing.
Response
The impact summary. Conditions owned by the question itself are not counted — they go with it and are not a surprise.
Field
Type
Description
data.has_impact
boolean
Whether any other question's logic depends on this one.
data.skip_condition_rules_affected
integer
Skip-logic rules on other questions that reference this question and would be removed.
data.skip_condition_targets_affected
integer
Skip conditions whose routing target is this question and would lose it.
data.display_condition_rules_affected
integer
Display-logic rules on other questions that reference this question and would be removed.
data.affected_question_ids
array[uuid]
The other questions whose logic references this one.
Copies a question with its options, stimulus, display and skip conditions, validation rules, and matrix rows. The copy lands immediately after the original in the same section.
Path parameters
Name
Type
Description
study_id
uuid
The study the question belongs to.
question_id
uuid
The question to duplicate.
Response
The new question, in the shape returned by the list endpoint — its own `id`, the position right after the original, and the copied configuration.
Field
Type
Description
data.id
uuid
Question identifier.
data.section_id
uuid
Section the question belongs to.
data.position
integer
Zero-indexed position within the section.
data.text
string
The question wording shown to participants.
data.question_type
enum
The answer format — see the table in this chapter's introduction.
data.options
array[object]
Answer choices, each with `id`, `text`, and `anchor_type` (`OTHER`, `NONE`, or null); for rating and matrix questions these are the generated scale points.
data.optional
boolean
Whether the participant may decline to answer and move on.
data.deep_probing
enum
How hard the AI interviewer probes after the first answer: `NONE`, `SINGLE`, `STANDARD`, `DEEP_PROBING`, or `ABYSS`.
data.probing_instructions
string|null
A scripted conditional probe the interviewer uses on top of the probing level.
data.ask_verbatim
boolean
Whether the interviewer must read the text exactly rather than rephrasing it.
data.has_display_logic
boolean
Whether display conditions gate this question.
data.condition_mode
enum
How display conditions are read: `SHOW_IF` (ask only when one matches) or `SKIP_IF` (skip when one matches).
data.display_conditions
array[object]
Display conditions on this question, each with an `id` and its `rules`.
data.skip_conditions
array[object]
Skip conditions on this question, each with an `id`, its `rules`, and either `target_question_id` or `end_immediately`.
data.rating_scale
enum|null
Rating questions only: the scale the generated options span.
data.rating_labels
array[string]|null
Endpoint labels for a rating or custom-matrix scale.
data.matrix_preset
enum|null
Matrix questions only: the preset the scale columns come from.
data.matrix_label_display
enum|null
Matrix questions only: `SHOW_ALL` or `SHOW_FIRST_AND_LAST`.
data.matrix_rows
array[object]|null
Matrix questions only: the row statements, each with `id` and `text`, in display order.
data.stimulus
object|null
Attached stimulus, if any, with a short-lived presigned URL — see the Media & Stimuli chapter.
data.created_at
timestamp
When the question was created, ISO 8601 UTC.
{
"data": {
"id": "b7d40c19-8e26-4f53-a90c-2f16d5b8e347",
"section_id": "3f1c9a02-5b3d-4e21-9a77-6c2f0c1de401",
"position": 1,
"text": "How likely are you to recommend us to a friend?",
"question_type": "RATING",
"rating_scale": "ZERO_THROUGH_TEN",
"rating_labels": [
"Not at all likely",
"Extremely likely"
],
"skip_conditions": [
{
"id": "d90e5b72-31af-4c68-95d0-8b2e47c1a063",
"target_question_id": "e58c2f10-9a4b-4d77-83e6-14b0d7c5a962",
"end_immediately": false,
"rules": [
"…"
]
}
],
"created_at": "2026-08-11T15:10:33Z"
}
}
Rules on the copy that referenced the original are rewired to the copy itself; references to other questions are left alone. Re-creating a question by hand instead silently loses its conditions, stimulus, and validation rules.
Sets the order of questions, and optionally moves them all into another section. Send the complete final order of every question in the scope you are reordering, not a diff.
Path parameters
Name
Type
Description
study_id
uuid
The study whose questions you are reordering.
Request body
The full desired order, plus an optional destination section.
Field
Type
Description
question_idsrequired
array[uuid]
Every question in the scope, in the order you want; the first becomes position 0.
target_section_id
uuid
Move all listed questions into this section; omit to reorder them where they are.
The list contains a matrix row — rows are ordered through their parent matrix question, not here.
Questions you leave out of the list keep their existing order values and will collide with the new ones, so list the whole scope. Moving a question into another section detaches it from its old section's question randomization.
Adds routing to a question: once it is answered and every rule in the condition matches, the interview jumps to another question or ends. Rules inside a condition are ANDed; separate conditions on the same question are ORed, so post one condition per alternative.
Path parameters
Name
Type
Description
study_id
uuid
The study the question belongs to.
question_id
uuid
The question this routing lives on.
Request body
The routing destination and the rules that trigger it. Each rule object is `{source_type, source_question_id | source_screener_question_id | url_variable, operator, value | values}`.
Field
Type
Description
target_question_id
uuid
Question to jump to when the rules match; must be later in the guide and mutually exclusive with `end_immediately`.
end_immediately
boolean
End the interview when the rules match, instead of jumping.
rulesrequired
array[object]
At least one rule; all must match for the condition to fire.
rules[].source_type
enum
What the rule reads: `question` (default, a guide question's answer), `screener` (a screener answer), `url_variable` (a value supplied at recruitment time), or `prior_answers` (an AI read over the transcript so far).
rules[].source_question_id
uuid
The guide question whose answer is read; required for `source_type: question`, and usually the question the condition lives on.
rules[].source_screener_question_id
uuid
The screener question whose answer is read; required for `source_type: screener`.
rules[].url_variable
string
Name of the recruitment URL variable to evaluate; required for `source_type: url_variable`.
rules[].operatorrequired
enum
The comparison, constrained by the source's type: `expresses`/`not_expresses` for open text and prior answers; `equals`/`not_equals` for single-choice; `contains`/`not_contains`/`includes_all`/`not_includes_all`/`includes_exactly`/`not_includes_exactly` for multi-select; `equals`/`not_equals`/`greater_than`/`less_than` for rating and number; `ranked_first`/`ranked_last` for stack rank; `task_completed`/`task_not_completed` for tasks.
rules[].value
string
What to compare against — an option's exact text, a number as a string, or the concept the AI should detect for `expresses`.
rules[].values
array[string]
Several option texts for an "is one of" rule; supply exactly one of `value` or `values`, and neither for the task-verdict operators.
The operator is not valid for the source question's type — the resulting rule would be unrenderable and would fail publish validation.
422
unsupported_on_interview_method
An AI-evaluated operator (`expresses` / `not_expresses`) is used on a live voice study, where evaluating it would stall the interviewer between questions.
422
invalid_target
The routing target sits inside a randomized block, so it may not be presented to every participant.
Matrix rows cannot carry skip logic — attach it to the parent matrix question. Skip logic and optional are mutually exclusive on the same question.
Adds a condition that decides whether a question is asked at all, evaluated before the participant reaches it. The rule objects are exactly those of a skip condition; rules inside one condition are ANDed and separate conditions are ORed.
Path parameters
Name
Type
Description
study_id
uuid
The study the question belongs to.
question_id
uuid
The question whose visibility this gates.
Request body
The rules that make this condition match.
Field
Type
Description
rulesrequired
array[object]
At least one rule, in the same shape as a skip condition's rules; all must match for the condition to trigger.
The first condition added to a question sets its mode to SHOW_IF — the question is asked only when a condition matches. Flip it to SKIP_IF with condition_mode on the question if you want the inverse; never mix the two intents on one question.
Lists the study's concepts, with each concept's field values and any attached stimulus. Concept IDs are unique per study, and this is where you discover them — for the endpoints below and for the concept stimulus endpoints in the Media & Stimuli chapter.
Path parameters
Name
Type
Description
study_id
uuid
The study whose concepts you are reading.
Query parameters
Name
Type
Description
section_id
uuid
Return only the concepts in this concept-testing section.
page_size
integer · default 50
Concepts per page, up to 200.
cursor
string
Opaque cursor from a previous page's `next_cursor`.
Response
Cursor-paginated list of concepts, ordered by their section's guide position and then by position within the section.
Field
Type
Description
data[].id
uuid
Concept identifier.
data[].section_id
uuid
Concept-testing section the concept belongs to.
data[].name
string
Short display name for the concept, e.g. "Variant A".
data[].position
integer
Zero-indexed position of the concept within its section.
data[].watermark_stimulus_enabled
boolean
Whether the interview's identifier is composited onto the concept's stimulus image.
data[].field_values
array[object]
This concept's value for each of the section's concept fields, each with `field_id`, `slug`, and `value`.
data[].stimulus
object|null
Attached stimulus, if any, with a short-lived presigned URL — see the Media & Stimuli chapter.
Adds one variant — a tagline, a pricing page, a design — to a concept-testing section. Give it content by setting its field values and, if it is visual, attaching a stimulus.
Path parameters
Name
Type
Description
study_id
uuid
The study to add the concept to.
Request body
The target section, the concept's name, and its stimulus handling.
Field
Type
Description
section_idrequired
uuid
The concept-testing section to add the concept to.
namerequired
string
Short display name for the concept, e.g. "Variant A".
watermark_stimulus_enabled
boolean
Composite the interview's identifier onto the concept's stimulus image so leaked screenshots are traceable (defaults to false).
Defines an attribute every concept in a section carries — "Price", "Tagline" — which becomes a {slug} token usable in that section's question text. The field starts blank on every existing concept.
Path parameters
Name
Type
Description
study_id
uuid
The study the section belongs to.
Request body
The target section plus the field's name and shape.
Field
Type
Description
section_idrequired
uuid
The concept-testing section to define the field on.
namerequired
string
Field name, e.g. "Price"; it is slugified into the `{slug}` token you reference in question text.
field_type
enum
`SINGLE_LINE` (default) for short values or `MULTI_LINE` for paragraphs.
Renames a concept field or changes its type. Renaming regenerates the {slug} token, and the response reports any question text still referencing the old one.
Path parameters
Name
Type
Description
study_id
uuid
The study the field belongs to.
field_id
uuid
The field to update.
Request body
Any subset of the field's mutable attributes.
Field
Type
Description
name
string
New field name; the slug is regenerated from it.
field_type
enum
`SINGLE_LINE` or `MULTI_LINE`.
Response
The updated field with its regenerated slug, plus the question text still pointing at the old one.
Field
Type
Description
data.id
uuid
Concept field identifier.
data.section_id
uuid
Concept-testing section the field is defined on.
data.name
string
Field name, e.g. "Price".
data.slug
string
Slugified field name; the `{slug}` token you reference in the section's question text.
data.field_type
enum
`SINGLE_LINE` or `MULTI_LINE`.
data.position
integer
Zero-indexed position of the field among the section's fields.
data.questions_referencing_old_slug
array[object]
Questions in this section whose text still embeds the previous `{slug}` token, each with `id` and `text`; empty when nothing references it.
Deletes a concept field definition and every concept's value for it, in one transaction.
Path parameters
Name
Type
Description
study_id
uuid
The study the field belongs to.
field_id
uuid
The field to delete.
Response
`200 OK`, not `204` — the deleted field as it last stood, plus the question text left pointing at its token.
Field
Type
Description
data.id
uuid
Concept field identifier.
data.section_id
uuid
Concept-testing section the field is defined on.
data.name
string
Field name, e.g. "Price".
data.slug
string
Slugified field name; the `{slug}` token you reference in the section's question text.
data.field_type
enum
`SINGLE_LINE` or `MULTI_LINE`.
data.position
integer
Zero-indexed position of the field among the section's fields.
data.questions_referencing_slug
array[object]
Questions in this section whose text still embeds the deleted `{slug}` token, each with `id` and `text`; they render an unknown value until you edit them.
Irreversible, and it takes every per-concept value with it. Question text referencing the token is left untouched and renders as an unknown value until you edit it.
Sets one concept's value for one field — Variant A's price, say. The value substitutes into that field's {slug} token wherever the section's question text uses it.
Path parameters
Name
Type
Description
study_id
uuid
The study the concept belongs to.
concept_id
uuid
The concept to set the value on.
field_id
uuid
The field whose value you are setting; it must belong to the concept's section.
Request body
The value for this concept-and-field pair.
Field
Type
Description
valuerequired
string
The value participants see in place of the token; send an empty string to clear it.
{
"value": "$12.99 / month"
}
Response
The stored value for this concept-and-field pair.
Field
Type
Description
data.id
uuid
Identifier of the stored value row.
data.concept_id
uuid
The concept the value belongs to.
data.field_id
uuid
The field the value belongs to.
data.slug
string
The field's `{slug}` token this value substitutes into.
data.value
string
The stored value, as participants will see it.
data.cleared
boolean
True when an empty value was sent, so the token now renders as an unknown value.
Idempotent — the value row is created on first write and replaced afterwards. The stimulus field is set through the concept stimulus endpoint, not here.
A study's screener is the qualification gate a participant passes before the interview starts; its consent sections are the permissions block that runs alongside it. The screener is a singleton on the study — create it once, then manage its questions, their per-option qualification statuses, and three distinct kinds of logic. Disqualification conditions screen a participant out and live on the screener itself, so one condition may span several questions (rules AND together inside a condition, conditions OR together). Skip conditions route a participant to a later question or qualify them on the spot once the owning question is answered. Display conditions decide whether a question is rendered at all — and their polarity belongs to the owning question, not to the condition: display_condition_mode is either SKIP_IF (hide the question when any condition matches) or SHOW_IF (show it only when at least one matches) for that question's entire condition group, so a single question can never mix skip-if and show-if logic. Everything here reads with studies:read and writes with studies:write; content writes are refused while the study is published — a 409 published_study — and consent writes are refused additionally while an active third-party recruitment locks the study for editing.
Creates the study's screener with its participant-facing copy. A study has at most one screener, so this call is a one-off — change copy afterwards with the PATCH on the same path, and clear questions with the reset action.
Path parameters
Name
Type
Description
study_id
uuid
The study the screener belongs to.
Request body
Screener copy. Every field is optional; omitted fields start empty.
Field
Type
Description
name
string
Internal display name for the screener.
introduction
string
Introductory message shown to participants before the first screener question.
rejection_message
string
Message shown to participants who are screened out.
{
"name": "Coffee drinkers screener",
"introduction": "A few quick questions before we start.",
"rejection_message": "Thanks for your interest — you are not a match for this study."
}
Response
The created screener, with an empty question list.
Field
Type
Description
data.id
uuid
Screener identifier.
data.name
string
Internal display name for the screener.
data.introduction
string
Introductory message shown before the first screener question.
data.rejection_message
string
Message shown to participants who are screened out.
data.screened_out_url
string
URL screened-out participants are redirected to, or null when they stay on the rejection message.
data.questions
array
Screener questions in participant order; empty on a freshly created screener.
{
"data": {
"id": "6b1f0c2e-9d84-4a17-b0d3-2f5e7c418a90",
"name": "Coffee drinkers screener",
"introduction": "A few quick questions before we start.",
"rejection_message": "Thanks for your interest — you are not a match for this study.",
"screened_out_url": null,
"questions": []
}
}
Errors
Status
Code
When
409
conflict
The study already has a screener — edit it in place instead.
Returns the whole screener: copy, questions in participant order with their options, the display and skip conditions attached to each question, and the screener-level disqualification conditions. This is the read you page through before editing anything, because every write below addresses questions, options, and conditions by the ids returned here.
Path parameters
Name
Type
Description
study_id
uuid
The study whose screener to read.
Response
The screener and its full question and condition graph.
Field
Type
Description
data.id
uuid
Screener identifier.
data.name
string
Internal display name for the screener.
data.introduction
string
Introductory message shown before the first screener question.
data.rejection_message
string
Message shown to participants who are screened out.
data.screened_out_url
string
URL screened-out participants are redirected to, or null when they stay on the rejection message.
data.questions[].id
uuid
Screener question identifier.
data.questions[].order
integer
Zero-based position of the question in the screener.
data.questions[].text
string
The question as the participant sees it.
data.questions[].question_type
enum
One of SELECT_ONE, SELECT_MULTIPLE, TEXT, or IMAGE_UPLOAD.
data.questions[].optional
boolean
Whether the participant may skip the question without answering.
data.questions[].preserve_options_order
boolean
Whether options are shown in the defined order rather than randomized.
data.questions[].min_selections
integer
Minimum options a participant must pick on a SELECT_MULTIPLE question, or null for no minimum.
data.questions[].max_selections
integer
Maximum options a participant may pick on a SELECT_MULTIPLE question, or null for no maximum.
data.questions[].allow_custom_other_text
boolean
Whether the OTHER-anchored option shows a free-text input alongside it.
data.questions[].text_is_email
boolean
Whether a TEXT answer must be a valid email address.
data.questions[].vision_criterion
string
Criterion an IMAGE_UPLOAD answer is evaluated against, or null on other question types.
data.questions[].qualify_immediately
boolean
Whether answering this question qualifies the participant and skips the rest of the screener.
data.questions[].disqualify_immediately
boolean
Whether this question's disqualification conditions are evaluated as soon as it is answered.
data.questions[].display_condition_mode
enum
SKIP_IF or SHOW_IF — how every display condition on this question is interpreted.
data.questions[].options[].id
uuid
Option identifier, used in condition rules.
data.questions[].options[].text
string
Option text as the participant sees it.
data.questions[].options[].order
integer
Zero-based position of the option within the question.
data.questions[].options[].anchor_type
enum
NONE for an exclusive "none of the above" anchor, OTHER for a write-in anchor, or null for a regular option.
data.questions[].display_conditions[]
array
Display conditions on this question, each with an id and its AND-combined rules; the conditions OR together.
data.questions[].skip_conditions[]
array
Routing conditions on this question, each with an id, its rules, and either target_question_id or qualify_immediately.
data.disqualification_conditions[]
array
Screener-level disqualification conditions, each with an id, disqualification_timing, and rules that may reference several questions. Every rule is `{screener_question_id, option_ids[], operator}`.
{
"data": {
"id": "6b1f0c2e-9d84-4a17-b0d3-2f5e7c418a90",
"name": "Coffee drinkers screener",
"introduction": "A few quick questions before we start.",
"rejection_message": "Thanks for your interest — you are not a match for this study.",
"screened_out_url": null,
"questions": [
{
"id": "a7c39d51-4e08-4b62-9f13-0d5a6c827e44",
"order": 0,
"text": "How often do you drink coffee?",
"question_type": "SELECT_ONE",
"disqualify_immediately": true,
"display_condition_mode": "SKIP_IF",
"options": [
{
"id": "3f8a1b40-77c2-4d95-8e6b-19a0c4f27d13",
"text": "Daily",
"order": 0,
"anchor_type": null
},
"… one entry per option"
],
"…": "remaining question fields"
},
"… one entry per screener question"
],
"disqualification_conditions": [
{
"id": "e91c4d76-2a58-4f30-b7e9-6c0d38a15b27",
"disqualification_timing": "IMMEDIATE",
"rules": [
"… one {screener_question_id, option_ids, operator} entry per rule; they AND together"
]
}
]
}
}
Errors
Status
Code
When
404
not_found
The study is outside the credential's reach, or it has no screener yet.
Updates the screener's participant-facing copy and screened-out redirect. Only the fields you send change; questions are never touched.
Path parameters
Name
Type
Description
study_id
uuid
The study whose screener to update.
Request body
Any subset of the screener's copy fields.
Field
Type
Description
name
string
Internal display name for the screener.
introduction
string
Introductory message shown to participants before the first screener question.
rejection_message
string
Message shown to participants who are screened out.
screened_out_url
string
URL screened-out participants are redirected to; must use http:// or https://, and an empty string clears it.
Response
The updated screener, in the shape the read endpoint returns.
Field
Type
Description
data.id
uuid
Screener identifier.
data.name
string
Internal display name for the screener.
data.introduction
string
Introductory message shown before the first screener question.
data.rejection_message
string
Message shown to participants who are screened out.
data.screened_out_url
string
URL screened-out participants are redirected to, or null when they stay on the rejection message.
data.questions[].id
uuid
Screener question identifier.
data.questions[].order
integer
Zero-based position of the question in the screener.
data.questions[].text
string
The question as the participant sees it.
data.questions[].question_type
enum
One of SELECT_ONE, SELECT_MULTIPLE, TEXT, or IMAGE_UPLOAD.
data.questions[].optional
boolean
Whether the participant may skip the question without answering.
data.questions[].preserve_options_order
boolean
Whether options are shown in the defined order rather than randomized.
data.questions[].min_selections
integer
Minimum options a participant must pick on a SELECT_MULTIPLE question, or null for no minimum.
data.questions[].max_selections
integer
Maximum options a participant may pick on a SELECT_MULTIPLE question, or null for no maximum.
data.questions[].allow_custom_other_text
boolean
Whether the OTHER-anchored option shows a free-text input alongside it.
data.questions[].text_is_email
boolean
Whether a TEXT answer must be a valid email address.
data.questions[].vision_criterion
string
Criterion an IMAGE_UPLOAD answer is evaluated against, or null on other question types.
data.questions[].qualify_immediately
boolean
Whether answering this question qualifies the participant and skips the rest of the screener.
data.questions[].disqualify_immediately
boolean
Whether this question's disqualification conditions are evaluated as soon as it is answered.
data.questions[].display_condition_mode
enum
SKIP_IF or SHOW_IF — how every display condition on this question is interpreted.
data.questions[].options[].id
uuid
Option identifier, used in condition rules.
data.questions[].options[].text
string
Option text as the participant sees it.
data.questions[].options[].order
integer
Zero-based position of the option within the question.
data.questions[].options[].anchor_type
enum
NONE for an exclusive "none of the above" anchor, OTHER for a write-in anchor, or null for a regular option.
data.questions[].display_conditions[]
array
Display conditions on this question, each with an id and its AND-combined rules; the conditions OR together.
data.questions[].skip_conditions[]
array
Routing conditions on this question, each with an id, its rules, and either target_question_id or qualify_immediately.
data.disqualification_conditions[]
array
Screener-level disqualification conditions, each with an id, disqualification_timing, and rules that may reference several questions. Every rule is `{screener_question_id, option_ids[], operator}`.
{
"data": {
"id": "6b1f0c2e-9d84-4a17-b0d3-2f5e7c418a90",
"name": "Coffee drinkers screener",
"introduction": "A few quick questions before we start.",
"rejection_message": "Thanks for your interest — you are not a match for this study.",
"screened_out_url": null,
"questions": [
{
"id": "a7c39d51-4e08-4b62-9f13-0d5a6c827e44",
"order": 0,
"text": "How often do you drink coffee?",
"question_type": "SELECT_ONE",
"disqualify_immediately": true,
"display_condition_mode": "SKIP_IF",
"options": [
{
"id": "3f8a1b40-77c2-4d95-8e6b-19a0c4f27d13",
"text": "Daily",
"order": 0,
"anchor_type": null
},
"… one entry per option"
],
"…": "remaining question fields"
},
"… one entry per screener question"
],
"disqualification_conditions": [
{
"id": "e91c4d76-2a58-4f30-b7e9-6c0d38a15b27",
"disqualification_timing": "IMMEDIATE",
"rules": [
"… one {screener_question_id, option_ids, operator} entry per rule; they AND together"
]
}
]
}
}
Deletes every question on the screener, along with their options and all display, skip, and disqualification conditions. The screener itself and its copy survive.
Path parameters
Name
Type
Description
study_id
uuid
The study whose screener to empty.
Response
How much was removed.
Field
Type
Description
data.id
uuid
Screener identifier.
data.removed_question_count
integer
Number of screener questions deleted by this call.
Irreversible over the API, and intended only for an explicit from-scratch rebuild — there is no undo endpoint. Prefer deleting individual questions when you only need to change part of the screener.
Rebuilds the study's screener from a saved template snapshot — questions, options, qualification logic, and intro and rejection copy — replacing whatever the screener holds today.
Path parameters
Name
Type
Description
study_id
uuid
The study to apply the template to.
Request body
The template to apply.
Field
Type
Description
template_idrequired
uuid
Screener template to rebuild the screener from; must be one the credential can reach.
The study is published. Unpublish it, apply the template, then publish again.
409
conflict
The screener already holds participant answers that a rebuild would orphan.
Destructive and not idempotent: every existing screener question and condition is discarded and recreated with fresh ids, so a retry after a timeout is not a no-op — read the screener back before re-sending.
Appends a question to the end of the screener and, optionally, creates its qualification logic in the same call. Single-question logic is expressed with the option-text lists below; logic spanning several questions needs a disqualification condition instead.
Path parameters
Name
Type
Description
study_id
uuid
The study whose screener gains the question.
Request body
The question, its options, and its qualification logic.
Field
Type
Description
textrequired
string
The question as the participant will see it.
question_typerequired
enum
SELECT_ONE for single choice, SELECT_MULTIPLE for multi-select, TEXT for a free-text answer, or IMAGE_UPLOAD for an image judged by a vision model.
options
array
Answer options, required for SELECT_ONE and SELECT_MULTIPLE and forbidden for IMAGE_UPLOAD; each entry is a string, or an object with text plus anchor_type NONE (exclusive "none of the above") or OTHER (write-in), at most one of each per question.
optional
boolean
Whether the participant may skip the question without answering; defaults to false.
preserve_options_order
boolean
Whether options are shown in the order supplied rather than randomized; defaults to true.
vision_criterion
string
Required for IMAGE_UPLOAD: the single positive criterion the vision model evaluates the uploaded image against.
disqualifying_options
array
Option texts the participant must NOT pick — selecting any of them disqualifies; matched against the option list case-insensitively.
required_options
array
Option texts the participant must pick at least one of, otherwise they are disqualified.
must_select_options
array
SELECT_MULTIPLE only: option texts the participant must EACH select; may not cover every option of the question and may not include a NONE anchor.
min_selections
integer
SELECT_MULTIPLE only: fewest options the participant may pick.
max_selections
integer
SELECT_MULTIPLE only: most options the participant may pick.
allow_custom_other_text
boolean
Whether an OTHER-anchored option shows a free-text input alongside it; ignored without that anchor.
text_is_email
boolean
TEXT only: require the answer to be a valid email address, rejecting invalid submissions so the participant can retry.
disqualification_timing
enum
IMMEDIATE (default) screens the participant out as soon as this question is answered; DEFERRED waits until the whole screener is finished.
qualify_immediately
boolean
Qualify the participant as soon as they answer this question, bypassing the remaining questions; mutually exclusive with disqualify_immediately and unavailable on IMAGE_UPLOAD.
disqualify_immediately
boolean
Evaluate this question's disqualification conditions the moment it is answered; requires the same call to create at least one, and is mutually exclusive with qualify_immediately.
display_condition_mode
enum
SKIP_IF (default) hides the question when any display condition matches; SHOW_IF shows it only when at least one matches.
{
"text": "How often do you drink coffee?",
"question_type": "SELECT_ONE",
"options": [
"Daily",
"A few times a week",
"Never"
],
"disqualifying_options": [
"Never"
],
"disqualification_timing": "IMMEDIATE",
"disqualify_immediately": true
}
Response
The created question, in the shape the read endpoint returns.
Field
Type
Description
data.id
uuid
Screener question identifier.
data.order
integer
Zero-based position of the question, always last on creation.
data.text
string
The question as the participant sees it.
data.question_type
enum
One of SELECT_ONE, SELECT_MULTIPLE, TEXT, or IMAGE_UPLOAD.
data.options[].id
uuid
Option identifier — the value condition rules reference.
data.options[].text
string
Option text as the participant sees it.
data.options[].order
integer
Zero-based position of the option within the question.
{
"data": {
"id": "a7c39d51-4e08-4b62-9f13-0d5a6c827e44",
"order": 0,
"text": "How often do you drink coffee?",
"question_type": "SELECT_ONE",
"options": [
{
"id": "3f8a1b40-77c2-4d95-8e6b-19a0c4f27d13",
"text": "Daily",
"order": 0
},
{
"id": "9e14a7c6-58d3-4b02-81fa-6c7d29e35b40",
"text": "A few times a week",
"order": 1
},
{
"id": "c5d20e83-16b4-49af-92c7-8b3e51d04f6a",
"text": "Never",
"order": 2
}
]
}
}
Errors
Status
Code
When
400
validation_error
An option text in a disqualification list matches no option, DEFERRED timing is combined with disqualify_immediately, or a SELECT_MULTIPLE-only field is sent on another question type.
403
feature_not_enabled
IMAGE_UPLOAD questions are not enabled for the organization.
IMAGE_UPLOAD is a gated question type — it is enabled per organization, and existing questions keep working even where new ones cannot be created.
Changes a screener question's wording, type, flags, options, or single-question qualification logic. Accepts every field of the create endpoint; only what you send changes.
Path parameters
Name
Type
Description
study_id
uuid
The study whose screener holds the question.
question_id
uuid
The screener question to update.
Request body
Any subset of the create endpoint's fields.
Field
Type
Description
options
array
Replaces the complete option list; options whose text matches an existing one case-insensitively keep their id, and any others are recreated.
disqualifying_options
array
Replaces every disqualification condition that references this question with a "must NOT pick" rule over these option texts.
required_options
array
Replaces every disqualification condition that references this question with a "must pick at least one of" rule over these option texts.
must_select_options
array
SELECT_MULTIPLE only: replaces this question's disqualification logic with a rule requiring each listed option.
display_condition_mode
enum
Flip the whole question's display logic between SKIP_IF and SHOW_IF.
Response
The updated question, in the shape the read endpoint returns.
Field
Type
Description
data.id
uuid
Screener question identifier.
data.order
integer
Zero-based position of the question in the screener.
data.text
string
The question as the participant sees it.
data.question_type
enum
One of SELECT_ONE, SELECT_MULTIPLE, TEXT, or IMAGE_UPLOAD.
data.optional
boolean
Whether the participant may skip the question without answering.
data.preserve_options_order
boolean
Whether options are shown in the defined order rather than randomized.
data.min_selections
integer
Minimum options a participant must pick on a SELECT_MULTIPLE question, or null for no minimum.
data.max_selections
integer
Maximum options a participant may pick on a SELECT_MULTIPLE question, or null for no maximum.
data.allow_custom_other_text
boolean
Whether the OTHER-anchored option shows a free-text input alongside it.
data.text_is_email
boolean
Whether a TEXT answer must be a valid email address.
data.vision_criterion
string
Criterion an IMAGE_UPLOAD answer is evaluated against, or null on other question types.
data.qualify_immediately
boolean
Whether answering this question qualifies the participant and skips the rest of the screener.
data.disqualify_immediately
boolean
Whether this question's disqualification conditions are evaluated as soon as it is answered.
data.display_condition_mode
enum
SKIP_IF or SHOW_IF — how every display condition on this question is interpreted.
data.options[].id
uuid
Option identifier, used in condition rules.
data.options[].text
string
Option text as the participant sees it.
data.options[].order
integer
Zero-based position of the option within the question.
data.options[].anchor_type
enum
NONE for an exclusive "none of the above" anchor, OTHER for a write-in anchor, or null for a regular option.
data.display_conditions[]
array
Display conditions on this question, each with an id and its AND-combined rules; the conditions OR together.
data.skip_conditions[]
array
Routing conditions on this question, each with an id, its rules, and either target_question_id or qualify_immediately.
{
"data": {
"id": "a7c39d51-4e08-4b62-9f13-0d5a6c827e44",
"order": 0,
"text": "How often do you drink coffee?",
"question_type": "SELECT_ONE",
"optional": false,
"preserve_options_order": true,
"qualify_immediately": false,
"disqualify_immediately": true,
"display_condition_mode": "SKIP_IF",
"options": [
{
"id": "3f8a1b40-77c2-4d95-8e6b-19a0c4f27d13",
"text": "Daily",
"order": 0,
"anchor_type": null
},
"… one entry per option"
],
"display_conditions": [],
"skip_conditions": [
"… one {id, rules, target_question_id, qualify_immediately} entry per condition"
],
"…": "remaining question fields"
}
}
Sending any of the three disqualification lists replaces all conditions referencing this question — including cross-question disqualification conditions created separately. Re-create those afterwards if you still need them.
Replaces a question's qualification logic with per-option statuses, the shape the study editor's per-option dropdown produces. Use it when the question needs several individually-required options, a genuine "at least one of" group, or per-option immediate screen-out — none of which the flat lists on the question endpoints can express.
Path parameters
Name
Type
Description
study_id
uuid
The study whose screener holds the question.
question_id
uuid
The screener question whose option statuses to replace.
Request body
The complete set of statuses. Options named in no bucket carry no rule, and all-empty buckets clear the question's logic.
Field
Type
Description
disqualify_options
array
Option texts that disqualify the participant when selected; matched case-insensitively.
must_select_options
array
SELECT_MULTIPLE only: option texts the participant must EACH select, otherwise they are disqualified; may not cover every option and may not include a NONE anchor.
must_select_one_of_options
array
SELECT_MULTIPLE only: one group of option texts the participant must select at least one of.
disqualify_immediately_options
array
Subset of the option texts above whose condition ends the screener the moment it matches; the must-select-one-of group shares a condition, so list either all of its options or none.
The updated question, in the shape the read endpoint returns. The statuses themselves are stored as screener-level disqualification conditions, so read the screener back to see them.
Field
Type
Description
data.id
uuid
Screener question identifier.
data.order
integer
Zero-based position of the question in the screener.
data.text
string
The question as the participant sees it.
data.question_type
enum
One of SELECT_ONE, SELECT_MULTIPLE, TEXT, or IMAGE_UPLOAD.
data.optional
boolean
Whether the participant may skip the question without answering.
data.preserve_options_order
boolean
Whether options are shown in the defined order rather than randomized.
data.min_selections
integer
Minimum options a participant must pick on a SELECT_MULTIPLE question, or null for no minimum.
data.max_selections
integer
Maximum options a participant may pick on a SELECT_MULTIPLE question, or null for no maximum.
data.allow_custom_other_text
boolean
Whether the OTHER-anchored option shows a free-text input alongside it.
data.text_is_email
boolean
Whether a TEXT answer must be a valid email address.
data.vision_criterion
string
Criterion an IMAGE_UPLOAD answer is evaluated against, or null on other question types.
data.qualify_immediately
boolean
Whether answering this question qualifies the participant and skips the rest of the screener.
data.disqualify_immediately
boolean
Whether this question's disqualification conditions are evaluated as soon as it is answered.
data.display_condition_mode
enum
SKIP_IF or SHOW_IF — how every display condition on this question is interpreted.
data.options[].id
uuid
Option identifier, used in condition rules.
data.options[].text
string
Option text as the participant sees it.
data.options[].order
integer
Zero-based position of the option within the question.
data.options[].anchor_type
enum
NONE for an exclusive "none of the above" anchor, OTHER for a write-in anchor, or null for a regular option.
data.display_conditions[]
array
Display conditions on this question, each with an id and its AND-combined rules; the conditions OR together.
data.skip_conditions[]
array
Routing conditions on this question, each with an id, its rules, and either target_question_id or qualify_immediately.
{
"data": {
"id": "a7c39d51-4e08-4b62-9f13-0d5a6c827e44",
"order": 0,
"text": "How often do you drink coffee?",
"question_type": "SELECT_ONE",
"optional": false,
"preserve_options_order": true,
"qualify_immediately": false,
"disqualify_immediately": true,
"display_condition_mode": "SKIP_IF",
"options": [
{
"id": "3f8a1b40-77c2-4d95-8e6b-19a0c4f27d13",
"text": "Daily",
"order": 0,
"anchor_type": null
},
"… one entry per option"
],
"display_conditions": [],
"skip_conditions": [
"… one {id, rules, target_question_id, qualify_immediately} entry per condition"
],
"…": "remaining question fields"
}
}
Errors
Status
Code
When
409
conflict
The question's existing disqualification logic spans other questions — edit those conditions through the disqualification-condition endpoints instead.
A full replacement, not a merge: send every status you want to keep on the question.
Rules elsewhere that read this question's answer are deleted so routing stays consistent; skip conditions that merely jump to it survive, with their target cleared. Read the screener back after a delete before addressing conditions by id.
Copies a screener question with its options, disqualification logic, and display and skip conditions. The copy lands immediately after the original, and rules that referenced the original are rewired to the copy.
Path parameters
Name
Type
Description
study_id
uuid
The study whose screener holds the question.
question_id
uuid
The screener question to copy.
Response
The new question, in the shape the read endpoint returns. Its id, option ids, and condition ids are all fresh.
Field
Type
Description
data.id
uuid
Screener question identifier.
data.order
integer
Zero-based position of the question in the screener.
data.text
string
The question as the participant sees it.
data.question_type
enum
One of SELECT_ONE, SELECT_MULTIPLE, TEXT, or IMAGE_UPLOAD.
data.optional
boolean
Whether the participant may skip the question without answering.
data.preserve_options_order
boolean
Whether options are shown in the defined order rather than randomized.
data.min_selections
integer
Minimum options a participant must pick on a SELECT_MULTIPLE question, or null for no minimum.
data.max_selections
integer
Maximum options a participant may pick on a SELECT_MULTIPLE question, or null for no maximum.
data.allow_custom_other_text
boolean
Whether the OTHER-anchored option shows a free-text input alongside it.
data.text_is_email
boolean
Whether a TEXT answer must be a valid email address.
data.vision_criterion
string
Criterion an IMAGE_UPLOAD answer is evaluated against, or null on other question types.
data.qualify_immediately
boolean
Whether answering this question qualifies the participant and skips the rest of the screener.
data.disqualify_immediately
boolean
Whether this question's disqualification conditions are evaluated as soon as it is answered.
data.display_condition_mode
enum
SKIP_IF or SHOW_IF — how every display condition on this question is interpreted.
data.options[].id
uuid
Option identifier, used in condition rules.
data.options[].text
string
Option text as the participant sees it.
data.options[].order
integer
Zero-based position of the option within the question.
data.options[].anchor_type
enum
NONE for an exclusive "none of the above" anchor, OTHER for a write-in anchor, or null for a regular option.
data.display_conditions[]
array
Display conditions on this question, each with an id and its AND-combined rules; the conditions OR together.
data.skip_conditions[]
array
Routing conditions on this question, each with an id, its rules, and either target_question_id or qualify_immediately.
{
"data": {
"id": "b8e4f27a-3c60-4d91-85fb-72a1e9d0c463",
"order": 1,
"text": "How often do you drink coffee?",
"question_type": "SELECT_ONE",
"options": [
{
"id": "d02a5f96-8e41-4b73-9c05-3f8a61d2e074",
"text": "Daily",
"order": 0,
"anchor_type": null
},
"… one entry per option"
],
"…": "remaining question fields"
}
}
The list omits a screener question, repeats one, or names an id that is not on this screener.
Display conditions may only read questions that come earlier in the screener, so a reorder that moves a source question after its dependent question is rejected.
Adds one display condition to a screener question. Rules inside a condition AND together; separate conditions on the same question OR together, so call this repeatedly to express OR.
Path parameters
Name
Type
Description
study_id
uuid
The study whose screener holds the question.
question_id
uuid
The screener question whose visibility this condition gates.
Request body
The condition's rules.
Field
Type
Description
rulesrequired
array
At least one rule; all of them must match for the condition to match.
rules[].source_typerequired
enum
screener to evaluate another screener question's answer, or url_variable to evaluate a variable passed in the participant's link.
rules[].source_screener_question_id
uuid
Required when source_type is screener: a SELECT_ONE or SELECT_MULTIPLE question earlier in the screener, since display logic runs before this question is asked.
rules[].value_option_id
uuid
Required when source_type is screener: the option on the source question to match.
rules[].url_variable
string
Required when source_type is url_variable: the variable name to read.
rules[].value
string
Required when source_type is url_variable: the value to compare against.
rules[].operatorrequired
enum
equals or not_equals for an exact match, contains or not_contains for membership in a multi-select answer.
The polarity lives on the question, not here: whether a match hides or shows the question is decided by its display_condition_mode, so every display condition on one question shares one mode and skip-if and show-if can never be mixed on the same parent.
Removes one display condition from a screener question. To clear a question's display logic entirely, delete each condition id the screener read returns.
Path parameters
Name
Type
Description
study_id
uuid
The study whose screener holds the question.
question_id
uuid
The screener question the condition belongs to.
condition_id
uuid
The display condition to delete.
Response
204 — empty body. The condition is gone; the question's other display conditions keep their ids and their shared display_condition_mode.
Errors
Status
Code
When
404
not_found
The condition does not exist on that question, or the study is outside the credential's reach.
Adds one routing condition to a screener question. When every rule matches, the screener either jumps to another question or qualifies the participant on the spot.
Path parameters
Name
Type
Description
study_id
uuid
The study whose screener holds the question.
question_id
uuid
The screener question this routing lives on; the rules fire once the participant submits it.
Request body
The routing target and the rules that trigger it.
Field
Type
Description
target_question_id
uuid
Screener question to jump to when the rules match; mutually exclusive with qualify_immediately.
qualify_immediately
boolean
Qualify the participant the moment the rules match, bypassing the remaining questions; mutually exclusive with target_question_id.
rulesrequired
array
At least one rule; all of them must match for the routing to fire.
rules[].source_screener_question_id
uuid
Screener question whose answer this rule reads; defaults to the owning question and may name any earlier one, never a later one.
rules[].value_option_idrequired
uuid
Option on the rule's source question to match.
rules[].operatorrequired
enum
equals or not_equals for SELECT_ONE answers, contains or not_contains for SELECT_MULTIPLE answers.
Routing respects display logic: a question hidden by its display conditions never fires its skip conditions. Conditions on the same question OR together — call this repeatedly for alternative routes.
Adds a screen-out condition to the screener whose rules may span several questions — the shape the per-question option lists cannot express, such as "disqualify when Q1 is Yes AND Q2 is EU". Rules AND together inside the condition; separate conditions OR together.
Path parameters
Name
Type
Description
study_id
uuid
The study whose screener gains the condition.
Request body
The rules and when they take effect.
Field
Type
Description
rulesrequired
array
At least one rule; the participant is screened out only when all of them match.
rules[].screener_question_idrequired
uuid
SELECT_ONE or SELECT_MULTIPLE question on this screener whose answer the rule reads.
rules[].option_idsrequired
array
Options on that question to compare the participant's answer against.
rules[].operatorrequired
enum
includes matches when the participant selected any of the options; not_includes matches when they selected none of them.
disqualification_timing
enum
DEFERRED (default) screens the participant out at the end of the screener; IMMEDIATE does it as soon as every referenced question has been answered.
The condition's rules as stored, in the shape they were sent.
{
"data": {
"id": "e91c4d76-2a58-4f30-b7e9-6c0d38a15b27",
"disqualification_timing": "DEFERRED",
"rules": [
{
"screener_question_id": "a7c39d51-4e08-4b62-9f13-0d5a6c827e44",
"option_ids": [
"c5d20e83-16b4-49af-92c7-8b3e51d04f6a"
],
"operator": "includes"
},
"… one entry per rule; they AND together"
]
}
}
DEFERRED is the only timing every recruitment provider supports for a condition spanning several questions. Two ordering traps: a participant who qualifies at an earlier question never answers the later ones, so a condition referencing them is bypassed; and updating any of the referenced questions with a disqualification list replaces this condition.
Snapshots a study's current screener — questions, options, qualification, routing and display logic, and intro and rejection copy — into a named, reusable template.
Request body
The source study, the template name, and its visibility.
Field
Type
Description
study_idrequired
uuid
Study whose screener to snapshot; it must already have one.
namerequired
string
Display name for the new template.
workspace_ids
array
Workspaces the template is visible in; omit to scope it to the study's own workspace, or send an empty list to share it org-wide.
A snapshot, not a link: later edits to the source study's screener do not flow into the template. Org-wide sharing may be restricted to organization admins.
Returns the study's consent sections in participant order, each with its questions and options. Returned whole — the set is bounded by the study's consent sections, not paginated. Use it to find the section and question ids the write endpoints address, and to see which sections are org-managed and therefore read-only.
Path parameters
Name
Type
Description
study_id
uuid
The study whose consent sections to read.
Response
The study's consent sections, ordered.
Field
Type
Description
data[].id
uuid
Consent section identifier.
data[].name
string
Display name of the section.
data[].order
integer
Zero-based position among the study's consent sections.
data[].source_template_id
uuid
Consent template the section was applied from, or null for a manually authored section.
data[].is_org_managed
boolean
Whether the section came from a template that ships its own questions, making it read-only on this study.
data[].questions[].id
uuid
Consent question identifier.
data[].questions[].order
integer
Zero-based position within the section.
data[].questions[].text
string
The consent question as the participant sees it.
data[].questions[].question_type
enum
One of SELECT_ONE, SELECT_MULTIPLE, or TEXT.
data[].questions[].optional
boolean
Whether the participant may skip the question.
data[].questions[].text_is_email
boolean
Whether a TEXT answer must be a valid email address.
data[].questions[].options[].id
uuid
Option identifier.
data[].questions[].options[].text
string
Option text as the participant sees it.
data[].questions[].options[].disqualifies
boolean
Whether selecting this option fails consent and screens the participant out.
{
"data": [
{
"id": "5c7e2b91-08fa-4d63-9e14-b7a3d0c528f6",
"name": "Recording consent",
"order": 0,
"source_template_id": null,
"is_org_managed": false,
"questions": [
{
"id": "f10b7d43-6e29-4c85-a0d7-3b91c46e2857",
"order": 0,
"text": "Do you consent to being recorded?",
"question_type": "SELECT_ONE",
"optional": false,
"text_is_email": false,
"options": [
{
"id": "24a9e0f7-5b13-4d68-92ca-0e7f8b3d1465",
"text": "Yes, I consent",
"disqualifies": false
},
{
"id": "8d3f61c0-72a4-4e19-b5d8-6f0a2c94e731",
"text": "No, I do not consent",
"disqualifies": true
}
]
}
]
}
]
}
Adds a consent section to the end of the study's consent block, either empty for you to author, or from one of the organization's consent templates. Send exactly one of name or template_id.
Path parameters
Name
Type
Description
study_id
uuid
The study that gains the consent section.
Request body
Either a name for a manual section, or the template to apply.
Field
Type
Description
name
string
Display name for a manually authored section, which starts with no questions.
template_id
uuid
Organization consent template to apply as a new section; each template may be applied to a study only once.
The template is already on this study, or an active third-party recruitment has locked the study for editing.
404
not_found
The template is not available to this study's workspace — unavailable and non-existent templates are indistinguishable on purpose.
A template that ships its own questions produces an org-managed section: its questions cannot be added to, edited, moved, or deleted on the study. Applying a disclaimer-only template gives you a section whose questions you author yourself.
The section is the only one satisfying the organization's required-consent policy for a recruitment path the study uses.
Organizations can require an approved consent block for a given recruitment path. Where that policy applies, add or swap in the replacement section before deleting the current one.
Replaces a consent section's contents with an organization consent template, in place and keeping its position. Use it to move to a different approved template, or to convert a manually authored section into a template-backed one.
Path parameters
Name
Type
Description
study_id
uuid
The study the section belongs to.
consent_section_id
uuid
The consent section whose contents to replace.
Request body
The template to swap in.
Field
Type
Description
template_idrequired
uuid
Organization consent template to replace the section's contents with.
The rewritten section, in the shape the list endpoint returns — same id and position, carrying the template's name and questions.
Field
Type
Description
data.id
uuid
Consent section identifier.
data.name
string
Display name of the section.
data.order
integer
Zero-based position among the study's consent sections.
data.source_template_id
uuid
Consent template the section was applied from, or null for a manually authored section.
data.is_org_managed
boolean
Whether the section came from a template that ships its own questions, making it read-only on this study.
data.questions[].id
uuid
Consent question identifier.
data.questions[].order
integer
Zero-based position within the section.
data.questions[].text
string
The consent question as the participant sees it.
data.questions[].question_type
enum
One of SELECT_ONE, SELECT_MULTIPLE, or TEXT.
data.questions[].optional
boolean
Whether the participant may skip the question.
data.questions[].text_is_email
boolean
Whether a TEXT answer must be a valid email address.
data.questions[].options[].id
uuid
Option identifier.
data.questions[].options[].text
string
Option text as the participant sees it.
data.questions[].options[].disqualifies
boolean
Whether selecting this option fails consent and screens the participant out.
{
"data": {
"id": "5c7e2b91-08fa-4d63-9e14-b7a3d0c528f6",
"name": "GDPR data processing consent",
"order": 0,
"source_template_id": "9a2c60d5-3f81-4b7e-8c04-e5d17b9a3f62",
"is_org_managed": true,
"questions": [
"… one entry per question the template installed"
]
}
}
Destructive: the section's existing questions are discarded. Swapping does not by itself satisfy an organization's required-consent policy — publishing still checks that an approved template is in place where the policy applies.
Appends a question to a consent section. Mark the refusal option with disqualifies so a participant who picks it fails consent.
Path parameters
Name
Type
Description
study_id
uuid
The study the section belongs to.
consent_section_id
uuid
The consent section that gains the question.
Request body
The consent question and its options.
Field
Type
Description
textrequired
string
The consent question as the participant will see it.
question_type
enum
SELECT_ONE (default), SELECT_MULTIPLE, or TEXT for a free-text answer.
options
array
Answer options, required for SELECT_ONE and SELECT_MULTIPLE and forbidden for TEXT; each entry is a string, or an object with text and disqualifies.
optional
boolean
Whether the participant may skip the question; defaults to false.
preserve_options_order
boolean
Whether options are shown in the order supplied rather than randomized; defaults to true.
text_is_email
boolean
TEXT only: require the answer to be a valid email address.
{
"text": "Do you consent to being recorded?",
"question_type": "SELECT_ONE",
"options": [
{
"text": "Yes, I consent",
"disqualifies": false
},
{
"text": "No, I do not consent",
"disqualifies": true
}
]
}
Response
The created consent question, in the shape the list endpoint returns.
Field
Type
Description
data.id
uuid
Consent question identifier.
data.order
integer
Zero-based position within the section.
data.text
string
The consent question as the participant sees it.
data.question_type
enum
One of SELECT_ONE, SELECT_MULTIPLE, or TEXT.
data.optional
boolean
Whether the participant may skip the question.
data.text_is_email
boolean
Whether a TEXT answer must be a valid email address.
data.options[].id
uuid
Option identifier.
data.options[].text
string
Option text as the participant sees it.
data.options[].disqualifies
boolean
Whether selecting this option fails consent and screens the participant out.
{
"data": {
"id": "f10b7d43-6e29-4c85-a0d7-3b91c46e2857",
"order": 0,
"text": "Do you consent to being recorded?",
"question_type": "SELECT_ONE",
"optional": false,
"text_is_email": false,
"options": [
{
"id": "24a9e0f7-5b13-4d68-92ca-0e7f8b3d1465",
"text": "Yes, I consent",
"disqualifies": false
},
{
"id": "8d3f61c0-72a4-4e19-b5d8-6f0a2c94e731",
"text": "No, I do not consent",
"disqualifies": true
}
]
}
}
Errors
Status
Code
When
409
conflict
The section is org-managed, so its questions come from the template and cannot be added to.
Lists the organization's consent templates. Pass a study to see only the templates available to that study's workspace, along with whether each is already on it.
Query parameters
Name
Type
Description
study_id
uuid
Restrict the list to templates available to this study's workspace and report which are already applied to it.
page_size
integer · default 50
Number of templates per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list of consent templates.
Field
Type
Description
data[].id
uuid
Template identifier, used when adding or swapping a consent section.
data[].name
string
Display name of the template.
data[].has_own_questions
boolean
Whether the template ships its own questions, which makes an applied section org-managed and read-only.
data[].approved_for_outset_recruitment
boolean
Whether the template satisfies the organization's required-consent policy for participants Outset recruits.
data[].approved_for_own_recruitment
boolean
Whether the template satisfies the organization's required-consent policy for participants you recruit yourself.
data[].already_applied_to_study
boolean
Whether the template is already on the study named in study_id; absent when no study is given.
Consent templates are authored by organization administrators in the product, not over this API — the API applies them. A template may be applied to a study only once.
Stimuli are the images, videos, and documents participants are shown during an interview. They live in a per-project bank, so one uploaded file can be reused across many studies, questions, and concepts in the same project. Getting a file into a study is always two steps — ask for a presigned upload, PUT the bytes straight to storage, then commit, at which point Outset validates the real bytes and mints the reusable stimulus — followed by a third step that attaches the stimulus to the question or concept that should show it. Content images are a separate, simpler surface: images embedded inside question or option markdown rather than attached as a question's stimulus.
Ask for a presigned URL to upload one stimulus file. Outset chooses the storage key and returns a short-lived PUT URL plus the exact headers to apply; you then send the bytes directly to storage and commit the upload. Nothing about the study changes until you commit.
Path parameters
Name
Type
Description
study_id
UUID
Study whose project the uploaded file will belong to.
Request body
The file you intend to upload. Only the name is needed — the bytes never pass through this request.
Field
Type
Description
filenamerequired
string
Name of the file you are about to upload, including its extension — images (`jpg`, `jpeg`, `png`, `webp`, `gif`), video (`mp4`, `mov`, `webm`, `avi`, `mpeg`, `ogv`), or documents (`pdf`, `docx`). Any other extension is rejected.
{
"filename": "pricing-page-v2.png"
}
Response
The upload ticket. `PUT` the file bytes to `upload_url`, applying every header in `headers` verbatim, then call the commit endpoint with `upload_id`.
Field
Type
Description
data.upload_id
UUID
Identifier for this upload, used to commit it and never reusable once committed.
data.upload_url
string
Presigned URL to send the file bytes to with a single HTTP PUT.
data.headers
array of objects
Headers that must be sent verbatim on the PUT — each entry has a `name` and a `value`. Omitting or altering one invalidates the signature and the upload fails.
data.expires_at
datetime
When the presigned URL stops working (15 minutes after issue). Request a new upload rather than retrying an expired URL.
The filename has no extension, or an extension outside the supported image / video / document set.
Outset chooses the storage key; clients never name their own. The stored content type is derived from the extension rather than from anything you send, so a mismatch between the extension and the real bytes is caught at commit, not here. Files up to the storage limit are fine — the bytes go straight to storage and never traverse the API.
Confirm that the bytes have landed. Outset reads the stored object, validates that its real content matches the declared file type, and creates the reusable stimulus in the study's project. The returned stimulus_id is what you attach to a question or concept.
Path parameters
Name
Type
Description
study_id
UUID
Study the upload was issued for.
upload_id
UUID
Upload ticket returned when the upload was started.
Request body
Empty — the commit call takes no fields. Outset validates the bytes that actually landed at the presigned URL.
Response
The created stimulus, now part of the project's reusable bank.
Field
Type
Description
data.id
UUID
Stimulus identifier, used to attach this asset to questions and concepts.
data.filename
string
Original file name as uploaded.
data.content_type
string
Media type detected from the stored bytes, e.g. `image/png`.
data.description
string
Outset-generated description of the asset's visual content, used to give the AI interviewer context about what participants are being shown. Empty until it has been generated.
data.file_url
string
Short-lived URL for viewing or downloading the file. Expires within minutes and must not be stored.
{
"data": {
"id": "b47c0e91-8f2d-4a63-91be-5d0c7a1f3e88",
"filename": "pricing-page-v2.png",
"content_type": "image/png",
"description": "A pricing page with three plan columns; the middle plan is highlighted as the recommended one.",
"file_url": "https://files.outset.ai/stimulus-bank/…?X-Amz-Signature=…"
}
}
Errors
Status
Code
When
400
validation_error
No object was found for this upload — the presigned URL expired before the PUT, or the PUT never succeeded.
400
validation_error
The stored bytes do not match the declared file type. Password-protected and sensitivity-labelled Word documents fail here specifically and cannot be used as stimuli.
Validation happens against the real bytes, not the filename you declared — an upload that passes the start call can still be rejected here. Commit is single-use: once committed, the upload ticket is spent and re-committing returns 404.
List every reusable stimulus in the project the study belongs to, newest first, with a short-lived view URL and the questions, concepts, and sections currently using each one. Use it to reuse an asset instead of uploading it again.
Path parameters
Name
Type
Description
study_id
UUID
Study whose project's stimulus bank is listed. The bank is per project, so sibling studies in the same project see the same assets.
Query parameters
Name
Type
Description
page_size
integer · default 50
Rows per page, maximum 200.
cursor
string
Opaque cursor from a previous page's `next_cursor`.
Response
Cursor-paginated stimuli, newest first.
Field
Type
Description
data[].id
UUID
Stimulus identifier, used when attaching it to a question or concept.
data[].filename
string
Original file name, or `null` for a stimulus captured from a web page rather than uploaded.
data[].description
string
Outset-generated description of the asset's visual content, used to give the AI interviewer context about what participants are being shown. Empty until it has been generated.
data[].content_type
string
Media type of the stored file, e.g. `image/png` or `video/mp4`.
data[].capture_url
string
Web page this stimulus was captured from, or `null` when the file was uploaded directly.
data[].file_url
string
Short-lived URL for viewing or downloading the file; `null` while a page capture is still rendering. Expires within minutes and must not be stored.
data[].used_by[]
array of objects
Where this stimulus is currently shown: each entry has `type` (`question`, `concept`, or `section`), `id`, and a human-readable `label`. Empty when the asset is not attached anywhere.
{
"data": [
{
"id": "b47c0e91-8f2d-4a63-91be-5d0c7a1f3e88",
"filename": "pricing-page-v2.png",
"description": "A pricing page with three plan columns; the middle plan is highlighted as the recommended one.",
"content_type": "image/png",
"capture_url": null,
"file_url": "https://files.outset.ai/stimulus-bank/…?X-Amz-Signature=…",
"used_by": [
{
"type": "question",
"id": "1d9a4c27-7b30-4e52-8c11-6ae2f905b7d4",
"label": "Q4"
}
]
}
],
"next_cursor": "cD0yMDI2LTA4LTExKzE0JTNBMzAlM0Ew",
"has_more": true
}
The stimulus bank is scoped to the project, not the study — an asset uploaded for one study is visible to every study in the same project, and detaching it from a question does not remove it from the bank.
Read one stimulus, including a freshly signed file_url. Use this to re-sign a download link that has expired without paging through the whole bank.
Path parameters
Name
Type
Description
study_id
UUID
Study whose project owns the stimulus.
stimulus_id
UUID
Stimulus to read.
Response
The stimulus, in the same shape as a row of the list endpoint.
Field
Type
Description
data.id
UUID
Stimulus identifier, used when attaching it to a question or concept.
data.filename
string
Original file name, or `null` for a stimulus captured from a web page rather than uploaded.
data.description
string
Outset-generated description of the asset's visual content, used to give the AI interviewer context about what participants are being shown. Empty until it has been generated.
data.content_type
string
Media type of the stored file, e.g. `image/png` or `video/mp4`.
data.capture_url
string
Web page this stimulus was captured from, or `null` when the file was uploaded directly.
data.file_url
string
Short-lived URL for viewing or downloading the file; `null` while a page capture is still rendering. Expires within minutes and must not be stored.
data.used_by[]
array of objects
Where this stimulus is currently shown: each entry has `type` (`question`, `concept`, or `section`), `id`, and a human-readable `label`. Empty when the asset is not attached anywhere.
{
"data": {
"id": "b47c0e91-8f2d-4a63-91be-5d0c7a1f3e88",
"filename": "pricing-page-v2.png",
"description": "A pricing page with three plan columns; the middle plan is highlighted as the recommended one.",
"content_type": "image/png",
"capture_url": null,
"file_url": "https://files.outset.ai/stimulus-bank/…?X-Amz-Signature=…",
"used_by": [
{
"type": "question",
"id": "1d9a4c27-7b30-4e52-8c11-6ae2f905b7d4",
"label": "Q4"
}
]
}
}
Errors
Status
Code
When
404
not_found
The stimulus belongs to a different project, or to a workspace outside the credential's grant.
Set what participants see alongside a question — an asset already in the project bank, a web page for Outset to capture, or whatever another question in the same study is showing. Replaces any stimulus currently attached.
Path parameters
Name
Type
Description
study_id
UUID
Study that owns the question.
question_id
UUID
Question to attach the stimulus to.
Request body
Exactly one source must be given; sending none or more than one is a 400.
Field
Type
Description
stimulus_id
UUID
An existing stimulus in this study's project — from a committed upload or from the list endpoint.
capture_url
string
Public web page for Outset to screenshot and use as the stimulus. Only pass a URL a person actually supplied.
source_question_id
UUID
Another question in the same study whose stimulus should be reused here.
The question is inside a concept-testing section — those questions carry no stimulus; attach to the concept instead.
400
validation_error
`capture_url` points at an internal or otherwise unroutable host. Only publicly reachable pages can be captured.
400
validation_error
`source_question_id` names a question with no stimulus to copy.
Attaching is a pointer, not a copy: the same stimulus can back many questions, and its used_by list on the read endpoints is how you see where else it appears. A capture_url attach renders the screenshot server-side, so the stimulus exists immediately but its file_url stays null until the render lands — poll the stimulus until it is populated. HTML co-design files are a restricted case: they attach only to co-design questions, and only for organizations with that capability enabled.
Detach whatever stimulus the question is showing. The asset itself stays in the project's stimulus bank and any other question or concept using it is unaffected.
Path parameters
Name
Type
Description
study_id
UUID
Study that owns the question.
question_id
UUID
Question to detach the stimulus from.
Response
204 — empty body. Detaching a question that has no stimulus succeeds unchanged, and the asset stays in the project's stimulus bank.
Set the asset a concept in a concept-testing section is evaluated against — an existing stimulus, a web page to capture, or the stimulus already on another concept in the same study. Replaces any stimulus currently attached.
Path parameters
Name
Type
Description
study_id
UUID
Study that owns the concept-testing section.
concept_id
UUID
Concept to attach the stimulus to.
Request body
Exactly one source must be given; sending none or more than one is a 400.
Field
Type
Description
stimulus_id
UUID
An existing stimulus in this study's project — from a committed upload or from the list endpoint.
capture_url
string
Public web page for Outset to screenshot and use as the stimulus. Only pass a URL a person actually supplied.
source_concept_id
UUID
Another concept in the same study whose stimulus should be reused here.
{
"capture_url": "https://example.com/pricing"
}
Response
The concept's stimulus after the change.
Field
Type
Description
data.concept_id
UUID
Concept that was updated.
data.section_id
UUID
Concept-testing section the concept belongs to.
data.stimulus
object
The attached stimulus: `id`, `filename`, `content_type`, `capture_url`, and a short-lived `file_url`, as documented on the stimulus read endpoint.
The stimulus is an HTML co-design file — those attach only to co-design questions, never to concepts.
400
validation_error
`source_concept_id` names a concept with no stimulus to copy.
A capture_url attach on a concept always renders asynchronously: the concept points at the new stimulus straight away, but its file_url is null until the screenshot completes.
Get a presigned URL for an image you want to embed inside a question's wording or an answer option's label, plus the durable URL to reference it by. Distinct from a stimulus, which is attached to the question as a whole rather than placed within its text.
Path parameters
Name
Type
Description
study_id
UUID
Study whose content the image will be embedded in.
Request body
The image you intend to upload, plus the alt text for the generated markdown snippet.
Field
Type
Description
filenamerequired
string
Name of the image file including its extension — `jpg`, `jpeg`, `png`, `webp`, or `gif`. Other types are rejected.
alt_text
string
Alt text used in the returned markdown snippet; defaults to empty.
The filename has no extension, or an extension outside the supported image set.
Unlike stimulus uploads there is no commit step — url resolves as soon as your PUT completes, and the image is not validated beyond its declared type. The upload alone changes nothing: the image only appears to participants once you put url (or the markdown snippet) into a question's text or an option's label through the study-content endpoints. Content images are served from a durable public URL rather than a signed one, so treat anything you upload here as publicly readable by whoever holds the link.
A study is written in one source language and can offer participants a curated set of additional languages, each translated by Outset and tracked through its own generation and review lifecycle. The flow is always the same three steps: declare the offered set, generate translations, then (optionally) review each language before the study publishes. Reads need studies:read; every write here is studies:write, and the two destructive ones — replacing the offered set and repinning the source language — are refused while the study is published.
Every offered language follows one lifecycle: QUEUED (never generated, or stale after a content edit) → RUNNING (generating) → GENERATED, REVIEWED, or FAILED. The endpoints below use those states throughout.
Returns the study's source language plus every language it offers participants, each with its translation status and review settings. Poll this after triggering generation until every language reads GENERATED, REVIEWED, or FAILED.
Path parameters
Name
Type
Description
study_id
uuid
The study whose language configuration to read.
Response
The study's language configuration as a single object.
Field
Type
Description
data.written_language_code
string
The language the guide, screener, and other study content are written in — the source every other language is translated from.
data.base_language_code
string
The language used for recruitment copy and as the participant-facing default; empty means it falls back to the written language. Set on the study resource, not here.
data.language_mode
string
How participant languages are offered: `CURATED` (a researcher-curated, pre-generated set) is the only implemented mode; `OPEN` is reserved for a future lazy-generation mode.
data.languages[].language_code
string
ISO 639-1 code, or a BCP-47 regional variant such as `pt-BR` or `zh-Hant`.
data.languages[].status
string
One of `QUEUED` (never generated, or stale after a content edit), `RUNNING` (generating), `GENERATED`, `REVIEWED`, or `FAILED`.
data.languages[].review_required
boolean
Whether this language must be marked reviewed before the study can publish.
data.languages[].reviewed_at
datetime
When the language was last marked reviewed, or null if it never was.
data.languages[].translation_context
string
The glossary and formality brief Outset generated for this language to keep its per-item translations consistent; empty until translations are generated.
data.languages[].interviewer_voice_engine
string
Per-language interviewer voice engine, `SIMPLE` or `ADVANCED`; null means the language inherits the study-level voice.
data.languages[].interviewer_voice_id
string
The picked voice for an `ADVANCED` engine, or null when none is set.
The source language appears in languages only when the study offers its own source text to participants. That row carries no translations, so it is always ready and never needs review.
Declaratively sets the languages the study offers participants. Send the full set on every call: languages you omit are removed, and newly added ones start at QUEUED until you trigger generation. Include the study's written language in the list to also offer participants the untranslated source text.
Path parameters
Name
Type
Description
study_id
uuid
The study whose offered languages to replace.
Request body
The complete set of languages the study should offer.
Field
Type
Description
languagesrequired
array
The full set of language codes to offer, at least one and at most 50; each must be a language Outset supports (a base ISO 639-1 code, or a regional variant such as `pt-BR`, `es-419`, `fr-CA`, `de-CH`, `zh-Hant`).
{
"languages": [
"en",
"de",
"ja"
]
}
Response
The study's language configuration after the change, in the same shape as the GET.
Field
Type
Description
data.written_language_code
string
The study's source language, unchanged by this call.
data.base_language_code
string
The language used for recruitment copy and as the participant-facing default; empty means it falls back to the written language. Set on the study resource, not here — the two differ when recruitment copy is deliberately fielded in another language.
data.language_mode
string
How participant languages are offered: `CURATED` (a researcher-curated, pre-generated set) is the only implemented mode; `OPEN` is reserved for a future lazy-generation mode.
data.languages[]
array
The resulting offered languages. Each carries `language_code`, `status`, `review_required`, `reviewed_at`, `translation_context`, `interviewer_voice_engine` and `interviewer_voice_id`, itemized under Get a study's language configuration.
One of the codes is not a language Outset supports.
400
validation_error
The list is empty — a study must always offer at least one language.
409
published_study
The study is published. Unpublish it, change languages, then publish again.
Removing a language deletes its generated translations, and that cannot be undone — re-adding the language starts it from QUEUED and costs a fresh generation run. Each newly added language is also seeded with a voice appropriate to it rather than inheriting the study-level (source-language) voice. If the study's base language is not in the set you send, it is cleared and falls back to the written language.
Declares the language the study is written in. Outset normally detects this from the guide's own text, so use this only to correct a wrong detection or a study authored in a language other than the detected one.
Path parameters
Name
Type
Description
study_id
uuid
The study whose source language to set.
Request body
The language the study content is written in.
Field
Type
Description
language_coderequired
string
The language code the study is written in; must be a language Outset supports.
{
"language_code": "ja"
}
Response
The study's language configuration after the change, in the same shape as the GET. The new source language reads REVIEWED, because source text needs no translation.
Field
Type
Description
data.written_language_code
string
The language the guide, screener, and other study content are written in — the source every other language is translated from.
data.base_language_code
string
The language used for recruitment copy and as the participant-facing default; empty means it falls back to the written language. Set on the study resource, not here.
data.language_mode
string
How participant languages are offered: `CURATED` (a researcher-curated, pre-generated set) is the only implemented mode; `OPEN` is reserved for a future lazy-generation mode.
data.languages[].language_code
string
ISO 639-1 code, or a BCP-47 regional variant such as `pt-BR` or `zh-Hant`.
data.languages[].status
string
One of `QUEUED` (never generated, or stale after a content edit), `RUNNING` (generating), `GENERATED`, `REVIEWED`, or `FAILED`.
data.languages[].review_required
boolean
Whether this language must be marked reviewed before the study can publish.
data.languages[].reviewed_at
datetime
When the language was last marked reviewed, or null if it never was.
data.languages[].translation_context
string
The glossary and formality brief Outset generated for this language to keep its per-item translations consistent; empty until translations are generated.
data.languages[].interviewer_voice_engine
string
Per-language interviewer voice engine, `SIMPLE` or `ADVANCED`; null means the language inherits the study-level voice.
data.languages[].interviewer_voice_id
string
The picked voice for an `ADVANCED` engine, or null when none is set.
The study is published. Unpublish it, change the source language, then publish again.
Destructive and irreversible. Everything else is translated from the source, so any translations already generated for the language you name here are deleted (source text is served directly instead). This call also pins the source language permanently: automatic detection will never override it afterward.
Kicks off translation of the study's guide, screener, concepts, upload instructions, and welcome/end messages into every offered language that needs work. Returns 202 immediately with the study's language configuration; each affected language flips to RUNNING and settles at GENERATED or FAILED. Poll GET /v2/studies/{study_id}/languages/ to follow progress.
Path parameters
Name
Type
Description
study_id
uuid
The study whose translations to generate.
Response
The study's language configuration as of the kickoff, in the same shape as the GET. There is no separate job handle: `GET /v2/studies/{study_id}/languages/` is the poll surface.
Field
Type
Description
data.written_language_code
string
The study's source language, unchanged by this call.
data.languages[]
array
The study's languages at kickoff, itemized under Get a study's language configuration — the languages picked up for this run read `RUNNING`.
The study offers no language other than the one it is written in, so there is nothing to translate.
503
source_language_detection_unavailable
The study's source language has not been detected yet and detection is temporarily unavailable. Retry; generating against the wrong source would be permanent.
Per-language progress is authoritative on GET /v2/studies/{study_id}/languages/ — poll that until each language reads GENERATED, REVIEWED, or FAILED. Generation is incremental: languages already GENERATED or REVIEWED are skipped unless a content edit made them stale (which flips them back to QUEUED), and only the edited items are re-translated. Repeated calls therefore cost only the outstanding work, but each run does consume model capacity — don't poll by re-triggering. Retrying a FAILED language is just another call to this endpoint.
Updates a single offered language's review requirement and interviewer voice override. Omitted fields keep their current value.
Path parameters
Name
Type
Description
study_id
uuid
The study the language belongs to.
language_code
string
The offered language to update, e.g. `de`.
Request body
The settings to change; send only the fields you want to move.
Field
Type
Description
review_required
boolean
Whether this language must be marked reviewed before the study can publish.
interviewer_voice_engine
string
`SIMPLE` for the default voice for the language, `ADVANCED` to pick a voice from the catalog, or null to clear the override and inherit the study-level voice.
interviewer_voice_id
string
The catalog voice to speak this language, only meaningful with an `ADVANCED` engine; null or empty clears it.
{
"review_required": true
}
Response
The updated language.
Field
Type
Description
data.language_code
string
The language that was updated.
data.status
string
Its translation status, unchanged by this call.
data.review_required
boolean
Whether the language now blocks publishing until it is marked reviewed.
data.interviewer_voice_engine
string
The resulting voice engine, or null when the language inherits the study-level voice.
Setting review_required on the study's own written language is a no-op — source text carries no translations to review, so the flag stays false. Switching the engine to SIMPLE, or clearing it, also clears any voice previously picked for this language unless you set a new one in the same request.
Records that a generated language has been checked, moving it from GENERATED to REVIEWED and clearing the publish gate for languages that require review. Calling it on an already-reviewed language succeeds and changes nothing.
Path parameters
Name
Type
Description
study_id
uuid
The study the language belongs to.
language_code
string
The offered language to mark reviewed, e.g. `de`.
Response
The reviewed language, carrying the same per-language fields itemized under Get a study's language configuration.
Field
Type
Description
data.language_code
string
The language that was marked reviewed.
data.status
string
`REVIEWED` after a successful call.
data.reviewed_at
datetime
When the language was marked reviewed.
data.review_required
boolean
Whether this language blocks publishing until reviewed.
The language is still `QUEUED`, `RUNNING`, or `FAILED` — only a `GENERATED` language can be marked reviewed.
409
language_has_missing_translations
The language's translations are incomplete. Run generation again, then retry.
A review mark is per item as well as per language: editing the study content a language was translated from makes the affected items stale again, which can drop the language back to QUEUED and require a fresh generate-and-review pass before publishing.
Fielding a study means answering three questions: who should take part, how many of them, and who pays. Outset supports two answers. Panel recruitment buys participants from a third-party provider (Prolific, UserInterviews, or Respondent) against your organization's recruitment wallet — you write participant-facing copy, pick audience filters, check an estimate, and launch. Self recruitment means you bring your own audience: you share the study link yourself, or you upload an email list and Outset sends the invites. Everything below hangs off one singleton resource, /v2/studies/{study_id}/recruitment/, which carries the chosen method, the panel draft, and the self-recruit configuration together. Launching or expanding recruitment spends real money and is deliberately gated behind its own studies:launch scope.
Read everything about how this study is being fielded: the chosen recruitment method, the panel recruitment draft (or the live recruitment, once launched), and the self-recruit invite configuration. This is the endpoint to poll after a launch to confirm the recruitment reached the provider.
Path parameters
Name
Type
Description
study_id
UUID
Study whose recruitment state you want to read.
Response
The study's recruitment singleton. `panel` is `null` until a panel draft is configured; `self_recruit` is `null` unless your organization has email-invite recruitment enabled.
Field
Type
Description
data.study_id
UUID
Study this recruitment configuration belongs to.
data.method
string
How this study recruits: `outset` (buy participants from a panel provider) or `self` (you share the link or invite your own audience). `null` until a method is chosen; a study cannot be published without one.
data.study_active
boolean
Whether the study is currently published and accepting participants.
data.panel
object, nullable
The panel recruitment draft, or the live recruitment once launched. `null` when no panel recruitment has ever been configured for this study.
data.panel.id
UUID
Identifier of this study's panel recruitment.
data.panel.launch_state
string
Lifecycle of the recruitment: `DRAFT` (never launched), `LAUNCHING` (charged, provider study being created), `LIVE` (running on the provider), `FAILED` (charged but the provider call never landed — retry the launch, which does not re-charge), or `CLOSED` (the recruitment was refunded — the draft is terminal and cannot be edited or relaunched). Read this rather than inferring launched-ness from `provider_recruitment_id`.
data.panel.is_active
boolean
Whether this recruitment is currently open on the provider.
data.panel.provider
string
Panel provider fielding this study: `PROLIFIC`, `USERINTERVIEWS`, or `RESPONDENT`.
data.panel.provider_recruitment_id
string, nullable
The provider's own identifier for the created recruitment, once it exists.
data.panel.title
string
Participant-facing study title, shown to panelists on the provider.
data.panel.description
string
Participant-facing study description, shown to panelists on the provider.
data.panel.translated_title
string, nullable
The participant-facing title in the study's target language; `null` when it has not been translated yet.
data.panel.translated_description
string, nullable
The participant-facing description in the study's target language; `null` when it has not been translated yet.
data.panel.target_participants
integer
How many participants this recruitment is buying.
data.panel.reward_usd
string, nullable
Per-participant reward in USD, as a decimal string. Serialized as a string so no precision is lost in transit.
data.panel.total_cost_usd
string, nullable
Up-front charge at launch, in USD. This figure excludes screen-out fees — request a cost estimate for the all-in expected spend.
data.panel.duration_seconds
integer, nullable
Estimated interview length used to price the reward, in seconds.
data.panel.screen_out_limit_usd
string, nullable
Maximum total USD this study will spend paying participants who screen out. The study pauses automatically once screen-out spend reaches it.
data.panel.screen_out_spend_usd
string, nullable
Screen-out spend charged so far against that limit. `null` on providers that do not charge for screen-outs.
data.panel.block_previous_participants
boolean
Whether people already recruited into a previously-launched study in the same project are excluded from this one. Always `false` on providers that cannot exclude previous participants.
data.panel.filters
array of objects
Audience filters currently applied to this recruitment, in the same shape the filter-selection endpoint accepts.
data.panel.provider_capabilities
object
What this provider can and cannot do — whether rewards or participant counts can be edited after launch, whether it charges for screen-outs, per-filter selection limits, and so on. Branch on these flags rather than on the provider name.
data.self_recruit
object, nullable
Email-invite configuration for a self-recruited audience. `null` when your organization does not have email-invite recruitment enabled.
data.self_recruit.invite_email_template.fields
object
The resolved invite copy — `subject`, `greeting`, `body`, `cta`, `signoff`, and the optional `researcher_name` / `researcher_email` contact.
data.self_recruit.invite_email_template.source
string
Which tier supplied the resolved copy: this study's own saved copy, or Outset's standard template.
data.self_recruit.invite_email_template.defaults
object
Outset's standard template copy, so a client can offer a reset to defaults.
data.self_recruit.audience_stats.recipients_ready
integer
Valid, de-duplicated recipients on this study's audience that have not been invited yet.
data.self_recruit.audience_stats.new_invites
integer
Recipients queued to receive an invite on the next send.
How many completed interviews this study is aiming for.
data.self_recruit.audience_uploads
array of objects
One entry per uploaded audience, each with `upload_id`, `original_filename`, `row_count`, `valid_count`, `duplicate_count`, `sent_count`, `response_count`, `status`, and the resolved `email_column` / `name_column` / `metadata_columns`.
data.self_recruit.invite_followup_enabled
boolean
Whether this study sends the 24-hour reminder to recipients who never started. Read-only — the reminder is off by default and only Outset can switch it on.
{
"data": {
"study_id": "0d3c9c7a-4b21-4f6e-9a03-6bb4f1c9a7d2",
"method": "outset",
"study_active": true,
"panel": {
"id": "b71f2e58-2c9d-4a15-8f30-1de6c4a90b77",
"launch_state": "LIVE",
"is_active": true,
"provider": "PROLIFIC",
"provider_recruitment_id": "66b1c0d4e2f39a0d5c118e42",
"title": "20-minute chat about your grocery shopping",
"description": "Tell us how you plan and shop for weekly groceries. Runs in your browser, no camera needed.",
"translated_title": null,
"translated_description": null,
"target_participants": 120,
"reward_usd": "4.50",
"total_cost_usd": "702.00",
"duration_seconds": 1200,
"screen_out_limit_usd": "200.00",
"screen_out_spend_usd": "18.00",
"block_previous_participants": true,
"filters": [
{
"id": "age",
"min": "25",
"max": "54"
},
"… one entry per applied filter"
],
"provider_capabilities": {
"can_edit_reward_after_publish": true,
"charges_for_screenouts": true,
"…": "remaining provider capability flags"
}
},
"self_recruit": null
}
}
total_cost_usd is the up-front charge only. For the all-in figure to quote to a stakeholder — including projected screen-out fees — request a cost estimate instead.
Choose the recruitment method and, for panel recruitment, write the participant-facing copy and set the target participant count, provider, and payout options. Omitted fields are left unchanged. This only edits the draft — nothing is charged and nobody is recruited until you launch.
Path parameters
Name
Type
Description
study_id
UUID
Study to configure recruitment for.
Request body
Recruitment settings to apply. Send `method: "self"` on its own to record that you will recruit your own audience; sending any panel field creates or updates the panel draft and sets the method to `outset` for you.
Field
Type
Description
method
string
How this study recruits: `self` (you share the link with your own audience, nothing is charged) or `outset` (panel recruitment). `outset` is only accepted when a panel draft already exists — send the panel fields below instead and the method follows.
title
string
Participant-facing study title shown to panelists on the provider, 1–255 characters. Required before launch.
description
string
Participant-facing study description shown to panelists on the provider. Required before launch.
target_participants
integer
How many participants to recruit, at least 1. Required before launch.
translated_title
string
Participant-facing title in the study's target language, for a study fielded in a language other than its source. Omit to leave unchanged — the translation pipeline fills it from `title` when empty.
translated_description
string
Participant-facing description in the study's target language. Omit to leave unchanged — the translation pipeline fills it from `description` when empty.
provider
string
Panel provider to field with: `PROLIFIC`, `USERINTERVIEWS`, or `RESPONDENT`. Defaults to `PROLIFIC` on a new draft; on an existing unlaunched draft, omitting it keeps the current provider.
block_previous_participants
boolean
Exclude people already recruited into a previously-launched study in the same project, through the same provider. Omit to leave unchanged; a new draft excludes them where the provider supports it. Forced to `false` on providers that cannot exclude previous participants.
screen_out_limit_usd
string
Maximum total USD to spend on participants who screen out, as a decimal string of at least `1.00`. The study pauses automatically once screen-out spend reaches it. Omit to leave unchanged; a new draft defaults to 200. Ignored on providers that do not charge for screen-outs.
payout_bonus_usd
string
Extra USD per participant on top of the provider-calculated base reward, as a non-negative decimal string with at most two decimal places. This is an absolute amount over the base, so sending the same value twice is idempotent. Send `"0"` to return to the base reward.
{
"title": "20-minute chat about your grocery shopping",
"description": "Tell us how you plan and shop for weekly groceries. Runs in your browser, no camera needed.",
"target_participants": 120,
"provider": "PROLIFIC",
"screen_out_limit_usd": "200.00",
"payout_bonus_usd": "0.50"
}
Response
The updated recruitment singleton, in the same shape the read endpoint returns, with the cost estimate recomputed.
Field
Type
Description
data
object
The recruitment singleton — see the read endpoint for the full field list.
This study's recruitment is already live on the provider — the guard is the live recruitment, not the study's published state. A live recruitment can only be expanded, through the increase endpoint.
403
feature_not_enabled
`provider: "RESPONDENT"` on an organization that is not enabled for Respondent recruitment.
title and description go straight onto the provider's panel listing, verbatim — they are participant-facing copy, not internal labels.
List the audience filters a panel provider supports — age, country, employment status, approval rate, and so on. Call this before selecting filters on a study, to discover valid filter ids and option keys. The catalog is a property of the provider, not of any one study.
Query parameters
Name
Type
Description
provider
string · default PROLIFIC
Panel provider whose catalog to read: `PROLIFIC`, `USERINTERVIEWS`, or `RESPONDENT`.
Response
The provider's full filter catalog. This is not a paginated collection — the catalog is returned whole.
Field
Type
Description
data.provider
string
Provider the catalog belongs to.
data.filters[].id
string
Filter id to send when selecting this filter on a study.
data.filters[].name
string
Human-readable filter name.
data.filters[].description
string, nullable
Longer explanation of what the filter matches, when the provider supplies one.
data.filters[].question
string, nullable
The question panelists answered to populate this filter, when the provider supplies it.
data.filters[].category
string, nullable
Grouping the provider files this filter under, useful for rendering a picker.
data.filters[].type
string
`select` (pick from `options`) or `range` (supply `min` / `max`).
data.filters[].options
array of objects
Select filters only: the option keys and labels that may be selected.
data.filters[].min
string, nullable
Range filters only: lowest value the provider accepts — a number, or an ISO 8601 date for date ranges.
data.filters[].max
string, nullable
Range filters only: highest value the provider accepts.
Replace the audience filters on a study's panel recruitment draft. This is a full replacement — send the complete set you want, or an empty list to clear all filters. Every filter is validated against the provider's catalog, and the reward estimate is recomputed because narrowing the audience can raise the provider's suggested minimum wage.
Path parameters
Name
Type
Description
study_id
UUID
Study whose recruitment filters to replace.
Request body
The complete filter set. Each entry carries a filter `id` from the catalog plus either `selected_options` (select filters) or `min` / `max` (range filters).
Field
Type
Description
filtersrequired
array of objects
Filters to apply. Pass `[]` to clear every filter.
filters[].idrequired
string
Filter id from the provider's catalog.
filters[].selected_options
array of strings
Select filters only: the option keys to accept. Omit for range filters.
filters[].min
string
Range filters only: lower bound, as a number or an ISO 8601 date string. Omit for select filters.
filters[].max
string
Range filters only: upper bound, as a number or an ISO 8601 date string. Omit for select filters.
block_previous_participants
boolean
Exclude people already recruited into a previously-launched study in the same project. Omit to leave the current setting unchanged.
The updated recruitment singleton, with the stored filters and the recomputed reward.
Field
Type
Description
data
object
The recruitment singleton — see the read endpoint for the full field list.
Errors
Status
Code
When
400
validation_error
A filter id, option key, or range bound is not one the provider accepts.
409
conflict
The recruitment is already live on the provider — most providers do not allow the audience to be changed after launch. Unpublishing the study does not lift this; the recruitment itself has to be closed.
Ask a provider how many panelists match a filter set, before committing to it on a study. Use it to check that criteria are not so narrow that recruitment would stall. Nothing is stored, and this does not need a study — pass the filters you are considering.
Request body
The provider and filter set to size.
Field
Type
Description
provider
string
Panel provider to ask: `PROLIFIC`, `USERINTERVIEWS`, or `RESPONDENT`. Defaults to `PROLIFIC`.
filters
array of objects
Filters to size, in the same shape the filter-selection endpoint accepts. Omit or pass `[]` for the provider's whole panel.
Price a study's recruitment draft from fresh data — the same arithmetic the launch runs before charging. Pass target_participants for a what-if estimate at a different participant count; nothing is persisted either way. This is a POST because it takes a body and calls the provider, not because it changes anything.
Path parameters
Name
Type
Description
study_id
UUID
Study whose recruitment draft to price.
Request body
Optional overrides for the estimate.
Field
Type
Description
target_participants
integer
What-if participant count, at least 1. Defaults to the draft's configured count.
{
"target_participants": 200
}
Response
The recomputed cost breakdown. Quote `expected_total_cost_usd` — it is the all-in figure, and it is what the product's Recruit page shows as "Total cost".
Field
Type
Description
data.study_id
UUID
Study the estimate is for.
data.provider
string
Panel provider the estimate was priced against.
data.target_participants
integer
Participant count the estimate was computed at.
data.estimated_duration_seconds
integer
Estimated interview length used to price the reward, in seconds.
data.reward_per_assignment_usd
string, nullable
Per-participant reward in USD, as a decimal string.
data.total_cost_usd
string, nullable
Up-front charge at launch, in USD. Excludes screen-out fees.
data.whitelabel_cost_usd
string, nullable
Portion of the cost attributable to whitelabeled fielding, when it applies.
data.expected_total_cost_usd
string, nullable
All-in expected spend: the up-front charge plus projected screen-out fees. The number to quote.
data.charges_for_screenouts
boolean
Whether this provider bills for participants who screen out.
data.per_screenout_fee_usd
string, nullable
What one screened-out participant costs, in USD.
data.expected_screenout_cost_usd
string, nullable
Projected total screen-out spend, computed at a conservative screen-out incidence. Screen-out fees are charged as they occur, not up front.
data.screen_out_limit_usd
string, nullable
The study's configured screen-out spending cap. If it is below `expected_screenout_cost_usd`, the launch is blocked until you raise it.
data.screen_out_spend_usd
string, nullable
Screen-out spend already charged against that cap.
Activate the study's panel recruitment draft. Outset validates the study, the screener, and your available funds, publishes the study if it is not already live, charges the full up-front amount, and creates the recruitment on the provider. Participants can start arriving within minutes.
Path parameters
Name
Type
Description
study_id
UUID
Study whose recruitment to launch.
Request body
No body is required.
Response
The recruitment singleton after launch, with `panel.launch_state` moved to `LAUNCHING` or `LIVE`.
Field
Type
Description
data
object
The recruitment singleton — see the read endpoint for the full field list.
The draft is incomplete (no participant-facing title or description, no participant count), the screener does not validate, or the screen-out cap is below the projected screen-out cost. The error detail names the specific blocker.
402
insufficient_credit_balance
The organization's recruitment wallet or workspace budget cannot cover the up-front charge. The detail carries the required and current balances.
409
conflict
Recruitment on this study is already active on the provider.
This spends real money. The full up-front amount is charged at launch — there are no partial commits, and the charge is not reversed by closing the study (unfilled slots are refunded on settlement). It also publishes the study if it was not already published, so the guide goes live at the same moment. Re-launching a recruitment left in FAILED state retries the provider call without charging again.
Expand a recruitment that is already live: buy more participants, raise the per-participant reward to speed up fielding, or both in one call. At least one field is required. Both changes cost money and are charged when applied.
Path parameters
Name
Type
Description
study_id
UUID
Study whose live recruitment to expand.
Request body
The increases to apply. Send at least one field.
Field
Type
Description
target_participants
integer
New target participant count. Must be higher than the current count; the additional cost is charged when the increase is applied.
payout_bonus_usd
string
Additional per-participant reward in USD, as a decimal string. This is an increment on top of the current reward, so sending the same value twice raises it twice. It applies only to the remaining unfilled slots.
The recruitment singleton with the new participant count and reward.
Field
Type
Description
data
object
The recruitment singleton — see the read endpoint for the full field list.
Errors
Status
Code
When
400
validation_error
The new participant count is not higher than the current one, or the provider does not allow reward edits at this stage of the recruitment. Check `provider_capabilities` before offering either control.
402
insufficient_credit_balance
The wallet or workspace budget cannot cover the additional cost.
409
conflict
The study has no active recruitment to expand.
Both operations charge immediately, and neither can be undone — a reward increase in particular is a one-way ratchet on the remaining slots. Note the difference from configuring a draft: there, payout_bonus_usd is an absolute amount over the base reward; here it is an increment.
Set quota targets on the study's screener so the completed sample matches a shape you specify — a 50/50 gender split, 25% per age band, and so on. Quotas are expressed as per-option shares of the screener questions you enable them on, and this is a full replacement of the quota configuration.
Path parameters
Name
Type
Description
study_id
UUID
Study whose screener quotas to set. The study must already have a screener with questions and options.
Request body
The quota configuration to apply.
Field
Type
Description
interaction_moderequired
string
How quotas combine across questions: `INDEPENDENT` (each option capped on its own — the common case) or `DEPENDENT` (caps apply to combinations of options across questions, e.g. "women aged 18–25").
questionsrequired
array of objects
Per-question quota configuration. Include every screener question that should carry quotas.
questions[].screener_question_idrequired
UUID
Screener question the quota applies to.
questions[].quota_enabled
boolean
Whether quotas apply to this question. When `false`, every option on it has its share cleared, whatever `options` contains.
questions[].optionsrequired
array of objects
Target share per option. List every option on the question; shares should sum to no more than 1.0.
questions[].options[].option_idrequired
UUID
Screener question option this share applies to.
questions[].options[].quota_percentage
number, nullable
Share of participants targeted for this option, as a fraction between 0.0 and 1.0 — pass 0.5 on each of two options for an even split. Must be `null` when `is_other_category` is true.
questions[].options[].is_other_category
boolean
Marks this option as the remainder bucket that catches participants who do not fit the explicit shares. At most one option per question.
flexible_fill
boolean
When true, a participant is kept as long as one of their matching quota buckets still needs filling and none is at its overflow cap, instead of being disqualified as soon as any matching bucket is full. Omit to leave the current setting unchanged.
The study has no screener yet, or a question or option id does not belong to this study's screener.
Quotas govern who is admitted, so a tight configuration can slow fielding or leave a recruitment unable to fill. Changing them invalidates Outset's cached per-quota counts, which regenerate on demand.
Clear all quota configuration from the study's screener: quotas are turned off on every screener question and every option's target share is cleared. Participants are then admitted purely on screener qualification.
Path parameters
Name
Type
Description
study_id
UUID
Study whose screener quotas to clear.
Response
204 — empty body. Quotas are off on every screener question and every target share is cleared; read the recruitment configuration back to confirm.
Set the copy of the initial invitation email sent to a self-recruited email audience. Saved per study, so editing one study never changes another. This only stores copy — it sends nothing.
Path parameters
Name
Type
Description
study_id
UUID
Study whose invite email copy to set.
Request body
The invite copy. Only the `{name}`, `{company}`, and `{study_name}` merge tags may be used.
Field
Type
Description
subject
string
Email subject line. Required for a self-recruit study; ignored for a panel-recruited diary session, whose message renders no email chrome.
bodyrequired
string
Email body — or, for a panel-recruited diary session, the whole message. A diary-session body may not use `{name}`, because a panel participant has no name available at send time.
cta
string
Label of the button that opens the study. Required for a self-recruit study; ignored for a panel-recruited diary session.
greeting
string
Greeting line, e.g. `Hello {name},`. Send an empty string to omit the line entirely.
signoff
string
Sign-off line, e.g. `Thanks so much,`. Send an empty string to omit the line entirely.
researcher_name
string
Researcher name shown in the sign-off. Omit to leave the study's stored contact unchanged; send an empty string to clear it.
researcher_email
string
Reply-to address so participants can reply to a person. Omit to leave the study's stored contact unchanged; send an empty string to clear it.
{
"subject": "Can we borrow 20 minutes of your time?",
"greeting": "Hi {name},",
"body": "We're improving how {company} plans weekly grocery shopping and would love your take. The conversation takes about 20 minutes and runs in your browser.",
"cta": "Start the interview",
"signoff": "Thanks so much,",
"researcher_name": "Priya Raman",
"researcher_email": "priya@example.com"
}
Response
The resolved template after the change.
Field
Type
Description
data.fields
object
The resolved copy an editor would pre-fill — the study's own copy where set, Outset's standard template otherwise.
data.source
string
Which tier supplied the resolved copy: this study's saved copy, or the standard template.
data.defaults
object
Outset's standard template copy, so a client can offer a reset.
Errors
Status
Code
When
403
feature_not_enabled
The organization does not have email-invite recruitment enabled.
400
validation_error
The copy uses a merge tag other than `{name}` / `{company}` / `{study_name}`, or uses `{name}` on a panel-recruited diary session.
The 24-hour follow-up reminder is never customizable — this endpoint only sets the initial email.
Add a list of email recipients to a self-recruited study, either as structured entries or as the raw text of a CSV. Send exactly one of recipients or csv_content. Recipients are de-duplicated within the request and against the study's existing audience, and suppression-filtered, so calling this repeatedly adds people without creating duplicate invites.
Path parameters
Name
Type
Description
study_id
UUID
Study to add the audience to.
Request body
The audience to add. `recipients` validates strictly — one bad address rejects the whole request; `csv_content` is lenient and reports skipped rows in the response counts.
Field
Type
Description
recipients
array of objects
Structured recipient list. Use instead of `csv_content`.
recipients[].emailrequired
string
Recipient email address. Every address must be valid.
recipients[].name
string
Recipient display name, used by the `{name}` merge tag.
recipients[].metadata
array of objects
Extra per-recipient columns as `key` / `value` pairs. Each value rides onto the participant and becomes filterable in reports. `email` and `name` are reserved keys, and at most 20 distinct column names may appear across all recipients.
csv_content
string
Raw text of an audience CSV, under 5 MB. Use instead of `recipients`. Columns are auto-detected; invalid rows are skipped and counted rather than failing the request.
email_column
string
With `csv_content`: header name of the email column, when auto-detection would get it wrong.
name_column
string
With `csv_content`: header name of the name column.
metadata_columns
array of strings
With `csv_content`: header names of extra columns to ingest as per-recipient metadata, up to 20.
The organization does not have email-invite recruitment enabled.
400
validation_error
Neither or both of `recipients` and `csv_content` were sent, an address in `recipients` is invalid, or the CSV cannot be parsed.
This is additive and sends nothing. Releasing the audience so invites actually go out is a separate step, and each call creates its own audience entry even though the addresses themselves de-duplicate.
Smoke-test a study before spending money on real participants: an AI simulates the audience you describe and runs it through the study's guide, producing synthetic interviews. Returns 202 with a job to poll; the interviews land over the following few minutes and appear in interview lists under the SYNTH interview type.
Path parameters
Name
Type
Description
study_id
UUID
Study to generate synthetic interviews for.
Request body
The audience to simulate and how many interviews to run.
Field
Type
Description
audience_descriptionrequired
string
Free-text description of the audience to simulate, e.g. "busy parents who grocery shop online weekly".
countrequired
integer
How many synthetic interviews to generate, between 1 and 50. Each one runs the full study guide.
{
"audience_description": "Busy parents in the UK who order groceries online at least weekly",
"count": 10
}
Response
`202 Accepted` with the job to poll.
Field
Type
Description
data.job_id
UUID
Job identifier — poll the job endpoint below for progress.
The study is a usability study (screen-share or mobile UX). Those lead with a task an AI participant cannot perform, so synthetic interviews would come back empty.
Synthetic interviews are AI-generated and are not evidence about real people — use them to check that a guide flows, not to draw conclusions. They are excluded from interview lists by default; request the SYNTH interview type to see them.
Poll a synthetic interview generation job. Terminal states are COMPLETED and FAILED; once complete, fetch the interviews themselves from the interview list endpoints with the SYNTH interview type.
Ask Outset's recruitment team to source participants the self-serve panels cannot reach — niche B2B roles, specific product users, hard-to-find markets. This files a request that a person picks up; it recruits nobody and launches nothing.
Request body
What you need recruited. All four fields are required and must be non-blank.
Field
Type
Description
interview_length_minutesrequired
string
How long each interview should run, in minutes. Up to 255 characters.
participant_countrequired
string
How many qualifying participants are needed. Up to 255 characters.
participant_locationrequired
string
Where participants should be located — city, state, country, or region. For several markets, use the form "Location: number of participants". Up to 255 characters.
screening_criteriarequired
string
Required participant criteria — B2B or B2C, job title, industry, company size, or the specific products they use. Up to 5000 characters.
{
"interview_length_minutes": "30",
"participant_count": "12",
"participant_location": "United Kingdom: 8, Ireland: 4",
"screening_criteria": "B2B. Procurement leads at grocery retailers with 50+ stores who have changed supplier software in the last 18 months."
}
Response
Confirmation that the request was accepted for delivery to the recruitment team.
Field
Type
Description
data.request_id
UUID
Reference for this request, worth quoting in any follow-up.
data.status
string
`submitted` — the request has been accepted for delivery.
Any of the four fields is blank or whitespace-only.
Managed recruitment is priced and scheduled by the Outset team, not by this API — the request opens a conversation, it does not commit either side to anything.
An interview is one participant's session through a study: the transcript, the screener answers they gave to qualify, recording metadata, and any observations the vision pipeline derived from their recordings. These are the rawest participant data the API exposes, so a PII gate sits under every endpoint in this chapter. A completed interview on a PII-enabled study is scanned before it is readable — while detection is running (or if it failed) the whole interview reads as 404, and a live interview on such a study is withheld until it completes. Once scanning finishes, flagged content is handled per message rather than per interview: a message with an unresolved flag comes back as "[PII Under Review]" with its structured answer values and media URLs withheld, and a message whose flag was accepted and redacted comes back as "[PII Redacted]" with is_redacted: true. Redaction is permanent — the original text is destroyed in place, so no surface can recover it. Reads need analytics:read; transcript corrections and the PII flag lifecycle need analytics:write.
Lists the interviews collected for one study, newest first. Defaults match the researcher UI — real participant interviews only, archived excluded — so opt in explicitly to synthetic, test, and imported rows.
Path parameters
Name
Type
Description
study_id
uuid
Identifier of the study whose interviews to list.
Query parameters
Name
Type
Description
interview_types
string (repeatable) · default USER
Interview types to include: USER (human participant), SYNTH (AI-generated), TEST (internal test), IMPRT (transcript imported from elsewhere).
only_screened_in
boolean · default False
Return only interviews that passed the screener and count as valid completes — excludes in-progress, screened-out, fraud, and low-quality sessions.
only_screened_out
boolean · default False
Return only interviews the screener rejected; cannot be combined with only_screened_in.
include_archived
boolean · default False
Include interviews the researcher has archived.
completed_only
boolean · default False
Return only interviews that reached the end of the study (completed_at is set).
since
datetime
Return only interviews created at or after this ISO 8601 UTC timestamp.
page_size
integer · default 50
Number of interviews per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list of interview rows, ordered newest-first.
Field
Type
Description
data[].id
uuid
Interview identifier, used as interview_id everywhere else in this chapter.
data[].name
string, nullable
Display name of the interview, when one was assigned.
data[].type
string
One of USER, SYNTH, TEST, IMPRT.
data[].started_at
datetime, nullable
When the interview was created, i.e. when the participant entered.
data[].completed_at
datetime, nullable
When the participant reached the end of the study; null if they never finished.
data[].is_active
boolean
Whether the session is still live.
data[].progress_percentage
float
Share of the study the participant got through, as a fraction between 0 and 1 (0.42 means 42%).
data[].screened_out
boolean, nullable
Whether the screener rejected this participant; null before the screener resolves.
data[].is_fraud
boolean
Whether fraud detection rejected this interview.
data[].is_low_quality
boolean
Whether quality checks marked the responses too poor to count.
data[].language_code
string, nullable
Language the interview was conducted in, as an IETF code such as en or pt-BR.
data[].fraud_score
float, nullable
Fraud-model score for the session; null when no score was produced.
data[].total_engagement_time_seconds
float, nullable
Total seconds the participant was engaged across all answers — thinking, speaking, and typing time, not media length.
data[].archived
boolean
Whether the researcher archived this interview.
data[].status
string
Human-readable rollup, first match wins: Fraud detected, Screened out, Over quota, Incomplete, Completed, Active.
data[].url
string
Deep link to this interview in the Outset app, of the form /project/{project_id}/interview/{interview_id}/read — it carries the project the interview sits in, not the study it belongs to.
An unknown interview_types code, only_screened_in combined with only_screened_out, an unparseable since, or a boolean parameter that is not true/false/1/0.
404
not_found
The study is outside the credential's organization or granted workspaces.
Interviews whose PII scan has not finished are omitted from this list entirely, so a study can report fewer interviews here than its recruitment numbers suggest for a few minutes after a wave completes. Interviews awaiting PII review are listed — their masking happens when you fetch the transcript.
Lists interviews across every study in a project. Identical filters, ordering, pagination, and row shape to the study-scoped list.
Path parameters
Name
Type
Description
project_id
uuid
Identifier of the project whose interviews to list.
Query parameters
Name
Type
Description
interview_types
string (repeatable) · default USER
Interview types to include: USER, SYNTH, TEST, IMPRT.
only_screened_in
boolean · default False
Return only interviews that passed the screener and count as valid completes.
only_screened_out
boolean · default False
Return only interviews the screener rejected; cannot be combined with only_screened_in.
include_archived
boolean · default False
Include interviews the researcher has archived.
completed_only
boolean · default False
Return only interviews that reached the end of their study.
since
datetime
Return only interviews created at or after this ISO 8601 UTC timestamp.
page_size
integer · default 50
Number of interviews per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list of interview rows in the same shape as the study-scoped list.
Field
Type
Description
data[].id
uuid
Interview identifier, used as interview_id everywhere else in this chapter.
data[].name
string, nullable
Display name of the interview, when one was assigned.
data[].type
string
One of USER, SYNTH, TEST, IMPRT.
data[].started_at
datetime, nullable
When the interview was created, i.e. when the participant entered.
data[].completed_at
datetime, nullable
When the participant reached the end of the study; null if they never finished.
data[].is_active
boolean
Whether the session is still live.
data[].progress_percentage
float
Share of the study the participant got through, as a fraction between 0 and 1 (0.42 means 42%).
data[].screened_out
boolean, nullable
Whether the screener rejected this participant; null before the screener resolves.
data[].is_fraud
boolean
Whether fraud detection rejected this interview.
data[].is_low_quality
boolean
Whether quality checks marked the responses too poor to count.
data[].language_code
string, nullable
Language the interview was conducted in, as an IETF code such as en or pt-BR.
data[].fraud_score
float, nullable
Fraud-model score for the session; null when no score was produced.
data[].total_engagement_time_seconds
float, nullable
Total seconds the participant was engaged across all answers — thinking, speaking, and typing time, not media length.
data[].archived
boolean
Whether the researcher archived this interview.
data[].status
string
Human-readable rollup, first match wins: Fraud detected, Screened out, Over quota, Incomplete, Completed, Active.
data[].url
string
Deep link to this interview in the Outset app, of the form /project/{project_id}/interview/{interview_id}/read — it carries the project the interview sits in, not the study it belongs to.
The project is outside the credential's organization or granted workspaces.
Rows carry no study identifier of their own, and url is project-scoped rather than study-scoped, so it cannot attribute an interview to its study either — pair this with the study-scoped list when you need that attribution.
Returns one interview with its full ordered transcript, the screener answers the participant gave, and any disqualification causes. Closed-ended answers carry structured values on the message that answered them, so you do not have to parse text to get selected options, numeric ratings, or matrix picks.
Path parameters
Name
Type
Description
interview_id
uuid
Identifier of the interview to fetch.
Query parameters
Name
Type
Description
include_recordings
boolean · default False
Include short-lived presigned URLs for message recordings and participant uploads; off by default because each file costs a signing round-trip.
Response
Single-object envelope carrying the interview row, its messages, and its screener answers. The example below shows one array element of each; interview object fields: see List a study's interviews.
Field
Type
Description
data.interview
object
Interview row in the same shape as the list endpoints.
data.messages[].id
uuid
Message identifier, used as message_id when correcting or flagging it.
data.messages[].order
integer
Position of the message in the transcript.
data.messages[].role
string
Who produced the message: PARTICIPANT, INTERVIEWER, or SYSTEM.
data.messages[].content
string
Message text; reads "[PII Under Review]" while a flag is unresolved and "[PII Redacted]" once redaction has run.
data.messages[].created_at
string
ISO 8601 UTC timestamp of when the message was recorded.
data.messages[].is_end
boolean
Whether this message closed the interview.
data.messages[].question_id
uuid, nullable
Study question this message answers or asks; null for messages outside the guide.
data.messages[].engagement_time_seconds
float, nullable
Seconds the participant was connected before sending this message — thinking, speaking, and typing time.
data.messages[].is_redacted
boolean
Whether the content was permanently redacted after a reviewer accepted a PII flag.
data.messages[].recording
object, nullable
Recording metadata as {duration_seconds, media_type, url}; url appears only with include_recordings=true and only when the media is cleared of PII.
data.messages[].participant_uploads[]
array
Files the participant uploaded on this message as {id, content_type, is_redacted, url}; url is never present for a redacted upload.
data.messages[].participant_skipped
boolean
Whether the participant deliberately skipped an optional question.
data.messages[].selected_options[]
array
Options the participant picked on a closed-ended question, in pick order (rank order for stack-rank questions), each as {id, text, custom_text}.
data.messages[].matrix_selections[]
array, nullable
Per-row picks for a matrix answer as {row_question_id, option_id}; null for non-matrix messages.
data.messages[].numeric_value
float, nullable
Numeric answer to a rating or number question; null when the message is not a plain numeric answer.
data.messages[].task_status
string, nullable
Task outcome (COMPLETE or INCOMPLETE) for task questions; null for other messages.
data.messages[].task_duration_seconds
integer, nullable
Seconds the participant spent on the task; null for other messages.
data.screener_answers[].question_id
uuid
Screener question that was answered.
data.screener_answers[].question_text
string
Text of the screener question as the participant saw it.
data.screener_answers[].selected_options[]
array
Options picked, each as {id, text}; options the researcher has since deleted still appear here so answer history stays readable.
data.screener_answers[].text
string
Free-text answer, empty when the question was closed-ended.
data.screener_answers[].participant_skipped
boolean
Whether the participant skipped this screener question.
data.screener_answers[].image_upload
object, nullable
Image the participant uploaded to an image-upload screener question as {id, content_type, status, meets_criterion, analysis, is_redacted, presigned_image_url}; null until the vision pass completes.
data.disqualification_causes
object, nullable
Free-form record of why the participant was disqualified, passed through as stored.
{
"data": {
"interview": {
"id": "0a4f7c62-9d31-4b8e-8a05-2f6d1c93b774",
"status": "Completed",
"…": "remaining interview row fields"
},
"messages": [
{
"id": "c40b25de-1a97-4f68-8d33-9b7e5c10a2f6",
"order": 5,
"role": "PARTICIPANT",
"content": "About twice a month, usually for bulk items.",
"created_at": "2026-07-28T14:06:41Z",
"question_id": "d21a8f36-77b4-4c0e-b5a9-3ef1c8d92057",
"is_redacted": false,
"recording": null,
"…": "remaining message fields, null or empty on this message"
},
"… one entry per turn, in transcript order"
],
"screener_answers": [
{
"question_id": "5f8e3c21-40db-4a76-91c5-6d2b7e0a34c9",
"question_text": "Which of these do you buy at least monthly?",
"selected_options": [
{
"id": "9c2d61b8-7e04-4f3a-a1d5-08b6c93e457f",
"text": "Fresh produce"
}
],
"text": "",
"participant_skipped": false
},
"… one entry per screener question"
],
"disqualification_causes": null
}
}
Errors
Status
Code
When
404
not_found
The interview is outside the credential's reach, its PII scan has not completed, or it is still live on a PII-enabled study — all three answer identically by design.
Presigned media URLs expire within minutes: fetch the bytes immediately rather than storing the URL. The transcript is returned in the language the interview was conducted in; translated transcripts are available through the export surface.
Corrects the text of a single transcript message — the surface for fixing transcription errors and typos. The pre-edit text is snapshotted so the correction can be undone, and the message is marked as edited. Participant, interviewer, and system messages can all be corrected.
Path parameters
Name
Type
Description
interview_id
uuid
Identifier of the interview the message belongs to.
message_id
uuid
Identifier of the message to correct.
Request body
The replacement text. `text` is the only editable field.
Field
Type
Description
textrequired
string
Corrected text, replacing the message's existing text in full.
{
"text": "About twice a month, usually for bulk items."
}
Response
Single-object envelope carrying the corrected message.
Field
Type
Description
data.id
uuid
Identifier of the message.
data.interview_id
uuid
Interview the message belongs to.
data.text
string
Text now stored on the message.
data.edited
boolean
Whether the message carries a researcher correction, which the app surfaces to viewers.
{
"data": {
"id": "c40b25de-1a97-4f68-8d33-9b7e5c10a2f6",
"interview_id": "0a4f7c62-9d31-4b8e-8a05-2f6d1c93b774",
"text": "About twice a month, usually for bulk items.",
"edited": true
}
}
Errors
Status
Code
When
400
pii_masked
The message has an unresolved PII flag. Editing it would write through content that is masked from every read surface, so resolve the flag first.
404
not_found
The interview is out of reach, or the message does not belong to that interview.
Sending text identical to what is already stored is a no-op — the message keeps its existing edited state and no new history snapshot is written. A correction invalidates derived artifacts built from the old text (translations of the message and burned captions on its recording), which regenerate asynchronously; a caption or translated transcript fetched immediately after may still show the pre-correction wording.
Restores a corrected message to the text the participant or interviewer originally produced and clears its edited marker.
Path parameters
Name
Type
Description
interview_id
uuid
Identifier of the interview the message belongs to.
message_id
uuid
Identifier of the message to revert.
Response
Single-object envelope carrying the reverted message, in the same shape as a correction.
Field
Type
Description
data.id
uuid
Identifier of the message.
data.interview_id
uuid
Interview the message belongs to.
data.text
string
Text now stored on the message.
data.edited
boolean
Whether the message carries a researcher correction, which the app surfaces to viewers.
{
"data": {
"id": "c40b25de-1a97-4f68-8d33-9b7e5c10a2f6",
"interview_id": "0a4f7c62-9d31-4b8e-8a05-2f6d1c93b774",
"text": "about twice a month usually for bulk items",
"edited": false
}
}
Errors
Status
Code
When
400
no_previous_version
The message was never corrected, so there is no original to restore.
400
pii_masked
The message has an unresolved PII flag; reverting would echo back masked content.
Reverting always restores the first pre-edit snapshot, not the previous one — a message corrected several times returns to the text it started with in a single call.
Raises a PII flag on a participant message, putting it into review. The flag is created OPEN, and the message is masked as "[PII Under Review]" on every read surface until the flag is reviewed.
Path parameters
Name
Type
Description
interview_id
uuid
Identifier of the interview the message belongs to.
message_id
uuid
Identifier of the participant message to flag.
Response
Single-object envelope carrying the newly created flag.
Field
Type
Description
data.id
uuid
Flag identifier, used to review the flag.
data.interview_id
uuid
Interview the flagged message belongs to.
data.message_id
uuid
Message the flag was raised on.
data.status
string
Lifecycle state of the flag; always OPEN on creation.
PII flagging is not enabled on the study, the message is not a participant message, or the message is already flagged — a message can only carry one flag.
404
not_found
The interview is out of reach, or the message does not belong to that interview.
Flagging takes effect immediately for readers: the message is masked from the moment the flag exists, before anyone reviews it. Keep the returned flag id — it is the only handle to the flag, and it is not carried on interview or message reads. GET /v2/pii-flags/{flag_id}/ reads its state back.
Resolves a PII flag: accept it to redact the flagged content, dismiss it to keep the content as-is, or reopen a dismissed flag for another look. The call records who reviewed the flag and when.
Path parameters
Name
Type
Description
flag_id
uuid
Identifier of the PII flag to review.
Request body
The review decision.
Field
Type
Description
actionrequired
string
One of accept (redact the flagged content), dismiss (keep it and unmask the message), or reopen (return a dismissed flag to review).
{
"action": "accept"
}
Response
Single-object envelope carrying the flag's new state.
Field
Type
Description
data.id
uuid
Identifier of the reviewed flag.
data.status
string
New lifecycle state: OPEN, DISMISSED, ACCEPTED (redaction queued or running), RESOLVED (redaction complete), or FAILED (redaction failed).
The decision is not legal from the flag's current state. An OPEN flag can be accepted or dismissed, a DISMISSED flag can only be reopened, a FAILED flag can only be accepted (retrying redaction), and a RESOLVED flag is terminal. A flag whose category the organization configured as delete-only can never be dismissed.
404
not_found
The flag is outside the credential's organization or granted workspaces.
Accepting is asynchronous and irreversible. The call returns immediately with status ACCEPTED while redaction runs in the background; poll GET /v2/pii-flags/{flag_id}/ to see it settle at RESOLVED or FAILED. On success the message content also reads "[PII Redacted]" with is_redacted: true on the interview transcript. The original text is destroyed — no surface, export, or support path can recover it, so treat accept as a permanent deletion of that content. Dismissing unmasks the message and regenerates the interview's summary. The route is top-level rather than nested under the interview because a flag id is globally unique and resolves its own interview, study, and workspace.
Reads one flag's current state. Redaction runs in the background after a flag is accepted, so this is the surface that reports whether it finished (RESOLVED) or failed (FAILED).
Path parameters
Name
Type
Description
flag_id
uuid
Identifier of the PII flag to read.
Response
Single-object envelope carrying the flag, in the same shape the flag-creation call returns.
Field
Type
Description
data.id
uuid
Flag identifier.
data.interview_id
uuid
Interview the flagged message belongs to.
data.message_id
uuid
Message the flag was raised on.
data.status
string
Lifecycle state: OPEN, DISMISSED, ACCEPTED (redaction queued or running), RESOLVED (redaction complete), or FAILED (redaction failed).
The flag is outside the credential's organization or granted workspaces.
A FAILED flag leaves the message masked as "[PII Under Review]" — accept it again to retry redaction. This read is the only way to see that state, because a failed redaction never changes the transcript.
Lists the observations Outset's vision pipeline derived from one interview's screen and camera recordings — moments of success, friction, or unexpected behavior, each anchored to a timestamp window in the recording. Only interviews with a completed vision analysis return rows.
Path parameters
Name
Type
Description
interview_id
uuid
Identifier of the interview whose observations to list.
Query parameters
Name
Type
Description
observation_type
string
Restrict to one kind of observation: success, friction, or unexpected.
page_size
integer · default 50
Number of observations per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list of observations, ordered by question and then by their position in the recording.
Field
Type
Description
data[].id
uuid
Observation identifier.
data[].title
string
Short label for what was observed.
data[].description
string
Full description of the observed behavior.
data[].observation_type
string
One of success, friction, or unexpected.
data[].ease_of_use
integer, nullable
Observed ease of use on a 1 (very difficult) to 5 (very easy) scale; null when the pipeline did not rate it.
data[].surface_area
string
Part of the tested experience the observation is about, as named by the pipeline; empty when unattributed.
data[].question_id
uuid, nullable
Study question the participant was working on; null when the observation is not tied to one.
data[].interview_id
uuid
Interview the observation came from.
data[].start_timestamp_seconds
integer
Seconds into the recording where the observed behavior starts.
data[].end_timestamp_seconds
integer
Seconds into the recording where the observed behavior ends.
{
"data": [
{
"id": "e8137a45-90cb-4d26-b7f1-2a5c0d6e39b8",
"title": "Could not find the filter control",
"description": "The participant scrolled the results list twice looking for a way to narrow by size before opening the sort menu by mistake.",
"observation_type": "friction",
"ease_of_use": 2,
"surface_area": "Search results",
"question_id": "d21a8f36-77b4-4c0e-b5a9-3ef1c8d92057",
"interview_id": "0a4f7c62-9d31-4b8e-8a05-2f6d1c93b774",
"start_timestamp_seconds": 142,
"end_timestamp_seconds": 171
}
],
"next_cursor": null,
"has_more": false
}
Errors
Status
Code
When
404
not_found
The interview is out of reach, still live on a PII-enabled study, or has unresolved PII — observations derive from participant recordings, so any unsettled PII state withholds them.
The PII gate here is stricter than the transcript's: an interview merely awaiting PII review returns 404 rather than a partially masked list, because an observation describes recorded behavior that cannot be masked message-by-message. An empty list is normal — it means the interview had no screen or camera recording to analyze, or the analysis found nothing notable.
This is where fielded research turns into answers. When a study's interviews complete, Outset analyzes them into a report — an executive topline, a per-question breakdown with categories and quotes, and, depending on what the study captured, emotion, vision, prototype-interaction, concept-testing and participant-upload layers on top. A report is an immutable point-in-time snapshot: every read below is a stable artifact you can cite, cache, and reconcile against, and nothing here mutates analysis output. The one write is a rerun, which regenerates the snapshot from the latest interviews. Crosstabs, emotion aggregates, prototype interaction maps, research-goal conclusions and concept reads are first-class endpoints here, not fields buried inside a page payload.
Lists the reports generated for one project, including the automatically maintained insights report every project gets once it has enough completed interviews. This is the usual entry point: resolve a report id here, then read its topline, questions, and answers.
Path parameters
Name
Type
Description
project_id
uuid
Identifier of the project.
Query parameters
Name
Type
Description
page_size
integer · default 50
Number of reports per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list of the project's reports, newest first.
Field
Type
Description
data[].id
uuid
Report identifier. It changes every time the report is rerun, because a rerun replaces the snapshot.
data[].name
string
Display name of the report.
data[].project_id
uuid
Project the report belongs to.
data[].study_ids
array[uuid]
Studies whose interviews this report analyzes; more than one when the report spans a project.
data[].is_insights_report
boolean
Whether this is the project's automatically generated and refreshed insights report.
data[].is_filtered
boolean
Whether the report covers a filtered subset of interviews rather than all of them.
data[].created_at
datetime
When this snapshot was created.
data[].last_run_at
datetime
When the analysis behind this snapshot last finished writing.
data[].latest_run
object
In-flight or failed run for this report, or null when nothing is running; see the run resource for its fields.
data[].url
url
Deep link to the report in the Outset web app, for a human hand-off.
Report ids are not stable across runs. A rerun deletes the previous snapshot and writes a new one with a new id, so an integration that stores a report id should re-resolve it from this list after any run completes rather than treating a later 404 as an error.
Lists every report the credential can reach across the organization, newest first. Use it to sweep an estate for freshly completed analysis instead of walking projects one at a time.
Query parameters
Name
Type
Description
workspace_id
uuid
Return only reports whose project lives in this workspace.
updated_since
datetime
Return only reports whose analysis last finished at or after this ISO 8601 timestamp.
page_size
integer · default 50
Number of reports per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated list of reports in the same row shape as the project-scoped list.
Field
Type
Description
data[].id
uuid
Report identifier. It changes every time the report is rerun, because a rerun replaces the snapshot.
data[].name
string
Display name of the report.
data[].project_id
uuid
Project the report belongs to.
data[].study_ids
array[uuid]
Studies whose interviews this report analyzes; more than one when the report spans a project.
data[].is_insights_report
boolean
Whether this is the project's automatically generated and refreshed insights report.
data[].is_filtered
boolean
Whether the report covers a filtered subset of interviews rather than all of them.
data[].created_at
datetime
When this snapshot was created.
data[].last_run_at
datetime
When the analysis behind this snapshot last finished writing.
data[].latest_run
object
In-flight or failed run for this report, or null when nothing is running; see the run resource for its fields.
data[].url
url
Deep link to the report in the Outset web app, for a human hand-off.
updated_since is the polling primitive for a sync integration: reports do not emit webhooks today, so a periodic sweep filtered on last-run time is the supported way to notice new analysis.
Returns one report's metadata, executive summary, and the list of analyzed questions with their headlines, summaries, and answer counts. Every other endpoint in this chapter is reached from the ids in this response.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Response
The report, its questions, and the studies it covers.
Field
Type
Description
data.id
uuid
Report identifier.
data.name
string
Display name of the report.
data.project_id
uuid
Project the report belongs to.
data.study_ids
array[uuid]
Studies whose interviews this report analyzes.
data.summary
string
AI-written executive summary of the whole report; empty until the summary stage has run.
data.interview_count
integer
Number of interviews included in this snapshot.
data.average_interview_duration_seconds
integer
Mean recorded duration of the included interviews, in seconds.
data.is_filtered
boolean
Whether the report covers a filtered subset of interviews rather than all of them.
data.filters
object
The filter set this snapshot was built with; null for an unfiltered report.
data.emotion_analysis_enabled
boolean
Whether any covered study captured emotion analysis, i.e. whether the emotion endpoints will return data.
data.vision_analysis_enabled
boolean
Whether any covered study captured vision analysis, i.e. whether the vision endpoints will return data.
data.created_at
datetime
When this snapshot was created.
data.last_run_at
datetime
When the analysis behind this snapshot last finished writing.
data.latest_run
object
In-flight or failed run for this report, or null when nothing is running.
data.url
url
Deep link to the report in the Outset web app.
data.questions[].id
uuid
Report question identifier, used as report_question_id everywhere below.
data.questions[].question_text
string
The question as participants saw it; for a report spanning several studies this is the merged wording.
data.questions[].headline
string
One-line AI headline answering this question; null before the summary stage runs.
data.questions[].summary
string
Paragraph-length AI summary of how participants answered; null before the summary stage runs.
data.questions[].source
string
How the question was analyzed: TRANSCRIPT_WIDE, STUDY_QUESTION, MULTIPLE_CHOICE, or PARTICIPANT_UPLOAD.
data.questions[].answer_count
integer
Number of analyzed participant answers behind this question.
{
"data": {
"id": "9d41c2a7-6b0e-4f38-a1c5-70e2b8d4f911",
"name": "Spring refresh — packaging concepts",
"project_id": "b41e7f28-0a6d-4c93-8e5b-2f9d1c604a77",
"study_ids": [
"3a7f5c19-24bd-4e08-9c6a-11d3f0e7b452"
],
"summary": "Shoppers read matte finishes as premium but associate the green band with value ranges, which undercuts the freshness cue the redesign is built on.",
"interview_count": 214,
"average_interview_duration_seconds": 612,
"is_filtered": false,
"filters": null,
"emotion_analysis_enabled": true,
"vision_analysis_enabled": false,
"created_at": "2026-08-09T04:11:07Z",
"last_run_at": "2026-08-09T04:38:52Z",
"latest_run": null,
"url": "https://app.outset.ai/project/b41e7f28-0a6d-4c93-8e5b-2f9d1c604a77/insights",
"questions": [
{
"id": "5e0b8a31-97c4-4d2f-b6e8-3a1f7c05d288",
"question_text": "What does this packaging tell you about the product inside?",
"headline": "Freshness reads from the window, not the color band",
"summary": "Most participants used the transparent window as their freshness cue and treated the green band as a price signal instead.",
"source": "STUDY_QUESTION",
"answer_count": 198
}
]
}
}
Errors
Status
Code
When
404
not_found
The report is outside the credential's organization or workspace grant, or it was replaced by a more recent rerun — re-resolve the id from the report list.
Report authoring is deliberately not part of this API: which questions a report analyzes, its research objectives, and its filters are configured in the product. The API reads reports and reruns them.
Regenerates the report from the latest interview data so its findings, summaries, and topline reflect everything fielded since the last run. Returns 202 with a run resource to poll; the finished run names the new report id.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report to regenerate.
Request body
No body. Send an empty object.
Response
202 with the queued (or already-running) analysis run.
Field
Type
Description
data.id
uuid
Run identifier; poll the run resource with it.
data.status
string
QUEUED, RUNNING, COMPLETED, or FAILED.
data.was_already_running
boolean
True when a run was already in flight and this call joined it instead of starting a second one.
The report has no studies, no questions to analyze, or no completed non-test interviews yet. Retrying will not help until interviews complete.
403
permission_denied
The token holds `analytics:write` but the user's organization role has no editing permission. Reruns consume analysis credits, so they are gated on an editing role as well as the scope.
A rerun replaces the snapshot: when it completes, the previous report and every id inside it (report, questions, answers) are gone and the new run's report_id is the one to use. It is not additive and it is not reversible, so schedule it deliberately rather than on every poll. Calling it while a run is in flight is safe — you get the running one back with was_already_running: true rather than a second run. Report and analysis-template authoring is out of scope for this API; this endpoint refreshes an existing report, it never creates one.
Returns the state of one analysis run — queued, running with progress, or terminal. This is the poll target after a rerun, and the same object appears as latest_run on a report so you can pick up a run somebody else started.
Path parameters
Name
Type
Description
run_id
uuid
Identifier of the run, from a rerun response or a report's latest_run.
Response
The run's current state.
Field
Type
Description
data.id
uuid
Run identifier.
data.status
string
QUEUED, RUNNING, COMPLETED, or FAILED. A run that stalls or is canceled reports FAILED.
data.progress
float
Fraction of the analysis pipeline completed, between 0 and 1.
data.current_step
integer
Index of the pipeline stage currently running, from 0.
data.total_steps
integer
Number of pipeline stages in this run.
data.report_id
uuid
The report this run produced; null until the run reaches COMPLETED.
data.requested_by_email
string
Email of the person or credential owner who triggered the run; null for platform-scheduled refreshes.
Analysis takes minutes, not seconds, on a real study — poll every 15–30 seconds rather than tightly. A run resource stays readable for a period after it terminates so a poll loop that reconnects late still sees the outcome; after that the report's last_run_at is the record.
Returns the report's executive narrative — methodology, research objectives, key findings, and recommendations — as ordered sections plus a single rendered markdown document. This is the shortest path from a finished study to something a stakeholder can read.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Response
The topline summary as sections and as markdown.
Field
Type
Description
data.report_id
uuid
Report the summary belongs to.
data.title
string
Title of the summary; null when the report has none.
data.markdown
string
The whole summary rendered as one markdown document, title as H1 and each section as H2.
data.sections[].heading
string
Section heading, e.g. Methodology or Key Findings.
data.sections[].text
string
Section body. Participant quotes are inlined; a non-English quote is served with its English rendering alongside.
data.sections[].order
integer
Display order of the section within the summary, from 0.
{
"data": {
"report_id": "9d41c2a7-6b0e-4f38-a1c5-70e2b8d4f911",
"title": "Packaging refresh — what shoppers actually see",
"markdown": "# Packaging refresh — what shoppers actually see\n\n## Methodology\n\n214 completed interviews across one study…\n\n## Key Findings\n\nFreshness reads from the window, not the color band…",
"sections": [
{
"heading": "Methodology",
"text": "214 completed interviews across one study…",
"order": 0
},
{
"heading": "Key Findings",
"text": "Freshness reads from the window, not the color band…",
"order": 1
}
]
}
}
Errors
Status
Code
When
404
not_found
The report exists but has no topline summary — the run has not reached the summary stage, or the report is too small to summarize.
Returns the analyzed per-participant answers inside a report, each with its AI summary and assigned categories. Narrow with a question filter, a literal keyword, or a semantic query — a plain-language description that is embedded and matched against answers by meaning rather than wording.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Query parameters
Name
Type
Description
report_question_id
uuid
Restrict answers to one report question.
keyword
string
Case-insensitive substring match against the answer summary.
semantic_query
string
Free-text description matched against answers by meaning; results come back ordered by similarity rather than recency.
category
string
Return only answers tagged with this category, exactly as it appears in the report's category list.
page_size
integer · default 50
Number of answers per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated answers, newest first — or most-similar first when semantic_query is set.
Field
Type
Description
data[].id
uuid
Report answer identifier; pass it to the source-messages endpoint to get the underlying quotes.
data[].report_question_id
uuid
Report question this answer belongs to.
data[].interview_id
uuid
Interview the answer came from; null when the interview has since been deleted.
data[].summary
string
AI summary of what this participant said in answer to the question.
data[].categories
array[string]
Categories this answer is tagged with; empty when the answer is uncategorized.
{
"data": [
{
"id": "2f8e4b6c-71a9-4d05-83bf-6c9a0e17d523",
"report_question_id": "5e0b8a31-97c4-4d2f-b6e8-3a1f7c05d288",
"interview_id": "7b3c9d51-0e42-4a86-95f7-1d8b6a04c2e9",
"summary": "Reads the window as proof of freshness and ignores the color band entirely.",
"categories": [
"Freshness cues",
"Ignores color"
]
}
],
"next_cursor": "cD0yMDI2LTA4LTA5VDA0OjM4OjUyWg",
"has_more": true
}
Answers from interviews whose PII scan has not completed are withheld, and an answer that draws on a message with an unresolved PII flag is dropped while the rest of that interview's answers stay visible. A count taken here is therefore a count of what you are cleared to see, not a participation total — use the report's interview_count or the category summaries for denominators.
Returns the interview messages an answer was derived from, with the exact quotes pulled from each. This is the citation trail behind any summary or category in the report.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
answer_id
uuid
Identifier of the report answer.
Query parameters
Name
Type
Description
page_size
integer · default 50
Number of messages per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated messages and quotes behind the answer, in transcript order; `next_cursor` and `has_more` sit beside `data` on every page.
Field
Type
Description
data[].message_id
uuid
Interview message the quotes come from; resolvable against the interview transcript.
data[].quotes
array[string]
Verbatim participant quotes extracted from that message.
{
"data": [
{
"message_id": "1c7a94e5-38fd-4b02-90c6-5d7e2a1f4b38",
"quotes": [
"I could not tell which address it was going to ship to",
"so I closed the tab and tried again on my phone"
]
},
{
"…": "remaining messages omitted — transcript order"
}
],
"next_cursor": null,
"has_more": false
}
Individual messages carrying an unresolved PII flag are dropped from this list even when the answer itself is visible, so the quote set can be narrower than the summary implies.
Returns the per-category rollup for one report question: how many participants fall into each category and, for rating and stack-rank questions, the scores behind it. This is the quantitative backbone of a report — the numbers the charts on the insights page are drawn from.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
report_question_id
uuid
Identifier of the report question.
Response
One row per category on the question, in report order. Returned whole — bounded by the report, not paginated.
Field
Type
Description
data[].category
string
Category label as it appears in the report.
data[].participant_count
integer
Number of distinct participants whose answer falls into this category — participants, not answers, so a participant with several answers is counted once.
data[].percentage
float
Share of the question's participants in this category, rounded to two decimals.
data[].text
string
AI description of what the category represents; null when the category has none.
data[].score
float
Summed rank score for stack-rank questions; null for other question types.
data[].average
float
Mean numeric value for rating questions; null for other question types.
data[].max
float
Maximum possible per-participant score, used as the denominator for stack-rank percentages; null for other question types.
{
"data": [
{
"category": "Freshness cues",
"participant_count": 118,
"percentage": 59.6,
"text": "Mentions of transparency, seals, or dates as evidence the product is fresh.",
"score": null,
"average": null,
"max": null
},
{
"category": "Ignores color",
"participant_count": 47,
"percentage": 23.74,
"text": "Did not attribute meaning to the color band.",
"score": null,
"average": null,
"max": null
}
]
}
Percentages are computed against the participants who answered that question, not against everyone in the report, so a question shown to a subset does not read as low participation.
Replays every comparison configured on the report and returns its crosstab in one call — each question's categories pivoted against another question's categories, against the study an interview belongs to, or against a participant metadata key. This is the general crosstab surface behind the insights page's comparison view.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Response
One entry per report question that has a comparison configured. Returned whole — bounded by the report, not paginated.
Field
Type
Description
data[].base_report_question_id
uuid
Report question whose categories form the rows of the crosstab.
data[].comparison_type
string
Axis the base question is pivoted against: REPORT_QUESTION, STUDY, or METADATA.
data[].comparison_report_question_id
uuid
Report question supplying the columns, when comparison_type is REPORT_QUESTION; otherwise absent.
data[].comparison_metadata_key
string
Participant metadata key supplying the columns, when comparison_type is METADATA; otherwise absent.
data[].labels
array[string]
Column labels of the crosstab, in display order.
data[].cells[].label
string
Row label — a category of the base question.
data[].cells[].comparison_label
string
Column label — a category, study name, or metadata value.
data[].cells[].count
integer
Participants in this row-and-column cell.
data[].cells[].percentage
float
The cell's share of its column.
data[].cells[].study_id
uuid
Study the column represents, when comparison_type is STUDY; otherwise absent.
data[].total_respondents
integer
Distinct participants across the whole crosstab, for METADATA comparisons.
data[].comparison_respondent_counts
array[object]
Distinct participants per column, as {comparison_label, count} rows, for METADATA comparisons.
Cell counts are per category-and-column pair, so a participant who selected several options appears in more than one cell — never sum cells to derive a participant total, use total_respondents and comparison_respondent_counts instead. Which comparison each question carries is configured in the product; this endpoint reads the result, it does not set it.
Returns the report's matrix-style comparisons — concepts or questions as rows, attributes as columns, one summarized cell per intersection. This is the shape behind concept-wide and question-by-question overview tables.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Response
One entry per matrix on the report, in display order. Returned whole — bounded by the report, not paginated.
Field
Type
Description
data[].id
uuid
Matrix identifier.
data[].name
string
Display name of the matrix.
data[].type
string
QUESTION_BY_QUESTION_OVERVIEW, CONCEPT_WIDE_ANALYSIS, or USER_DEFINED.
data[].order
integer
Display order among the report's matrices, from 0.
data[].items[].id
uuid
Row identifier.
data[].items[].name
string
Row label — usually a concept or a question.
data[].attributes[].id
uuid
Column identifier.
data[].attributes[].name
string
Column heading, which a researcher may have renamed.
data[].cells[].item_id
uuid
Row this cell belongs to.
data[].cells[].attribute_id
uuid
Column this cell belongs to.
data[].cells[].report_question_id
uuid
Report question the cell summarizes; null when the cell is not question-backed.
data[].cells[].value
string
Cell contents — a short summary, a score, or a label depending on the matrix type.
data[].cells[].box_data
object
Distribution behind a scale cell (for example top-box and bottom-box shares); null when the cell is not scale-backed.
Pivots one question's dominant-emotion breakdown against a comparison axis — a participant metadata key, another question's categories, or the study an interview belongs to. Each participant is counted once per question, using the emotion that dominated their answer.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Query parameters
Name
Type
Description
report_question_idrequired
uuid
Report question whose dominant-emotion breakdown you want.
comparison_typerequired
string
Axis to split the breakdown by: METADATA, REPORT_QUESTION, or STUDY.
metadata_key
string
Required when comparison_type is METADATA: the participant metadata key to split by.
comparison_report_question_id
uuid
Required when comparison_type is REPORT_QUESTION: the question whose answer categories split the breakdown.
comparison_axis
string
Only valid with comparison_type REPORT_QUESTION; CATEGORY splits by the comparison question's categories.
Response
The pivot table, one cell per emotion-and-column pair.
Field
Type
Description
data.report_question_id
uuid
Report question the breakdown is for.
data.comparison_type
string
Axis the breakdown was split by, echoed back.
data.comparison_label
string
Human-readable name of the comparison axis, for chart labelling.
A participant with no recorded emotional signal on the question counts as neutral, matching how the product's charts read. Questions with no underlying guide question — research-objective rollups, for instance — have no per-participant emotion verdict and return an empty cell list rather than an error.
Returns the whole report's emotion rollup in one call: the dominant emotion, per-emotion counts, and shares for every question, and for every concept when the report covers concept testing. Use it for the report-level emotional picture; use the crosstab when you need one question split by a segment.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Response
The emotion aggregation, or null when no covered study captured emotion analysis.
Field
Type
Description
data.status
string
State of the aggregation: QUEUED, RUNNING, COMPLETED, or FAILED, plus NOT_STARTED when aggregation has never been attempted. Anything but COMPLETED means the numbers are not final.
data.total_participants
integer
Participants with an emotion verdict anywhere in the report.
data.question_breakdowns[].report_question_id
uuid
Report question the breakdown is for.
data.question_breakdowns[].dominant_emotion
string
Most common dominant emotion on this question.
data.question_breakdowns[].participant_count
integer
Participants counted in this breakdown.
data.question_breakdowns[].emotion_counts
array[object]
Participants per emotion, as {emotion, count} rows.
data.question_breakdowns[].emotion_percentages
array[object]
Share per emotion, as {emotion, percentage} rows.
data.concept_breakdowns[].concept_name
string
Concept the breakdown is for; present only on concept-testing reports.
data.concept_breakdowns[].dominant_emotion
string
Most common dominant emotion toward this concept.
data.concept_breakdowns[].participant_count
integer
Participants counted in this concept breakdown.
data.concept_breakdowns[].emotion_counts
array[object]
Participants per emotion for this concept, as {emotion, count} rows.
Emotion analysis runs on answer recordings, so a text-only study returns null here even when everything else in the report is complete. Check emotion_analysis_enabled on the report before treating an empty response as a failure.
Returns the individual emotional moments the analysis identified in participant answer recordings, each with the evidence behind it and its position in the recording. Use this when you need the moments themselves; use the emotion summary or crosstab for quantities.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Query parameters
Name
Type
Description
report_question_id
uuid
Restrict observations to one report question.
keyword
string
Case-insensitive match against observation titles and evidence.
emotion_label
string
One of anger, contempt, disgust, fear, happiness, sadness, surprise, neutral.
priority
string
How notable the moment is: low, medium, or high.
confidence
string
Model confidence in the reading: low, medium, or high.
page_size
integer · default 50
Number of observations per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated observations, newest first.
Field
Type
Description
data[].id
uuid
Observation identifier.
data[].title
string
Short label for the moment.
data[].evidence
string
What the participant said or did that supports the reading.
data[].emotion_label
string
The emotion read at this moment.
data[].priority
string
How notable the moment is: low, medium, or high.
data[].confidence
string
Model confidence in the reading: low, medium, or high.
data[].interview_id
uuid
Interview the moment occurred in.
data[].message_id
uuid
Interview message the moment sits in; null when it cannot be attributed to one.
data[].study_question_id
uuid
Study question being answered at the time; null when unattributed.
data[].start_seconds
float
Start of the moment within the answer recording, in seconds.
data[].end_seconds
float
End of the moment within the answer recording, in seconds.
Observations are derived from recordings, which are not independently PII-scanned — so every observation from an interview that is not fully PII-cleared is withheld, not masked. A participant missing here may simply be under review.
Returns the cross-participant vision aggregation for one question: clustered behavioral patterns with participant counts, plus the question's aggregate usability metrics — ease of use, task completion as judged by the AI against what participants said about themselves, and average task duration. This is the endpoint to use for quantities; the raw observations are a separate read.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
report_question_id
uuid
Identifier of the report question.
Response
The aggregation and its clustered patterns.
Field
Type
Description
data.status
string
State of the aggregation: QUEUED, RUNNING, COMPLETED, or FAILED, plus NOT_STARTED when aggregation has never been attempted. Anything but COMPLETED means the numbers are not final.
data.total_participants
integer
Participants who reached this question.
data.participants_with_observations
integer
Participants for whom the analysis found at least one notable moment.
data.avg_ease_of_use
float
Mean ease-of-use rating across observed participants; null when unmeasured.
data.ai_task_complete_count
integer
Participants the AI judged to have completed the task, with any researcher override applied; null when the question is not a task.
data.ai_task_incomplete_count
integer
Participants the AI judged not to have completed the task; null when the question is not a task.
data.self_report_complete_count
integer
Participants who said they completed the task; compare against the AI counts for the say-do gap.
data.self_report_incomplete_count
integer
Participants who said they did not complete the task.
data.avg_task_duration_seconds
float
Mean time on task in seconds; null when unmeasured.
data.insights[].title
string
Name of the behavioral pattern.
data.insights[].description
string
What participants did, and why it matters.
data.insights[].insight_type
string
friction_pattern, success_pattern, or unexpected_pattern.
data.insights[].participant_count
integer
Participants exhibiting this pattern.
data.insights[].total_participants
integer
Participants the pattern was assessed against, as the denominator.
data.insights[].avg_ease_of_use
float
Mean ease-of-use rating among participants in this pattern; null when unmeasured.
data.insights[].common_surface_areas
array[string]
Parts of the interface the pattern concentrated on.
data.insights[].representative_observation_ids
array[uuid]
Observation ids illustrating the pattern; resolve them through the observation search.
{
"data": {
"status": "COMPLETED",
"total_participants": 42,
"participants_with_observations": 37,
"avg_ease_of_use": 3.4,
"ai_task_complete_count": 24,
"ai_task_incomplete_count": 18,
"self_report_complete_count": 33,
"self_report_incomplete_count": 9,
"avg_task_duration_seconds": 96.2,
"insights": [
{
"title": "Checkout entry point missed on first pass",
"description": "Participants scanned the header for a cart affordance and only found the inline button after scrolling back.",
"insight_type": "friction_pattern",
"participant_count": 14,
"total_participants": 42,
"avg_ease_of_use": 2.6,
"common_surface_areas": [
"Header",
"Product detail"
],
"representative_observation_ids": [
"a19c7e35-4d80-4b12-9f6a-2c5e8d0b41f7"
]
}
]
}
}
A say-do gap is the point of the AI-versus-self-report counts: they are computed from different sources and are meant to disagree. The AI counts already reflect any researcher override recorded in the product, so they can move without the report being rerun.
Returns the individual usability moments the vision analysis identified during interviews — friction points, successes, and unexpected behaviors — with where in the interface and where in the recording each occurred.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Query parameters
Name
Type
Description
report_question_id
uuid
Restrict observations to one report question.
keyword
string
Case-insensitive match against observation titles and descriptions.
observation_type
string
One of success, friction, unexpected.
page_size
integer · default 50
Number of observations per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated observations, newest first.
Field
Type
Description
data[].id
uuid
Observation identifier.
data[].title
string
Short label for what happened.
data[].description
string
What the participant did, in the analysis's words.
data[].observation_type
string
success, friction, or unexpected.
data[].ease_of_use
integer
Ease-of-use rating attached to this moment; null when unrated.
data[].surface_area
string
Part of the interface the moment happened on; null when unattributed.
data[].interview_id
uuid
Interview the moment occurred in.
data[].study_question_id
uuid
Study question in play at the time; null when unattributed.
data[].start_seconds
integer
Start of the moment within the recording, in seconds.
data[].end_seconds
integer
End of the moment within the recording, in seconds.
Like emotion observations, these are recording-derived: any interview that is not fully PII-cleared is withheld wholesale rather than partially masked.
Returns one row per participant and task question: whether the AI judged the task complete, whether a researcher overrode that, and why the AI and the participant's own answer disagreed when they did. This is the per-participant detail behind the say-do gap.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Query parameters
Name
Type
Description
report_question_id
uuid
Restrict verdicts to one report question.
task_completed
boolean
Filter on the effective verdict — the researcher override when one exists, otherwise the AI's.
only_with_discrepancy
boolean · default False
Return only rows where the AI verdict disagreed with what the participant said about themselves.
page_size
integer · default 50
Number of verdicts per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
Cursor-paginated verdicts, newest first.
Field
Type
Description
data[].id
uuid
Verdict identifier.
data[].interview_id
uuid
Interview the verdict is for.
data[].study_question_id
uuid
Study question whose task was judged; null when unattributed.
data[].task_completed
boolean
The AI's raw verdict, before any researcher override.
data[].effective_task_completed
boolean
The verdict that counts: the researcher override when one exists, otherwise the AI's.
data[].has_researcher_override
boolean
Whether a researcher replaced the AI verdict.
data[].discrepancy_reason
string
Why the AI verdict and the participant's self-report disagreed; null when they agreed.
{
"data": [
{
"id": "0a53c9e4-71b6-4d28-95f1-c3e8b204a7d6",
"interview_id": "6e2b0a37-4c81-49d5-b7f3-1a90d5c8e264",
"study_question_id": "a7c30f18-4d92-4b6a-9f57-0e1b8c2d4a63",
"task_completed": false,
"effective_task_completed": true,
"has_researcher_override": true,
"discrepancy_reason": "Participant said they finished, but the order confirmation never rendered on screen."
},
{
"…": "remaining verdicts omitted — newest first"
}
],
"next_cursor": null,
"has_more": false
}
Returns the frozen interaction map for a question backed by a Figma prototype: per-screen click and rage-click geometry, dwell time, navigation confusion, the deterministic metric tiles the insights page shows, and the AI's per-screen narration. This is the richest analysis layer Outset produces and it has no equivalent anywhere else in the API.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
report_question_id
uuid
Identifier of the report question.
Response
The interaction map, its metric tiles, and its narration.
Field
Type
Description
data.participant_count
integer
Participants whose sessions are aggregated into the map.
data.screens[].id
string
Prototype screen identifier, stable across the other blocks in this response.
data.screens[].name
string
Screen name as it appears in the prototype.
data.screens[].image_url
url
Short-lived signed URL to the rendered screen image, to draw the heatmap over. Expires within minutes — fetch it when you render, do not persist it.
data.screens[].design_dimensions
object
Width and height of the screen image in design pixels; click coordinates share this space.
data.screens[].clicks
array[object]
Click points as {x, y} in design pixels, capped per screen on very high-traffic studies.
data.screens[].clicks_truncated
boolean
Whether the click list hit the per-screen cap; the density picture is unaffected, the tail of the session trail is.
data.screens[].rage_clicks
array[object]
Rapid repeated-click clusters, as centroid plus cluster size — computed from the full click set, never truncated.
data.screens[].dwell
object
Average time on this screen and how many sessions it was measured across; null when unmeasured.
data.screens[].navigation
object
How many sessions visited the screen and how many came back to it, the basis of the navigation-confusion reading; null when the screen saw no navigation.
data.metrics[].screen_id
string
Screen these metric tiles belong to.
data.metrics[].avg_dwell_ms
float
Average dwell in milliseconds; null when unmeasured.
data.metrics[].rage_click_count
integer
Total rage clicks on the screen.
data.metrics[].nav_confusion
string
Navigation-confusion reading for the screen: Low, Medium, or High; null when unmeasured.
data.findings[].screen_id
string
Screen the narration is about.
data.findings[].summary
string
One-paragraph read of what happened on this screen.
data.findings[].findings[].title
string
Short label for a specific finding on the screen.
data.findings[].findings[].detail
string
What participants did and where.
data.findings[].findings[].quotes
array[object]
Supporting participant quotes, as {text, participant} rows.
The question has no prototype interaction map — it is not a Figma-prototype question, or the prototype capture failed or is still in flight for this study.
The map is frozen into the report snapshot at run time, so it is stable and citable, but it only reflects the interviews included in that run — a rerun rebuilds it. Screen images are delivered as short-lived signed URLs; store the screen id, not the URL.
Returns one entry per research objective configured on the report, with the conclusion the analysis reached, the recommendations that follow, and the supporting themes. This is the answer to "did we learn what we set out to learn", separate from the per-question breakdown.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Response
The report's research objectives and their conclusions, in configured order. Returned whole — bounded by the report, not paginated.
Field
Type
Description
data[].id
uuid
Research-goal identifier within this report snapshot.
data[].objective
string
The research objective as written when the report ran.
data[].conclusions
array[string]
The conclusions the analysis reached against this objective, as ordered paragraphs.
data[].recommendations
array[string]
Recommended actions that follow from the conclusions.
data[].interviews_considered
integer
Interviews that contributed evidence to this objective.
data[].report_question_id
uuid
Report question generated for this objective, if any — read its answers for the underlying evidence.
data[].order
integer
Display order among the report's objectives, from 0.
data[].themes[].title
string
Supporting theme's name.
data[].themes[].summary
string
What the theme says, in the analysis's words.
data[].themes[].participant_count
integer
Participants whose answers support the theme.
data[].themes[].order
integer
Display order of the theme within its objective, from 0.
{
"data": [
{
"id": "b7e40c18-6a29-4f53-90d1-8c2e5b7a1f34",
"objective": "Find the steps where shoppers abandon checkout",
"conclusions": [
"Address entry is where most shoppers stall; the saved-address prompt arrives too late to help."
],
"recommendations": [
"Surface saved addresses before the form renders.",
"Keep the promo-code field expanded on the payment step."
],
"interviews_considered": 198,
"report_question_id": "5e0b8a31-97c4-4d2f-b6e8-3a1f7c05d288",
"order": 0,
"themes": [
{
"title": "Address entry stalls the flow",
"summary": "Shoppers retype addresses they had already saved, then lose confidence in the order.",
"participant_count": 63,
"order": 0
}
]
},
{
"…": "remaining objectives omitted — configured order"
}
]
}
Objectives are captured at run time, so a report keeps answering the objectives it was run against even after somebody edits them in the product — the wording here is the wording the analysis actually used.
Returns the concept groups a concept-testing report covers — each tested concept with the stimulus participants saw and, when emotion analysis ran, its emotional reception. Empty for a report with no concept testing.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Response
The report's concept groups, in display order. Returned whole — bounded by the report, not paginated.
Field
Type
Description
data[].id
uuid
Concept group identifier.
data[].name
string
Internal name of the concept group.
data[].display_name
string
Name as participants and readers see it.
data[].order
integer
Display order among the report's concept groups, from 0.
data[].concepts[].id
uuid
Concept identifier within the group.
data[].concepts[].name
string
Concept name.
data[].concepts[].fields
array[object]
The concept's defined fields and values, as {name, value} rows — the copy, price, or claim under test.
data[].concepts[].stimulus_url
url
Short-lived signed URL to the image or file participants were shown; null when the concept has no stimulus.
data[].emotion_breakdowns[].dominant_emotion
string
Most common dominant emotion toward this concept; present only when emotion analysis ran.
data[].emotion_breakdowns[].participant_count
integer
Participants counted in the concept's emotion breakdown.
Returns one concept group with every report question scoped to it — the per-concept view of the report, so a monadic design can be read concept by concept instead of question by question.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
concept_group_id
uuid
Identifier of the concept group.
Response
The concept group and its questions.
Field
Type
Description
data.concept_group
object
The concept group, in the same shape the list returns.
data.questions[].id
uuid
Report question identifier, scoped to this concept.
data.questions[].question_text
string
The question as participants saw it.
data.questions[].headline
string
One-line AI headline for this concept's answers; null before the summary stage runs.
data.questions[].summary
string
Paragraph-length AI summary for this concept's answers.
data.questions[].answer_count
integer
Analyzed answers behind the question for this concept.
{
"data": {
"concept_group": {
"id": "3d9a1c76-4b02-48e5-9f37-a1c6b05d287e",
"name": "packaging_v3",
"display_name": "Packaging refresh",
"order": 0
},
"questions": [
{
"id": "5e0b8a31-97c4-4d2f-b6e8-3a1f7c05d288",
"question_text": "What does this packaging tell you about the product inside?",
"headline": "Freshness reads from the window, not the color band",
"summary": "Participants used the transparent window as their freshness cue and treated the green band as a price signal.",
"answer_count": 74
},
{
"…": "remaining questions omitted — one per report question scoped to this concept group"
}
]
}
}
Question ids here are the same ids the report read returns, so category summaries, comparisons, and answer searches all accept them unchanged.
Returns the rollup for a participant-upload question — how many participants uploaded, what share passed AI validation, and every upload with its AI description and assigned tags.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
report_question_id
uuid
Identifier of the report question; it must be a participant-upload question.
Query parameters
Name
Type
Description
tag
string
Return only uploads carrying this AI-assigned tag. Repeatable.
page_size
integer · default 50
Number of uploads per page, up to 200.
cursor
string
Opaque cursor from the previous page's next_cursor.
Response
The question's upload rollup and rows. `data.uploads` is cursor-paginated, with `next_cursor` and `has_more` beside `data`; the rollup figures cover the whole question rather than the page.
Field
Type
Description
data.total_uploads
integer
Uploads submitted for this question across the report's interviews.
data.ai_validation_rate
float
Share of uploads that passed AI validation against the question's requirements, between 0 and 1.
data.uploads[].id
uuid
Upload identifier within this report snapshot.
data.uploads[].interview_id
uuid
Interview the upload came from.
data.uploads[].media_type
string
Kind of file uploaded: IMAGE, VIDEO, or DOCUMENT.
data.uploads[].content_type
string
MIME type of the uploaded file.
data.uploads[].file_url
url
Short-lived signed URL to the uploaded file; expires within minutes and must not be persisted.
data.uploads[].validation_status
string
PASSED when the upload met the question's requirements, FLAGGED when it did not, QUEUED while validation has not finished.
data.uploads[].tags
array[string]
AI-assigned tags describing what is in the upload.
data.uploads[].summary
string
AI description of the upload's contents; null when the analysis produced none.
data.uploads[].study_id
uuid
Study the uploading participant took part in — worth carrying on a report spanning several studies, since participant numbering restarts per study.
{
"data": {
"total_uploads": 142,
"ai_validation_rate": 0.87,
"uploads": [
{
"id": "e91c47b3-2d68-4a05-9713-5f0b8c2e6a4d",
"interview_id": "6e2b0a37-4c81-49d5-b7f3-1a90d5c8e264",
"media_type": "IMAGE",
"content_type": "image/jpeg",
"file_url": "https://media.outset.ai/uploads/e91c47b3…?X-Amz-Expires=900&…",
"validation_status": "PASSED",
"tags": [
"pantry shelf",
"competitor product"
],
"summary": "A pantry shelf with three cereal boxes, the study's brand on the left.",
"study_id": "3a7f5c19-24bd-4e08-9c6a-11d3f0e7b452"
},
{
"…": "remaining uploads omitted"
}
]
},
"next_cursor": "cD0yMDI2LTA4LTA5VDA0OjExOjA3Wg%3D%3D",
"has_more": true
}
Errors
Status
Code
When
400
validation_error
The report question is not a participant-upload question. Use the answer search for every other question type.
Uploads are not independently PII-scanned, so uploads from an interview that is not fully PII-cleared are withheld and the totals reflect only what you are cleared to see.
Enumerates every axis a report can be segmented on: the participant metadata keys and their observed values, the studies the report covers, and each question with the categories its answers were tagged with. Use it to discover valid values before filtering answers or requesting a crosstab, instead of guessing at keys.
Path parameters
Name
Type
Description
report_id
uuid
Identifier of the report.
Response
The report's filterable axes. Returned whole — bounded by the report, not paginated.
Field
Type
Description
data.metadata_items[].key
string
Participant metadata key present on this report's interviews — a valid metadata_key for the emotion crosstab.
data.metadata_items[].values
array[string]
Distinct values observed for that key.
data.study_items[].study_id
uuid
Study included in the report.
data.study_items[].study_name
string
Name of that study.
data.question_items[].report_question_id
uuid
Report question that can be filtered on.
data.question_items[].question_text
string
The question's wording.
data.question_items[].categories
array[string]
Categories its answers were tagged with — valid values for the answer search's category filter.
A highlight reel is a rendered video built from trimmed clips of participant recordings — the shareable artifact at the end of an analysis. Building one is a two-step flow: search a project's interviews for the clips worth keeping, then ask Outset to stitch a selection of them into one reel. Rendering is asynchronous, so creation answers 202 and you poll the reel until it reports COMPLETED, at which point its media, its automatically generated captions, and its poster frame are readable through short-lived presigned URLs. Reels can also be minted from an existing report — every quote tagged with one category of a question, one emotion drill-down, or a single emotion observation. Share links are one of two deliberate exceptions to short-lived URLs: a share link is a durable public URL anyone can open without signing in, and it stays valid until it expires or you revoke it.
Finds candidate clips across every interview in a project — the picker that feeds reel creation. Search semantically with vector_text, literally with keyword_text, or both together to narrow. Only answers that have a recording behind them are returned, since a clip without media cannot be rendered.
Path parameters
Name
Type
Description
project_id
uuid
Project whose interviews to search.
Query parameters
Name
Type
Description
vector_text
string
Natural-language description of the clips you want; it is embedded and matched by meaning rather than wording. Must be at least 10 characters after trimming.
keyword_text
string
Case-insensitive substring match over the clip text. No query syntax — it is a plain contains match.
study_ids
uuid, repeatable
Narrow the search to specific studies in the project. Every ID must belong to this project; an ID that does not is an error rather than a silent skip.
limit
integer · default 10
Maximum number of clips to return, between 1 and 50.
Response
Clips ranked by match quality, best first. This is a ranked search rather than a page of rows, so the response is a plain list with no cursor.
Field
Type
Description
data[].message_id
uuid
Interview message the clip is trimmed from — this is the ID you pass to reel creation.
data[].interview_id
uuid
Interview the clip came from, or `null` when the answer is not attributed to one.
data[].study_id
uuid
Study the interview belongs to.
data[].report_question_text
string
Text of the report question the clip answers, for showing the clip in context.
data[].excerpt
string
First 200 characters of what the participant said, as a preview of the clip.
data[].categories
array[string]
Categories the report assigned to this answer, or `null` when the answer was never categorized.
data[].similarity_score
float
Relevance of this clip to `vector_text`, higher is closer. Always `1.0` for a keyword-only search, where results are not ranked by meaning.
{
"data": [
{
"message_id": "b4f0d6a2-18c7-4f53-9a61-2d7e0c5b83f4",
"interview_id": "51c9e7b3-6a02-4d18-bf74-9e3a1c6d2085",
"study_id": "0d3b8f16-72ae-4c95-8b30-6f1c4a9d7e52",
"report_question_text": "What made you hesitate before checking out?",
"excerpt": "I got to the payment step and there was suddenly a shipping fee I hadn't seen anywhere, so I closed the tab.",
"categories": [
"Unexpected cost",
"Checkout friction"
],
"similarity_score": 0.71
}
]
}
Errors
Status
Code
When
400
validation_error
Neither `vector_text` nor `keyword_text` was given, `vector_text` is shorter than 10 characters, or a `study_ids` value does not belong to this project.
Clips are cut from recordings, which are not scanned for PII independently of their interview. Answers from an interview whose PII review has not cleared are therefore excluded from search results entirely, so a reel can never be built from unreviewed media.
Stitches selected clips into one reel, in the order you list them. Returns 202 with the reel in QUEUED — rendering runs in the background, so poll the reel until it reports COMPLETED before expecting media.
Path parameters
Name
Type
Description
project_id
uuid
Project the reel belongs to; every clip must come from an interview in this project.
Request body
The reel's name and the clips it contains.
Field
Type
Description
titlerequired
string
Name shown for the reel, and rendered as its title slide. Truncated at 255 characters.
message_idsrequired
array[uuid]
Interview messages to include, between 1 and 25, in the order they should play. Use the `message_id` values returned by highlight-clip search.
A message ID is not in this project, has no recording behind it, or comes from an interview whose PII review has not cleared. The response names the offending IDs; nothing is created.
Rendering costs transcoding time, and each call creates a new reel — a repeated request is a second reel, not a retry of the first. A reel is only ever as clean as its sources: a clip from an interview still under PII review is refused outright rather than dropped from the selection, so the reel you get always contains exactly the clips you asked for.
Returns the reel's rendering status, its media, its caption availability, and its live share links. This is the polling endpoint for every kind of reel — one created from clips, from a report question, or from a single emotion observation. Media URLs appear only once status is COMPLETED.
Path parameters
Name
Type
Description
reel_id
uuid
ID of the reel.
Response
The reel, its media, and its share links. Every URL in this payload except `share_links[].share_url` is a short-lived presigned URL — fetch the bytes promptly and re-read the reel instead of storing them.
Field
Type
Description
data.id
uuid
Reel ID.
data.project_id
uuid
Project the reel belongs to.
data.title
string
Name of the reel; empty for single-clip downloads, which render without a title slide.
data.type
enum
How the reel was built — `AI_PROJECT_HIGHLIGHT` (clip selection), `AI_C_HIGHLIGHT` (report question category), `REPORTQ_HIGHLIGHT` (report question), `OBS_HIGHLIGHT` (observation clip or emotion drill-down), `CODESIGN_HIGHLIGHT` (co-design edits), or `USER_GENERATED` (built by a researcher in the product).
data.status
enum
`QUEUED`, `RUNNING`, `COMPLETED`, `FAILED`, or `NO_RESULTS` when no clip matched what was asked for.
data.clips_count
integer
Number of clips stitched into the reel.
data.total_duration_seconds
float
Length of the rendered reel in seconds, or the sum of its clips' durations while it is still rendering.
data.created_at
datetime
When the reel was created (ISO 8601, UTC).
data.video_preview_url
string
Presigned URL for the reel's poster frame, or `null` when no thumbnail has been rendered.
data.captions.media_url
string
Presigned URL for the rendered reel video, or `null` until it completes.
data.captions.has_captioned_clip
boolean
Whether a second render with captions burned into the picture exists.
data.captions.captioned_media_url
string
Presigned URL for the burned-in captioned video, or `null` when there is none.
data.captions.has_captions_srt
boolean
Whether a standalone SRT subtitle file was generated for the reel.
data.captions.captions_srt_url
string
Presigned URL for the SRT sidecar, or `null` when there is none.
data.captions.available_caption_languages
array[string]
Language codes every contributing study can caption in — the intersection, so a mixed-language reel offers only what all of its studies share.
data.captions.media_withheld_for_pii_review
boolean
True when a source interview is under PII review; while it is, every media URL on the reel reads `null` even though the reel itself is `COMPLETED`.
data.captions.translated_renders[].language_code
string
Language of this translated caption render.
data.captions.translated_renders[].status
enum
`QUEUED`, `RUNNING`, `COMPLETED`, `FAILED`, or `STALE` when the reel was re-rendered and this language has not caught up.
data.captions.translated_renders[].media_url
string
Presigned URL for the translated render, or `null` until it completes.
data.share_links[].share_link_id
uuid
ID of the share link, used to revoke it.
data.share_links[].share_url
string
Durable public URL that opens the reel without signing in.
data.share_links[].expires_at
datetime
When the link stops working, or `null` for a link that never expires.
Captions are not a setting: an SRT sidecar and, where applicable, a burned-in render are produced automatically after the reel finishes, so a reel can read COMPLETED a short while before its caption fields fill in. Expired share links are omitted from share_links rather than listed as dead.
Lists the project's reels, newest first, including those researchers built in the product. Rows carry the same fields as the read endpoint, so this is how you discover reel IDs you did not create yourself.
Path parameters
Name
Type
Description
project_id
uuid
Project whose reels to list.
Query parameters
Name
Type
Description
status
enum
Return only reels in this state — `QUEUED`, `RUNNING`, `COMPLETED`, `FAILED`, or `NO_RESULTS`.
page_size
integer · default 50
Rows per page, up to 200.
cursor
string
Opaque cursor from the previous page's `next_cursor`.
Response
A cursor-paginated page of reels, newest first. Each element of `data[]` is a full reel object — its fields are listed below with the single-object `data.` prefix the read endpoint uses, and every URL in a row is presigned the same way.
Field
Type
Description
data.id
uuid
Reel ID.
data.project_id
uuid
Project the reel belongs to.
data.title
string
Name of the reel; empty for single-clip downloads, which render without a title slide.
data.type
enum
How the reel was built — `AI_PROJECT_HIGHLIGHT` (clip selection), `AI_C_HIGHLIGHT` (report question category), `REPORTQ_HIGHLIGHT` (report question), `OBS_HIGHLIGHT` (observation clip or emotion drill-down), `CODESIGN_HIGHLIGHT` (co-design edits), or `USER_GENERATED` (built by a researcher in the product).
data.status
enum
`QUEUED`, `RUNNING`, `COMPLETED`, `FAILED`, or `NO_RESULTS` when no clip matched what was asked for.
data.clips_count
integer
Number of clips stitched into the reel.
data.total_duration_seconds
float
Length of the rendered reel in seconds, or the sum of its clips' durations while it is still rendering.
data.created_at
datetime
When the reel was created (ISO 8601, UTC).
data.video_preview_url
string
Presigned URL for the reel's poster frame, or `null` when no thumbnail has been rendered.
data.captions.media_url
string
Presigned URL for the rendered reel video, or `null` until it completes.
data.captions.has_captioned_clip
boolean
Whether a second render with captions burned into the picture exists.
data.captions.captioned_media_url
string
Presigned URL for the burned-in captioned video, or `null` when there is none.
data.captions.has_captions_srt
boolean
Whether a standalone SRT subtitle file was generated for the reel.
data.captions.captions_srt_url
string
Presigned URL for the SRT sidecar, or `null` when there is none.
data.captions.available_caption_languages
array[string]
Language codes every contributing study can caption in — the intersection, so a mixed-language reel offers only what all of its studies share.
data.captions.media_withheld_for_pii_review
boolean
True when a source interview is under PII review; while it is, every media URL on the reel reads `null` even though the reel itself is `COMPLETED`.
data.captions.translated_renders[].language_code
string
Language of this translated caption render.
data.captions.translated_renders[].status
enum
`QUEUED`, `RUNNING`, `COMPLETED`, `FAILED`, or `STALE` when the reel was re-rendered and this language has not caught up.
data.captions.translated_renders[].media_url
string
Presigned URL for the translated render, or `null` until it completes.
data.share_links[].share_link_id
uuid
ID of the share link, used to revoke it.
data.share_links[].share_url
string
Durable public URL that opens the reel without signing in.
data.share_links[].expires_at
datetime
When the link stops working, or `null` for a link that never expires.
data.share_links[].is_expired
boolean
Whether the link has already passed its expiry.
{
"data": [
{
"id": "7c31a0e8-5d64-4b92-8f07-1a6e3c9d4b25",
"project_id": "2f8b41c9-0e76-4d3a-95b8-c70d51a6f284",
"title": "Checkout friction — top moments",
"type": "AI_PROJECT_HIGHLIGHT",
"status": "COMPLETED",
"clips_count": 2,
"total_duration_seconds": 74.3,
"created_at": "2026-08-11T09:14:22Z",
"…": "video_preview_url, captions, and share_links exactly as on the read endpoint"
},
"… one entry per reel, newest first"
],
"next_cursor": "cD0yMDI2LTA4LTExVDA5OjE0OjIyWg",
"has_more": true
}
Archived reels are omitted. Every row presigns its own media URLs, so keep page_size modest when you only need the IDs.
Mints a public URL that plays the reel for anyone who has it, with no sign-in. Set expires_at to have it stop working at a chosen time, or omit it for a link that never expires. Each call mints a new, independent link.
Path parameters
Name
Type
Description
reel_id
uuid
Reel to share.
Request body
Optional expiry for the link.
Field
Type
Description
expires_at
datetime
ISO 8601 datetime when the link stops working. Omit for a link that never expires; a naive timestamp is read as UTC.
{
"expires_at": "2026-09-30T00:00:00Z"
}
Response
The minted link.
Field
Type
Description
data.share_link_id
uuid
ID of the link, used to revoke it. It is also the bearer token embedded in the URL — treat it as a secret.
data.highlight_reel_id
uuid
Reel the link opens.
data.share_url
string
The public URL to hand out.
data.expires_at
datetime
When the link stops working, or `null` when it never expires.
This is one of the two places the API hands back a durable URL rather than a short-lived presigned one (the other is content images embedded in question text), and it grants unauthenticated access to participant video — anyone the URL reaches can watch the reel, and neither organization membership nor workspace scoping applies to them. Prefer an expires_at, and revoke a link the moment it has served its purpose.
Builds a reel from what a report already found for one question: pass category for every quote tagged with that category, or emotion for the clips behind one bar of the question's emotion breakdown. Returns 202 with a reel to poll. Exactly one of the two is required.
Path parameters
Name
Type
Description
report_id
uuid
Report the question belongs to.
report_question_id
uuid
Report question to build the reel from — the report question id carried by the report reads, not the id of a question in the study guide.
Request body
Which slice of the question to turn into a reel.
Field
Type
Description
category
string
A category the report assigned to this question, spelled exactly as the report has it. Every quote in that category goes into the reel.
emotion
enum
One of `anger`, `contempt`, `disgust`, `fear`, `happiness`, `sadness`, `surprise`, `neutral` — collects the clips behind that emotion for this question.
emotion_scope
enum
Set to `codesign_reactions` to collect reactions to AI-generated images instead of the question's overall emotion drill-down. Only valid alongside `emotion`.
title
string
Name for the reel. Defaults to the category and the question's headline; ignored for emotion reels, which render without a title slide.
prompt_text
string
Optional instruction steering which quotes in the category are picked. Only valid alongside `category`.
{
"category": "Unexpected cost",
"title": "Unexpected cost — what people actually said"
}
Response
The reel, in the same shape as the read endpoint. Poll it at `GET /v2/highlight-reels/{reel_id}/`.
Field
Type
Description
data
object
The reel, in the same shape `GET /v2/highlight-reels/{reel_id}/` returns: id, project_id, title, type, status, clips_count, created_at.
data.id
uuid
Reel ID.
data.status
enum
`QUEUED` for a fresh reel, or the current state of the reel that was reused.
data.type
enum
`AI_C_HIGHLIGHT` for a category reel, `OBS_HIGHLIGHT` for an emotion reel.
Neither or both of `category` and `emotion` were given, `emotion_scope` or `prompt_text` was paired with the wrong one, or the category is not one this report question carries — categories change when a report is regenerated, so re-read the report's categories before retrying.
An emotion reel is deduplicated: asking again for the same question, emotion, and scope returns the reel that already exists — 202 while it renders, 200 once it is complete — so polling by re-posting is safe. A category reel is not deduplicated; each call renders another one. The reel is a snapshot of the report at build time and does not follow later report reruns.
Renders the moment behind one emotion observation as a standalone trimmed clip, for pulling a single reaction out of an interview. Returns 202 with a reel of one clip to poll.
Path parameters
Name
Type
Description
report_id
uuid
Report the observation belongs to.
observation_id
uuid
Emotion observation to clip, from the report's emotion observations.
Response
The one-clip reel, in the same shape as the read endpoint.
Field
Type
Description
data
object
The reel, in the same shape `GET /v2/highlight-reels/{reel_id}/` returns: id, project_id, title, type, status, clips_count, created_at.
data.id
uuid
Reel ID; poll it at `GET /v2/highlight-reels/{reel_id}/`.
data.status
enum
`QUEUED` for a fresh render, or the current state of the clip that was reused.
Deduplicated per observation: repeating the request returns the existing clip — 202 while it renders, 200 once complete — so it doubles as a safe poll. The clip renders raw, with no title slide or overlays.
This is the "get everything out" surface: bulk answer, transcript, report, and image files, plus direct access to the recordings behind them. Anything that renders a file is asynchronous — you create an export job, poll it, and download the finished artifact from a short-lived presigned URL; anything already sitting in storage (a stitched interview recording, a single message's audio, aggregated screener counts) is a direct read. Every artifact type here is produced by Outset in production today, in the formats it already produces — CSV, JSON, ZIP, DOCX, and PPTX; there is no XLSX anywhere. The export-job API presents them behind one create-poll-download contract, so a partner writes the polling loop once. All of it reads participant output, so all of it needs analytics:read; the study-instrument export is study design and needs studies:read instead. PII gating applies below the API on every route here: interviews whose PII scan has not completed are omitted from exports entirely, messages under review render as [PII Under Review], and redacted content never leaves the platform.
Starts an export job over one study's interviews and returns 202 with the job to poll. The same filter, language, and translation options apply to every artifact type, so a CSV and a transcript ZIP of the same cohort are requested identically.
Path parameters
Name
Type
Description
study_id
uuid
The study whose interviews to export.
Request body
The artifact to render, plus the cohort and language to render it for.
Field
Type
Description
artifactrequired
string
What to render: `answers-csv` (one row per interview, one column group per question), `answers-json` (the same data as JSON), or `transcripts-zip` (one transcript file per interview, screener answers included inline).
language
string
Render participant text in this language, translating on demand where a translation does not exist yet; must be a ready (generated or reviewed) target language of the study, or its source language.
translated
boolean
Render participant text in the language the study was written in rather than the language each interview was conducted in; ignored when `language` is set. Defaults to false.
filters.only_completed
boolean
Include only interviews that finished — no fraud-flagged, archived, low-quality, or still-running sessions. Defaults to false.
filters.screened_out
boolean
Include only interviews that were screened out. Defaults to false.
filters.exclude_low_quality
boolean
Drop interviews Outset flagged as low quality. Defaults to false.
filters.exclude_test_interviews
boolean
Drop test and synthetic interviews, unless the study has nothing else — a study still in its pre-launch test phase exports its test interviews regardless. Defaults to false.
filters.include_archived
boolean
Include archived interviews, which are otherwise excluded. Defaults to false.
filters.interview_ids
array
Restrict the export to these interviews; omit to export every interview the other filters allow.
filters.metadata
array
Match interviews on the metadata you passed in at recruitment; each entry is `{"key": "", "values": ["", …]}`, matching any listed value for that key and requiring every entry to match.
The `artifact` is not one this endpoint renders — report-scoped artifacts go to `POST /v2/reports/{report_id}/exports/`.
400
language_not_available
The requested `language` is not a ready target language of this study, or its source language.
403
pii_export_blocked
The study's PII review is still open and the organization's policy blocks bulk export until it clears.
503
translation_unavailable
An on-demand translation into `language` could not be produced right now; the job is not created, so retry.
Filters are applied at kickoff, so a job started while the study is still fielding captures the cohort as of that moment — re-run the export to pick up later interviews. Two cohort rules are not negotiable through filters: interviews whose PII detection has not completed are always withheld, and the answers artifacts always exclude test interviews. In the product an export like this also emails the requester a link; that email is a convenience, not part of this API's contract — poll the job.
Starts an export job over one report — the analyzed, point-in-time snapshot of a study's answers — and returns 202 with the job to poll. A report can span several studies in a project, which is why report exports are addressed by report rather than by study.
Path parameters
Name
Type
Description
report_id
uuid
The report to export.
Request body
The artifact to render, and for image exports which questions to pull images from.
Field
Type
Description
artifactrequired
string
What to render: `report-csv` (one row per interview, a quotes/categories/summary column group per question), `report-json` (the same data as JSON, plus concept groups and comparison matrices when the report has them), `report-slides-pptx` (an auto-generated slide deck of the report), or `images-zip` (participant-uploaded images, foldered by question).
selections
array
For `images-zip` only: which questions to include, each `{"question_id": "", "variants": ["uploads"]}`; omit to be rejected rather than silently exporting everything.
The `artifact` is not one this endpoint renders — study-scoped artifacts go to `POST /v2/studies/{study_id}/exports/`.
400
validation_error
`images-zip` was requested without `selections`, or a selected question has no downloadable images.
404
not_found
The report does not exist, or is outside the credential's workspaces.
A report is an immutable snapshot: exporting one twice returns the same content, and interviews that completed after the report ran are not in it. To capture newer interviews, re-run the report first (Analysis & Insights chapter) and export the new one. images-zip covers participant uploads; co-design (AI image-edit) journeys are exported as a Word document from the co-design surface, not from here.
Starts one export job across a list of studies and returns 202 with the job to poll. The result is a single ZIP containing a per-study folder — the shape to use for a periodic "pull everything since last time" sync rather than one job per study.
Request body
The studies to bundle and what to include for each of them.
Field
Type
Description
study_idsrequired
array
The studies to include; every one must be reachable by the credential, and at least one is required.
include_answers
boolean
Include each study's answers file in its folder; at least one of `include_answers` and `include_transcripts` must be true.
include_transcripts
boolean
Include each study's per-interview transcripts in its folder.
only_completed
boolean
Restrict every study in the bundle to interviews that finished. Defaults to false.
language
string
Render participant text in this language for every study that offers it; a study that does not is exported in its own language rather than failing the bundle.
translated
boolean
Render participant text in each study's own written language rather than the language its interviews were conducted in; ignored when `language` is set. Defaults to false.
`study_ids` is empty, or neither `include_answers` nor `include_transcripts` is true.
404
not_found
At least one of the studies does not exist or is outside the credential's workspaces; the response names the ids it could not resolve.
The bundle is all-or-nothing on access — if any study in the list is unreachable, no job is created — but not on content: a study with no matching interviews still gets a folder, and a study that cannot serve the requested language falls back to its own language instead of failing the run. Bundles are the expensive shape here; a nightly sync of a large estate should page through studies rather than requesting hundreds in one job.
Returns an export job's current state, and once it reaches COMPLETED, a short-lived presigned URL to download the finished file. Poll every few seconds; large studies take minutes.
Path parameters
Name
Type
Description
export_id
uuid
The export job to read, as returned by any of the create endpoints.
Response
The export job as a single object.
Field
Type
Description
data.id
uuid
Export job identifier.
data.status
string
Job state: `QUEUED`, `RUNNING`, `COMPLETED`, or `FAILED`. Only the last two are terminal.
data.artifact
string
The artifact type this job renders.
data.study_id
uuid
The study the export covers, or null for report and bundle exports.
data.report_id
uuid
The report the export covers, or null for study and bundle exports.
data.study_ids
array
The studies a bundle export covers; empty for single-study and report exports.
data.requested_at
datetime
When the job was created (ISO 8601, UTC).
data.completed_at
datetime
When the job reached a terminal state, or null while it is still running.
data.file_name
string
Suggested file name for the artifact, or null before it exists.
data.content_type
string
MIME type of the artifact — `text/csv`, `application/json`, `application/zip`, or the Office type for a slide deck; null before it exists.
data.byte_size
integer
Size of the finished artifact in bytes, or null before it exists.
data.download_url
string
Presigned download URL, present only while the job is `COMPLETED` and the URL has not expired. Fetch it immediately and never store it.
data.download_url_expires_at
datetime
When the presigned URL stops working; re-read this endpoint to get a fresh one.
data.error.code
string
Machine-readable failure reason on a `FAILED` job, e.g. `translation_unavailable` or `render_failed`; null otherwise.
data.error.detail
string
Human-readable failure description on a `FAILED` job; null otherwise.
No such job, the job belongs to a study or report outside the credential's workspaces, or the finished artifact has passed its retention window and been deleted.
Finished artifacts are retained for a limited period and then deleted — a job id is not an archive, so download the file and keep your own copy. The presigned URL expires within minutes and is bound to the artifact, not to your credential: treat it as a secret, do not forward it, and re-read this endpoint if it lapses. A FAILED job is never retried automatically; create a new one.
Returns a presigned URL for the stitched MP4 of a whole interview session — every recorded turn in one file, as opposed to the per-message audio below. Not every interview has one: it is rendered on demand, so a NOT_GENERATED interview needs the POST on the same path first.
Path parameters
Name
Type
Description
interview_id
uuid
The interview whose full-session recording to read.
Response
The recording's availability and, when it is ready, where to fetch it.
Field
Type
Description
data.status
string
`READY` (downloadable), `RUNNING` (a render is in flight), `NOT_GENERATED` (none exists yet), or `WITHHELD` (it exists but the interview's PII review has not cleared). This describes artifact availability, not an async job.
data.download_url
string
Presigned URL for the stitched MP4, present only when the status is `READY`; expires within minutes and must not be persisted.
data.download_url_expires_at
datetime
When the presigned URL stops working, or null when there is no URL.
data.duration_seconds
float
Length of the stitched recording in seconds, or null before it is rendered.
The interview does not exist, is outside the credential's workspaces, or its PII detection has not completed — an unscanned interview is withheld rather than returned.
WITHHELD and 404 mean different things and both are normal: an interview awaiting PII review exists and is listable but its media is held back (WITHHELD), while an interview whose PII scan has not run at all is not returned by any read endpoint (404). Recordings are not redactable per message, so the whole file stays withheld until review clears — poll rather than treating it as a permanent failure.
Requests the stitched full-session MP4 for an interview that does not have one yet and returns 202. Stitching runs on the media pipeline, so poll the GET on the same path until it reads READY.
Path parameters
Name
Type
Description
interview_id
uuid
The interview to render a full-session recording for.
Response
The render request that was accepted.
Field
Type
Description
data.status
string
`RUNNING` when a render was started, or `READY` when one already existed and nothing was queued.
{
"data": {
"status": "RUNNING"
}
}
Errors
Status
Code
When
404
not_found
The interview does not exist, is outside the credential's workspaces, or its PII detection has not completed.
409
no_recorded_media
The interview has no recorded turns to stitch — a text-only interview never gets a session recording.
Calling this for an interview that already has a recording, or one whose render is in flight, is a no-op rather than a second render — it is safe to call unconditionally before polling. Rendering is billable machine time on long video interviews, so do not use repeated POSTs as a polling mechanism.
Redirects to a presigned URL for the raw audio of a single participant answer. Point a downloader at it directly — following redirects is all a client needs to do.
Path parameters
Name
Type
Description
interview_id
uuid
The interview the message belongs to.
message_id
uuid
The message whose recording to download.
Response
302 redirect — the Location header carries a short-lived presigned URL for the raw recording.
Errors
Status
Code
When
404
not_found
The message has no recording, the interview is outside the credential's workspaces, or the recording is withheld — because the message is redacted, its PII flag is unresolved, or the project hides message recordings.
The Location URL expires within minutes and must not be persisted — re-request this endpoint instead of storing it. This is per-message audio, not the session video: use GET /v2/interviews/{interview_id}/recording/ for one file covering the whole interview. The interview detail endpoint (Interviews chapter) can inline these URLs for a whole transcript in one call with include_recordings=true, which is cheaper than a redirect per message.
Requests burned-in captions, in one language, for every recorded answer in an interview, and returns 202. Burning runs on the media pipeline; read the per-message endpoint below to see which recordings are ready.
Path parameters
Name
Type
Description
interview_id
uuid
The interview to caption.
Request body
The caption language.
Field
Type
Description
languagerequired
string
Language to caption in; must be a ready target language of the study, or its source language.
{
"language": "fr"
}
Response
What the request queued.
Field
Type
Description
data.language
string
The language the captions are being produced in.
data.requested_count
integer
How many of the interview's recordings a caption burn was queued for.
data.ready_count
integer
How many already had captions in this language and were left alone.
The language is not one this study offers for download.
403
feature_not_enabled
Per-language captions are not enabled for the organization.
409
translation_not_current
The interview's transcript has no current translation in that language yet — read the transcript in that language first, then retry.
Captions embed the spoken text, so they inherit the transcript's privacy gates exactly: a message that is redacted or under PII review is skipped, and its caption artifacts stay withheld even after the rest of the interview finishes burning. Each language is burned once per recording and cached, so re-requesting an already-captioned language costs nothing.
Returns the caption languages this study can produce, plus the per-language state of this message's captioned video and a presigned URL for each finished burn. Poll it while a language reads RUNNING.
Path parameters
Name
Type
Description
interview_id
uuid
The interview the message belongs to.
message_id
uuid
The message whose captioned recordings to list.
Response
Available caption languages and this message's burns. Returned whole — the set is bounded by the study's caption languages, not paginated.
Field
Type
Description
data.available_languages
array
Language codes this study can caption in — its ready target languages plus its source language.
data.captions[].language
string
The language of this burn.
data.captions[].status
string
`QUEUED`, `RUNNING`, `COMPLETED`, or `FAILED`.
data.captions[].download_url
string
Presigned URL for the captioned MP4, present only on a `COMPLETED` burn whose media is not withheld for privacy review.
data.captions[].download_url_expires_at
datetime
When the presigned URL stops working, or null when there is no URL.
Per-language captions are not enabled for the organization.
404
not_found
The message does not exist in that interview, or the interview is outside the credential's workspaces.
A message with no captionable recording — no audio, redacted, or PII-unresolved — returns an empty captions list rather than an error, so an empty list is not a failure signal.
Requests a burned-in captioned video of a single answer in one language and returns 202. This is the "key quote" download — use it when you want one clip rather than captioning a whole interview.
Path parameters
Name
Type
Description
interview_id
uuid
The interview the message belongs to.
message_id
uuid
The message to caption.
Request body
The caption language.
Field
Type
Description
languagerequired
string
Language to caption in; must be a ready target language of the study, or its source language.
{
"language": "fr"
}
Response
The burn that was queued, in the same row shape the GET returns.
Field
Type
Description
data.language
string
The language of the burn.
data.status
string
`QUEUED` or `RUNNING` on a fresh request, `COMPLETED` when the burn already existed.
Returns per-question option-selection counts across everyone who answered the study's screener — the aggregate view, not per-participant rows. Use it for incidence and quota reporting without exporting a file.
Path parameters
Name
Type
Description
study_id
uuid
The study whose screener results to read.
Response
One entry per screener question, in the screener's own question order.
Field
Type
Description
data[].question_id
uuid
The screener question.
data[].question_text
string
The question as participants saw it, in the study's source language.
data[].question_type
string
The screener question's type, e.g. `MULTIPLE_CHOICE` or `MULTIPLE_SELECT`.
data[].total_responses
integer
How many respondents answered this question.
data[].options[].option_text
string
The answer option's display text.
data[].options[].count
integer
How many respondents picked this option; options nobody picked are included with a count of zero.
{
"data": [
{
"question_id": "a17c4e63-92d5-4f10-b8e7-5d3f0a2c6b91",
"question_text": "How often do you buy coffee outside the home?",
"question_type": "MULTIPLE_CHOICE",
"total_responses": 412,
"options": [
{
"option_text": "Daily",
"count": 148
},
{
"option_text": "A few times a week",
"count": 173
},
{
"option_text": "Rarely or never",
"count": 91
}
]
}
]
}
Errors
Status
Code
When
404
not_found
The study does not exist, is outside the credential's workspaces, or has no screener.
Counts include respondents who were later flagged as fraudulent or low quality — screening happened before those judgements, and excluding them here would disagree with the screen-out counts elsewhere in the product. An option the researcher has since removed still appears if participants picked it, so historical counts never silently drop. For per-participant screener answers, use the interview detail endpoint or a transcript export, both of which carry screener Q&A inline.
Renders the study's discussion guide and/or screener as a Word document and returns a presigned URL to download it. This is study design, not participant output — the shape to archive alongside fieldwork, or to hand to a client for sign-off.
Path parameters
Name
Type
Description
study_id
uuid
The study whose instrument to export.
Query parameters
Name
Type
Description
section
string
Which part to render: `guide` or `screener`; omit for both.
language
string
Repeatable. Render each item's translation in this language beneath the source text; each value must be a ready (generated or reviewed) target language of the study. Omit for the source language only.
Response
Where to download the rendered document.
Field
Type
Description
data.download_url
string
Presigned URL for the `.docx` file; expires within minutes and must not be persisted.
`section` is neither `guide` nor `screener`, or a requested `language` is not a ready target language of this study.
404
not_found
The study does not exist, or is outside the credential's workspaces.
The study's source language is always the base text and cannot be requested as a translation target. The document reflects the study as it stands right now, including unpublished edits — it is not a snapshot of what any particular participant saw.
Webhooks let Outset push study and organization events to your backend instead of you polling for them. Delivery is existing and already live for whitelabel partners; the event catalog and a self-service subscription API are planned.
How delivery works today
Webhook delivery is configured per partner integration, not per organization — every client organization provisioned under your provider shares the same set of destination endpoints. An integration carries a list of endpoint URLs and a parallel list of header dictionaries, one per URL, sent verbatim on every request (this is how a partner supplies its own bearer token or API key — Outset does not generate or manage that credential).
On a triggering event, Outset does a synchronous POST of a JSON body to every configured URL, with a 20-second timeout. Delivery is synchronous with the triggering event — there is no queue and no redelivery: each endpoint gets exactly one delivery attempt per event, in whatever order the endpoints are listed. A non-2xx response or a network failure is logged and monitored on Outset's side but does not stop delivery to the remaining endpoints and does not raise back to the caller — falling back to some other channel on a rejected webhook isn't an option for organizations whose whole point is that Outset never contacts their users directly.
Event types
Type
Status
Carries
SEND_EMAIL
existing
The email type, recipient, organization id, study ids, and the template's rendered fields — sent whenever Outset would otherwise have emailed the organization's own users directly (study launched, study completed, transcript ready, export ready, etc.), plus estimated cost fields on the payloads that have them.
STUDY_UNPUBLISHED
existing
Organization id, study id, study name and URL, estimated current paid amount, and answered-question count.
FUNDS_REQUIRED
planned
Fires when a study launch is blocked by an organization's spend cap or wallet balance — carries the organization, the study, and the amount required to unblock it. This is the mechanism for a partner funding multiple client organizations from one central wallet to find out a client needs a top-up.
WORKSPACE_CREATED / WORKSPACE_ARCHIVED
planned, reserved
Reserved event names for when workspace write access lands on the platform API. Not fired today.
Two payload fields are also planned additions across every event type: an external organization id (the partner's own identifier for the org, rather than Outset's internal one) and a nullable workspace id. Today's payloads carry only Outset's organization id with no workspace reference.
If your integration validates the type field against a fixed allow-list, expect these new types to arrive once they ship — the addition is coordinated with existing webhook partners rather than sprung on a running integration, but a consumer that hard-rejects unrecognized types should be prepared to add them.
Proposed: self-service subscriptions
Today, adding or changing a webhook endpoint is a configuration change Outset makes for you. A proposed /v2/webhooks/ resource would let a partner manage its own subscriptions under a reserved webhooks:manage scope, replacing that manual step with routes it fully owns. This is a sketch, not a committed design — the routes below describe the shape under discussion, not a built API.
Method
Route
Purpose
GET
/v2/webhooks/
List configured subscriptions (URL, event types, status)
POST
/v2/webhooks/
Register a new subscription
PATCH
/v2/webhooks/{id}/
Update a subscription's URL, headers, or event types
DELETE
/v2/webhooks/{id}/
Remove a subscription
Delivery guarantees
Treat delivery as at-most-once, best-effort, not exactly-once or guaranteed. There is no retry today — a single non-2xx response or timeout means that event never arrives at that endpoint, only a log line on Outset's side records the attempt — and today's payloads carry no dedicated event id or delivery id to dedupe against, so a partner that needs stronger guarantees should build reconciliation against the underlying resource (poll the study or organization state) rather than relying on the webhook stream alone. An idempotency and delivery-security refactor (signed payloads, retries, event ids) is planned; until it ships, key any dedup logic you do have on the fields already present in a payload (organization id, study id, email type) rather than waiting on an id field that doesn't exist yet.