API Reference

Base URL: https://app.usertold.ai

Authentication

Three auth methods depending on the endpoint:

MethodHeaderUsed By
JWTAuthorization: Bearer <token> or auth_token cookieDashboard
SDK KeyX-Project-Key: ut_pub_... or ?key=ut_pub_...Embedded widget
WebhookHMAC signature verificationGitHub, Polar webhooks

Error Format

{
  "code": "OPTIONAL_CODE",
  "retryable": false,
  "message": "Description of what went wrong",
  "action": "Optional next step for user"
}

Common status codes: 400 validation, 401 auth required, 402 payment required, 403 forbidden, 404 not found, 429 rate limited.

Project-scoped routes use canonical handles in the path:

  • frontend route params: :org and :project (for /o/:org/p/:project/...)
  • API path params remain :orgHandle and :projectHandle

Scoped resolver support:

  • :studyId accepts study id or handle (within project scope)
  • :screenerId accepts screener id or handle (within project scope)

MCP for Agentic Workflows

Use MCP when your agents need discovery and project-aware execution in one protocol surface.

  • Endpoint: POST /mcp
  • Auth: OAuth 2.1 for remote MCP clients (Authorization Code + PKCE). MCP accepts bearer tokens after OAuth issuance.
  • Methods: initialize, initialized, tools/list, tools/call, prompts/list, prompts/get, resources/list, resources/templates/list, resources/read
  • Discovery: GET /.well-known/openid-configuration, GET /.well-known/oauth-protected-resource
  • Dynamic client registration: supported via POST /api/oauth/register (RFC 7591). Management endpoints: GET/PUT/DELETE /api/oauth/register/:clientId. Static allowlist clients are also supported for pre-registered integrations.

Initialize:

{
  "jsonrpc": "2.0",
  "id": "init-1",
  "method": "initialize",
  "params": { "protocolVersion": "2025-11-25", "capabilities": { "tools": {}, "prompts": {}, "resources": {} } }
}

Call a tool:

{
  "jsonrpc": "2.0",
  "id": "tools-1",
  "method": "tools/call",
  "params": {
    "name": "tasks.list",
    "arguments": { "projectRef": "prj_..." }
  }
}

For non-agent automation workflows, continue using the REST routes in this doc.


Health

GET /api/health

Public. Returns service status.

{ "status": "ok", "environment": "production" }

Auth

POST /api/auth/google

Exchange Google OAuth token for a session. Rate limited: 10 req/min.

POST /api/auth/logout

Clear session.

GET /api/user/profile

Auth: JWT. Returns current user profile.

PATCH /api/user/profile

Auth: JWT. Update user name.


Projects

Canonical project metadata fields on project-bearing responses:

  • organization_id
  • org_handle
  • project_handle
  • project_ref (org/project)
  • canonical_path (/o/:org/p/:project)

GET /api/orgs/:orgHandle/projects

Auth: JWT. List all projects for the authenticated user.

POST /api/orgs/:orgHandle/projects

Auth: JWT. Create a project.

{ "name": "My Product", "description": "optional", "github_repo_url": "owner/repo" }

GET /api/orgs/:orgHandle/projects/:projectHandle

Auth: JWT. Get project details and members.

PATCH /api/orgs/:orgHandle/projects/:projectHandle

Auth: JWT (owner/admin). Update project fields.

DELETE /api/orgs/:orgHandle/projects/:projectHandle

Auth: JWT (owner only). Delete project.


Sessions

All session routes require JWT + Project membership.

GET /api/orgs/:orgHandle/projects/:projectHandle/sessions

List sessions. Query params: status, processing_status, limit, offset.

POST /api/orgs/:orgHandle/projects/:projectHandle/sessions

Create a session. Requires billing check.

{
  "participant_name": "Jane",
  "participant_email": "jane@example.com",
  "interview_mode": "voice",
  "screener_id": "scr_...",
  "screener_response_id": "resp_..."
}

GET /api/orgs/:orgHandle/projects/:projectHandle/sessions/:sessionId

Get session with messages, events, and audio chunks.

PATCH /api/orgs/:orgHandle/projects/:projectHandle/sessions/:sessionId

Update session fields (status, summary, consent flags).

POST /api/orgs/:orgHandle/projects/:projectHandle/sessions/:sessionId/reprocess

Reprocess a completed session (re-run signal extraction).

DELETE /api/orgs/:orgHandle/projects/:projectHandle/sessions/:sessionId

Delete a session.


Signals

All signal routes require JWT + Project membership.

GET /api/orgs/:orgHandle/projects/:projectHandle/signals

List signals. Query params: type, session_id, task_id, search, min_confidence, dismissed, limit, offset.

GET /api/orgs/:orgHandle/projects/:projectHandle/signals/:signalId

Get signal details.

PATCH /api/orgs/:orgHandle/projects/:projectHandle/signals/:signalId

Update signal fields (type, confidence, quote, analysis, context).

POST /api/orgs/:orgHandle/projects/:projectHandle/signals/:signalId/annotate

Add human annotation. Body: { "text": "..." } (max 2000 chars).

POST /api/orgs/:orgHandle/projects/:projectHandle/signals/:signalId/dismiss

Soft-exclude signal. Body: { "reason": "..." }.

POST /api/orgs/:orgHandle/projects/:projectHandle/signals/:signalId/undismiss

Restore dismissed signal.

POST /api/orgs/:orgHandle/projects/:projectHandle/signals/:signalId/link

Link signal to task. Body: { "task_id": "..." }.

POST /api/orgs/:orgHandle/projects/:projectHandle/signals/:signalId/unlink

Unlink signal from task.

DELETE /api/orgs/:orgHandle/projects/:projectHandle/signals/:signalId

Delete a signal.


Tasks

All task routes require JWT + Project membership.

GET /api/orgs/:orgHandle/projects/:projectHandle/tasks

List tasks. Query params: status, type, limit, offset.

GET /api/orgs/:orgHandle/projects/:projectHandle/tasks/ready

List high-priority tasks ready for implementation. Query params: limit (default 10).

POST /api/orgs/:orgHandle/projects/:projectHandle/tasks

Create a task.

{
  "title": "Fix checkout abandonment",
  "description": "Users drop off at payment step",
  "task_type": "bug",
  "priority_score": 75,
  "effort_estimate": "m"
}

GET /api/orgs/:orgHandle/projects/:projectHandle/tasks/:taskId

Get task with linked signals.

PATCH /api/orgs/:orgHandle/projects/:projectHandle/tasks/:taskId

Update task fields. Priority label auto-calculated from score (critical >= 80, high >= 60, medium >= 40, low < 40).

DELETE /api/orgs/:orgHandle/projects/:projectHandle/tasks/:taskId

Delete a task.


Screeners

Dashboard Routes (JWT + Project)

GET /api/orgs/:orgHandle/projects/:projectHandle/screeners — List screeners.

POST /api/orgs/:orgHandle/projects/:projectHandle/screeners — Create a screener with optional inline questions.

{
  "title": "Beta User Study",
  "welcome_message": "Help us improve!",
  "brand_color": "#3b82f6",
  "consent_text": "I agree to participate",
  "max_participants": 50,
  "questions": [
    {
      "question_text": "How often do you use our product?",
      "question_type": "single_choice",
      "options": ["Daily", "Weekly", "Monthly", "Rarely"],
      "qualification_rules": { "qualify": ["Daily", "Weekly"] }
    }
  ]
}

GET /api/orgs/:orgHandle/projects/:projectHandle/screeners/:screenerId — Get screener with questions and responses.

PATCH /api/orgs/:orgHandle/projects/:projectHandle/screeners/:screenerId — Update screener fields.

DELETE /api/orgs/:orgHandle/projects/:projectHandle/screeners/:screenerId — Delete screener.

POST /api/orgs/:orgHandle/projects/:projectHandle/screeners/:screenerId/questions — Add a question.

PATCH /api/orgs/:orgHandle/projects/:projectHandle/screeners/:screenerId/questions/:questionId — Update a question.

DELETE /api/orgs/:orgHandle/projects/:projectHandle/screeners/:screenerId/questions/:questionId — Delete a question.

POST /api/orgs/:orgHandle/projects/:projectHandle/screeners/:screenerId/questions/reorder — Reorder questions. Body: { "question_ids": ["q1", "q2", "q3"] }.

Public SDK Routes (SDK Key)

GET /api/sdk/screeners — List active screeners for the project. Rate limited: 30 req/min.

GET /api/sdk/screener/:screenerId — Get screener with questions (public fields only). Rate limited: 60 req/min.

POST /api/sdk/screener/:screenerId/respond — Submit screener response. Rate limited: 10 req/min.

{
  "answers": { "q1": "Daily", "q2": "Developer" },
  "consent_given": true,
  "consent_recording": true,
  "participant_name": "Jane",
  "participant_email": "jane@example.com"
}

Studies

Dashboard Routes (JWT + Project)

GET /api/orgs/:orgHandle/projects/:projectHandle/studies — List studies.

POST /api/orgs/:orgHandle/projects/:projectHandle/studies — Create a study.

{
  "title": "Checkout Research",
  "study_type": "usability",
  "goals": [{ "id": "g1", "description": "Find checkout friction" }],
  "script": {
    "version": 2,
    "goals": [{ "id": "g1", "description": "Find checkout friction" }],
    "segments": [
      { "id": "intro", "mode": "speak", "title": "Instructions" },
      { "id": "task", "mode": "observe", "title": "Complete checkout" },
      { "id": "debrief", "mode": "talk", "title": "Discuss" }
    ]
  }
}

GET /api/orgs/:orgHandle/projects/:projectHandle/studies/:studyId — Get study details.

PATCH /api/orgs/:orgHandle/projects/:projectHandle/studies/:studyId — Update study.

DELETE /api/orgs/:orgHandle/projects/:projectHandle/studies/:studyId — Delete study.

Public SDK Route

GET /api/sdk/study/:studyId — Get active study with parsed script and settings.


Conductor (Interview Runtime)

SDK Key auth. These power the real-time interview experience.

POST /api/sdk/conductor/:sessionId/start

Initialize conductor session. Returns WebSocket URL. Rate limited: 10 req/min.

{ "ws_url": "wss://app.usertold.ai/api/sdk/conductor/ses_.../ws" }

GET /api/sdk/conductor/:sessionId/ws

WebSocket upgrade endpoint for real-time interview orchestration.

POST /api/sdk/conductor/:sessionId/transcription-secret

Get ephemeral token for browser-side speech transcription. Rate limited: 5 req/min.

POST /api/sdk/conductor/:sessionId/sts-secret

Get ephemeral token for browser-side voice conversation. Rate limited: 5 req/min.

POST /api/sdk/conductor/:sessionId/end

End conductor session. Idempotent. Queues session for processing.


SDK Utilities

POST /api/sdk/sessions

Create a session without a screener. Rate limited: 20 req/min.

POST /api/sdk/audio/upload

Upload audio chunk (FormData). Max 10 MB per chunk. Rate limited: 30 req/min per session.

POST /api/sdk/screen/upload

Upload screen recording chunk (FormData). Max 25 MB per chunk. Rate limited: 30 req/min per session.

POST /api/sdk/events

Batch event ingestion. Max 256 KB payload.

{
  "session_id": "ses_...",
  "events": [
    { "type": "click", "timestamp_ms": 1707000000000, "data": { "selector": ".btn" } },
    { "type": "navigate", "timestamp_ms": 1707000001000, "data": { "url": "/checkout" } }
  ]
}

Billing

GET /api/billing

Auth: JWT. Get billing status (payment status, spending cap, usage).

PATCH /api/billing/cap

Auth: JWT. Set spending cap. Minimum $10 (1000 cents).

{ "spending_cap_cents": 5000 }

POST /api/billing/checkout

Auth: JWT. Create payment checkout session.


Webhooks

POST /api/webhooks/github

GitHub webhook receiver. Verified via HMAC-SHA256. Handles issue state changes for task status tracking.

POST /api/webhooks/polar

Polar.sh webhook receiver. Verified via HMAC-SHA256. Handles payment events.


Rate Limits

Endpoint CategoryLimit
Auth (Google)10 req/min per IP
SDK screener list30 req/min per IP
SDK screener detail60 req/min per IP
SDK screener respond10 req/min per IP
SDK session create20 req/min per IP
SDK audio/screen upload30 req/min per session
Conductor start10 req/min per IP
Conductor secrets5 req/min per IP

Upload limits: audio 10 MB/chunk, screen 25 MB/chunk, events 256 KB/batch, max 500 chunks per session over 24h.

See also