HTTP API

Start runs, stream their events and control threads from any language over HTTP.

Every host serves a typed JSON API under /v1. Use it from a web app, a backend in another language, or curl. Each route is a thin wrapper over a library method (startRun, Thread.timeline, Thread.approve, ...), so it behaves exactly like the code API.

Authenticate callers

The API is closed by default: without authenticate, every /v1 route answers 401. Give the host a function that maps a request to a principal (who is calling, and which tenant they belong to), or null to reject it.

const app = host({
  store: sqlite(":memory:"),
  agents: { support },
  authenticate: async (request) => {
    const token = request.headers.get("authorization");
    if (token !== `Bearer ${process.env["API_TOKEN"]}`) return null;
    return { issuer: "api", tenant: "acme", subject: "alice" };
  },
});

Everything a caller does is scoped to their tenant: another tenant's thread answers 404. Approvals and parked actions also check that the caller is allowed to decide them.

Start a run and stream it

Start a run

curl -X POST http://localhost:8787/v1/runs \
  -H "authorization: Bearer $API_TOKEN" \
  -H "content-type: application/json" \
  -H "idempotency-key: req-1" \
  -d '{"agent": "support", "input": "Hello"}'

The host answers 202 as soon as the input is safely stored, and the run continues in the background:

{"thread_id": "01a0cead-...", "branch_id": "01a0cead-...", "run_id": "01a0cead-..."}

To continue an existing conversation, add "thread_id" (and optionally "branch_id") to the body. You can also pass a "budget" for this run.

Stream its events

curl -N http://localhost:8787/v1/threads/$THREAD_ID/runs/$RUN_ID/events \
  -H "authorization: Bearer $API_TOKEN"

The response is a Server-Sent Events stream. Each logged step arrives as an event message with its sequence number as the SSE id, and the stream ends with the run's result:

id: 2
data: {"kind":"event","event":{"seq":2,"type":"user_input", ...}}

id: 4
data: {"kind":"event","event":{"seq":4,"type":"model_response", ...}}

data: {"kind":"result","run_id":"01a0cead-...","result":{"status":"completed","output":"Hi! How can I help?", ...}}

If the connection drops, reconnect with Last-Event-ID (browsers' EventSource does this for you) or ?after_seq=<n> to resume after the last event you saw. Subscribing never starts or changes anything.

Retries are safe

POST /v1/runs requires an Idempotency-Key header (1 to 255 characters). If a response is lost and you retry:

RetryResult
Same key, same body, same caller202 with the original receipt. No second run.
Same key, different body409 idempotency_key_reused
Same key, different caller409 idempotency_key_principal_mismatch

Routes

MethodPathDoes
POST/v1/runsStart a run (new or existing thread)
GET/v1/threads/{thread_id}/runs/{run_id}/eventsStream a run's events (SSE)
GET/v1/threads/{thread_id}/timelineThe thread's steps. See Timeline
GET/v1/threads/{thread_id}/branchesList branches
GET/v1/threads/{thread_id}/fork-pointsWhere the thread can be forked
POST/v1/threads/{thread_id}/forksFork a branch. See Fork
GET/v1/threads/{thread_id}/approvalsPending approvals
POST/v1/threads/{thread_id}/approvals/{challenge_id}Approve or deny: decision is grant or deny
POST/v1/threads/{thread_id}/parked/{effect_key}/resolveResolve an uncertain action: assume_done or assume_not_done
POST/v1/threads/{thread_id}/cancelCancel the thread's current work
POST/v1/threads/{thread_id}/settingsSwitch the thread's model
POST/v1/threads/{thread_id}/modeChange the permission mode
GET, POST/channels/{channel}/eventsChannel webhooks (no authenticate; verified per provider)

Errors are always {"error": {"code": "...", "message": "..."}}: 400 invalid_request, 401 unauthenticated, 403 forbidden, 404 not_found, and 409 for domain errors such as branch_busy. Most thread routes take an optional ?branch_id= query parameter to act on a branch other than the main one.

The full request and response schemas are in the HTTP API reference, generated from the OpenAPI file at spec/schema/host-api/openapi.json.

From code

The same two operations are methods on the host, for when you are already in-process:

TypeScriptPython
app.startRun(request, { principal, idempotencyKey })await app.start_run(request, principal=..., idempotency_key=...)
app.subscribe(threadId, runId, { principal, afterSeq })await app.subscribe(thread_id, run_id, principal=..., after_seq=...)

Both return a result value (ok / Ok) instead of throwing for expected failures.

Edit on GitHub

On this page