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; or
  • audio: an audio file up to 25 MiB, plus optional video playback 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 routeRequired signature inputReplay input
POST /api/webhooks/githubRaw request body and X-Hub-Signature-256 (sha256= HMAC); X-GitHub-Event selects the event typeX-GitHub-Delivery when supplied
POST /api/webhooks/linearRaw request body and Linear-Signature HMAC; payload must include a current webhookTimestampLinear-Delivery when supplied
POST /api/webhooks/polarRaw request body plus webhook-id, webhook-timestamp, and webhook-signatureSigned 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 retryable as the primary retry signal when it is present.
  • Retry 429 and retryable 5xx responses with bounded exponential backoff and jitter.
  • Do not automatically retry validation, authentication, permission, or payment errors; surface error and action to 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 routeLimit
Google sign-in start10 requests per IP
OAuth authorize20 requests per IP
OAuth token exchange10 requests per IP
OAuth dynamic client create5 requests per IP
OAuth dynamic client read, update, or delete20 requests per IP for each route
Agent registration30 requests per IP; claim-producing registrations also share a 5-request bucket
Agent claim trigger, claim completion, and revocation5, 10, and 60 requests per IP, respectively
SDK intake list, detail, and response30, 60, and 10 requests per IP, respectively
SDK interview create, runtime token, and consent20 requests per IP for each route
Legacy SDK audio form upload30 requests per interview
Conductor start10 requests per IP
Conductor transcription secret, STS secret, and realtime call5 requests per IP for each route
Project integration-key validation and health10 requests per IP for each route
Admin email delivery test6 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.