API Reference
Base URL: https://usertold.ai
Use REST for custom backends and integrations, the CLI for terminal automation, and MCP for project-aware agents.
The OpenAPI 3.1 document is the source of truth for registered endpoint paths, request fields, response schemas, and enums:
Multipart uploads and provider webhooks are not yet complete in OpenAPI, so their required inputs are below.
Versioning and deprecation
The current /api/... routes are stable major version 1. Backward-compatible fields and operations may be added within v1; breaking changes require a new major path beginning with /api/v2.
Before removal, UserTold publishes migration guidance and sends RFC 9745 Deprecation plus Link: rel="deprecation". A scheduled removal also sends RFC 8594 Sunset at least 90 days ahead. No REST route is currently deprecated or scheduled for removal.
Authentication
Authenticated REST requests use a user-delegated bearer token:
Authorization: Bearer <token>
The CLI can complete browser login and expose the token for local automation:
usertold auth login
export USERTOLD_TOKEN="$(usertold auth token --json | jq -r .token)"
Keep bearer tokens server-side. The widget's public SDK key works only on participant SDK routes; it cannot replace a user token. Provider webhooks use the signatures below.
Project scope
Builder operations use organization and project handles:
/api/orgs/:orgHandle/projects/:projectHandle/...
Responses include org_handle, project_handle, and project_ref (org/project); opaque storage IDs are not builder references.
Integration recipes
Resolve the current workspace
Read the profile first; personal_org_handle identifies the default workspace.
curl -sS https://usertold.ai/api/user/profile \
-H "Authorization: Bearer $USERTOLD_TOKEN"
Create a project
curl -sS https://usertold.ai/api/orgs/acme/projects \
-H "Authorization: Bearer $USERTOLD_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Checkout research" }'
Use the project handle for later calls. Project creation atomically provisions one active First user interview with no Intake or placement restrictions, so the canonical Project snippet can be installed immediately. POST /api/orgs/:orgHandle/projects/:projectHandle/starter-study idempotently restores that starter only when an existing Project has no Studies. Study responses include nullable recruitment_url for direct links.
Review Evidence before creating a Finding
curl -sS "https://usertold.ai/api/orgs/acme/projects/checkout/signals?type=struggling_moment&limit=10" \
-H "Authorization: Bearer $USERTOLD_TOKEN"
Inspect the source quote, each card's page_url and page_title, and the transcript behind it — GET /api/orgs/{org}/projects/{project}/sessions/{session_id}/transcript for the card's session_id — before grouping Evidence into a draft Finding. Creating a Finding does not mark it reviewed or create external work.
curl -sS https://usertold.ai/api/orgs/acme/projects/checkout/findings/from-evidence \
-H "Authorization: Bearer $USERTOLD_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "title": "Checkout payment failures leave people unsure what happened", "evidence_refs": ["sig_1", "sig_2"] }'
Set the Finding's research_state to reviewed only after source review confirms the Evidence, synthesis, and current project context. Review does not create external work; send the reviewed Finding explicitly to Linear product intake or GitHub delivery.
Import a transcript
Send multipart/form-data to /api/orgs/:orgHandle/projects/:projectHandle/sessions/import-transcript. The transcript file is required, must contain non-empty text, and must be no larger than 5 MiB. Optional fields are participant_name, participant_email, and study_ref; when supplied, study_ref must be the handle of an active study.
curl -sS https://usertold.ai/api/orgs/acme/projects/checkout/sessions/import-transcript \
-H "Authorization: Bearer $USERTOLD_TOKEN" \
-F "transcript=@interview.txt;type=text/plain" \
-F "study_ref=checkout-study"
A successful import returns 201 with the created interview and queued: true. Processing continues asynchronously.
Upload a recording
The dashboard and CLI use resumable direct-to-R2 uploads up to 20 GiB. Processing
derives audio, representative frames, OCR, and provenance before
transcription. See /api/openapi for the resumable upload endpoint schemas.
The older single-request endpoint remains available for small CLI/MCP imports:
Send multipart/form-data to /api/orgs/:orgHandle/projects/:projectHandle/sessions/upload-video using one of these shapes:
media: one audio or video file, up to 25 MiB; oraudio: an audio file up to 25 MiB, plus optionalvideoplayback media up to 100 MiB.
Optional fields are participant_name, participant_email, and study_ref; when supplied, study_ref must be the handle of an active study. Supported audio formats are MP3, M4A, WAV, OGG, FLAC, AAC, and WebM. Supported video formats are MP4, WebM, and MPEG.
curl -sS https://usertold.ai/api/orgs/acme/projects/checkout/sessions/upload-video \
-H "Authorization: Bearer $USERTOLD_TOKEN" \
-F "audio=@interview.ogg" \
-F "video=@screen.mp4" \
-F "study_ref=checkout-study"
A successful upload returns 201 with the created interview and queued: true.
Receive provider webhooks
These signature-verified routes receive provider payloads for connected UserTold integrations. They are not generic outgoing-webhook configuration endpoints, and they are not currently listed in OpenAPI.
| Provider route | Required signature input | Replay input |
|---|---|---|
POST /api/webhooks/github | Raw request body and X-Hub-Signature-256 (sha256= HMAC); X-GitHub-Event selects the event type | X-GitHub-Delivery when supplied |
POST /api/webhooks/linear | Raw request body and Linear-Signature HMAC; payload must include a current webhookTimestamp | Linear-Delivery when supplied |
POST /api/webhooks/polar | Raw request body plus webhook-id, webhook-timestamp, and webhook-signature | Signed timestamp must be within five minutes |
Configure these through the corresponding GitHub, Linear, or billing integration. Preserve the raw request body for signature verification; do not parse and reserialize it first.
Errors and retries
Errors that carry retry guidance use this JSON envelope:
{
"code": "OPTIONAL_CODE",
"retryable": false,
"error": "Description of what went wrong",
"action": "Optional next step for the caller"
}
- Treat
retryableas the primary retry signal when it is present. - Retry
429and retryable5xxresponses with bounded exponential backoff and jitter. - Do not automatically retry validation, authentication, permission, or payment errors; surface
errorandactionto the operator. - Do not retry an ambiguous write timeout indefinitely.
- Upload and background-processing operations may succeed asynchronously. Poll the documented status operation instead of resubmitting the original write.
Common statuses are 400 validation, 401 authentication required, 402 payment required, 403 forbidden, 404 not found, 429 rate limited, and 5xx service failure.
Rate limits
Limits use independent 60-second buckets for each route. They are keyed by client IP unless the table says otherwise.
| Integration route | Limit |
|---|---|
| Google sign-in start | 10 requests per IP |
| OAuth authorize | 20 requests per IP |
| OAuth token exchange | 10 requests per IP |
| OAuth dynamic client create | 5 requests per IP |
| OAuth dynamic client read, update, or delete | 20 requests per IP for each route |
| Agent registration | 30 requests per IP; claim-producing registrations also share a 5-request bucket |
| Agent claim trigger, claim completion, and revocation | 5, 10, and 60 requests per IP, respectively |
| SDK intake list, detail, and response | 30, 60, and 10 requests per IP, respectively |
| SDK interview create, runtime token, and consent | 20 requests per IP for each route |
| Legacy SDK audio form upload | 30 requests per interview |
| Conductor start | 10 requests per IP |
| Conductor transcription secret, STS secret, and realtime call | 5 requests per IP for each route |
| Project integration-key validation and health | 10 requests per IP for each route |
| Admin email delivery test | 6 requests per IP |
General per-IP/session middleware returns IETF-draft RateLimit-Policy and RateLimit, legacy RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, and X-RateLimit-*, plus Retry-After on 429. Atomic claim trigger/completion, email throttle, and OTP lockout expose only documented error/reason guidance; they do not publish remaining/reset state.