Conventions

The hosts, credentials, headers, ids, paging rules and error shapes every route of the project API shares, and the index of routes.

The SDK covers the routes an app calls, so most apps never call the API directly; user erasure, MCP, message deletion and the project read API other than its thread and message lists are REST only. These pages are for the cases that do: a server in another language, an exporter, an evaluation job, or a look at what the SDK sends. Each resource has its own page with every route, parameter table, example and status table; this page holds what they share.

Hosts and credentials

HostWho calls itCredential
https://proj-<id>.assistant-api.com, the frontend hostThe browser, React Native, InkAuthorization: Bearer <token>: an anonymous access token, or a JWT your auth provider signed
https://backend.assistant-api.com, the backend hostYour serverAuthorization: Bearer sk_aui_proj_…, with Aui-User-Id and Aui-Workspace-Id naming the user and the workspace the request acts as

A credential is accepted only on its own host; presented elsewhere it answers 403 with Origin does not match project ID. On a valid provider JWT the response carries an Authorization header with a five minute token of the project's own, which the SDK uses for the following requests. The API key routes, the routes that accept nothing but a key, and how each credential names the user and the workspace are on Authentication.

Headers

HeaderDirectionRule
AuthorizationrequestBearer <value>. Missing answers 401 with Authorization header is missing; another scheme answers 401 with Authorization header must begin with "Bearer "; a value that is neither a key nor a JWT answers 403 with Unsupported authorization value.
Aui-User-Idrequest, API key onlyThe user the request acts as, 1 to 255 characters without whitespace or control characters. __SYSTEM__ when absent.
Aui-Workspace-Idrequest, API key onlyThe workspace the request acts in, same rules. Required on every route except POST /v1/traces, where it defaults to the trace workspace.
Aui-Sdkrequest, optionalSpace separated name/version tokens naming the client software, at most 8, names up to 214 characters and versions up to 64. The SDK sets it; the dashboard lists the values under Settings › Telemetry.
Content-Typerequestapplication/json for every JSON body. The trace receiver also takes application/x-protobuf and application/protobuf, with gzip or deflate in Content-Encoding.
AcceptrequestRequired on POST /v1/runs/stream only: text/plain or text/event-stream.
AuthorizationresponseOn a provider JWT, the project's own token for the following requests. Exposed to the browser through CORS.
Retry-AfterresponseSeconds to wait, on every 429.

The frontend host answers the browser origins listed under Settings › Access; an empty list answers every origin. See Allowed origins.

Send a request

A server call carries the key and the two identity headers. The same request, creating a thread, in curl and in TypeScript:

From a server
curl https://backend.assistant-api.com/v1/threads \
  -H "Authorization: Bearer $ASSISTANT_API_KEY" \
  -H "Aui-User-Id: user_123" \
  -H "Aui-Workspace-Id: workspace_123" \
  -H "Content-Type: application/json" \
  -d '{ "last_message_at": "2026-09-17T09:30:00.000Z", "external_id": "crm:conversation:42" }'
From a server, without the SDK
const response = await fetch("https://backend.assistant-api.com/v1/threads", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ASSISTANT_API_KEY}`,
    "Aui-User-Id": "user_123",
    "Aui-Workspace-Id": "workspace_123",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    last_message_at: new Date().toISOString(),
    external_id: "crm:conversation:42",
  }),
});
const { thread_id } = await response.json();

A browser call carries the token the SDK holds for the session, on the project's frontend host:

From the browser
const response = await fetch(
  `${process.env.NEXT_PUBLIC_ASSISTANT_BASE_URL}/v1/threads?limit=20`,
  { headers: { Authorization: `Bearer ${accessToken}` } },
);
const { threads } = await response.json();

Every route on the following pages has this curl form with its headers, then the request and response bodies and the status table.

Ids, time and paging

Ids are a prefix and 24 characters: thread_0…, msg_0…, run_0…, score_…, usr_anon_0…. Timestamps are ISO 8601 in UTC. Thread, project, MCP and trace responses are sent with Cache-Control: private, no-store.

ListCursorDefault pageMaximum
GET /v1/threads, GET /v1/projects/threadsafter, the last thread id of the page20100
GET /v1/threads/{thread_id}/messages and the project variantafter, the last message id of the page200200
GET /v1/projects/scoresafter, the last score id of the page50200
GET /v1/projects/runscursor, an opaque value returned as next_cursor50100
MCP list_threads, list_scores, list_runscursor, opaque, returned as next_cursor50100

Errors

Errors return JSON. The message is in a string error, such as { "error": "Thread not found" }, except for the /v1/mcp transport's 401 and 406 responses, which MCP describes. Branch on the status code, not the text: the message is for people. A body or query that fails the route's schema answers 400 with { "error": "Schema validation error", "details": […] }, where details lists the failed checks. An unknown route answers 404 with { "error": "Not Found" }. The 402 and 429 bodies below carry more fields beside error, and an unexpected failure answers 500 with { "error": "Internal Server Error" }.

StatusWhen
400A field fails validation, an unknown field is sent to a strict route, malformed JSON, a message parent that is not in the thread, a message_id that is not in the thread, a bad cursor, a usage day range over 366 days, an unsupported attachment type, a malformed trace export, a URL that fails the public host check
401No or malformed Authorization, an expired token, a token not yet valid
402{ "error": "plan_limit_reached", "plan", "cap", "period_end" }: a new end user past the plan's active user cap, on POST /v1/threads/{thread_id}/messages with a user message only
403A credential on the wrong host, an unknown or expired key, an invalid token, a rule that matches no token, a route the credential may not use, anonymous access turned off, an anonymous caller claiming threads
404Thread not found, Message not found, Messages not found, Run not found, Assistant not found, Attachment not found, Project not found, and Not Found for an unknown route
405Any method other than POST on /v1/mcp
409Message role cannot change
413, 415A trace export over 4 MB, or in a content type or encoding the receiver does not take
429{ "error": "rate_limited" } with Retry-After: anonymous sessions at 30 a minute, refreshes at 120 a minute per address, event requests at 600 a minute per user
503Attachments are not configured on this deployment

Threads, messages, run reports, scores, traces, the project read API and MCP are not rate limited. Every size limit is on Limits.

Routes

ResourceRoutesPage
Auth tokensPOST /v1/auth/tokens, POST /v1/auth/tokens/anonymous, POST /v1/auth/tokens/refreshAuth tokens
ThreadsPOST, GET /v1/threads, GET, PUT, DELETE /v1/threads/{thread_id}, POST /v1/threads/claimThreads
MessagesPOST, GET, DELETE /v1/threads/{thread_id}/messages, PUT /v1/threads/{thread_id}/messages/{message_id}, POST …/feedbackMessages
RunsPOST /v1/runs, POST /v1/runs/streamRuns
EventsPOST /v1/eventsEvents
ScoresPOST /v1/scoresScores
FilesPOST /v1/files/attachments/generate-presigned-upload-url, POST /v1/files/attachments/generate-presigned-download-urlFiles
TracesPOST /v1/tracesTraces
Project read APIGET /v1/projects/runs, GET /v1/projects/runs/{run_id}, GET /v1/projects/usage/daily, GET /v1/projects/usage/period, GET /v1/projects/threads, GET /v1/projects/threads/{thread_id}/messages, GET /v1/projects/scores, GET /v1/projects/skillsProject read API
UsersDELETE /v1/projects/users/{user_id}Users
MCPPOST /v1/mcpMCP

GET /healthz answers { "ok": true } without a credential. The TypeScript surface of the same routes is on The assistant-cloud client.