REST API

Updated Sep 2, 2026View as Markdown

The engine serves four HTTP APIs on one host. Requests and responses are JSON. Streaming endpoints return text/event-stream. Every shape used in a body is defined under Objects.

APIPrefixAuthFor
Client/api/clientClient JWTEnd-user clients: a browser or an app backend.
Machine/api/machineAPI keyYour backend and your worker's callbacks.
Admin/api/adminAPI keyReading sessions on a server you host.
Cloud/api/v1API keyThe hosted control plane. The subs cloud commands use it.

Common tasks

TaskEndpoint
Send a message as your backendPOST /api/machine/sessions/submit
Send a message as a browserPOST /api/client/sessions/input
Mint a token for a browserPOST /api/machine/client-tokens
Load a conversation on page loadGET /api/client/sessions/{id}
Watch a turn happenGET /api/client/sessions/{id}/events/stream
Answer an async or browser toolPOST /api/machine/sessions/{id}/calls/settle
Resume an interruptPOST /api/client/sessions/{id}/interrupt
Drive a chat frontendPOST /api/channels/ag-ui/agents/{id}/run
Manage projects and keysCloud API

Base URL

Put your engine's host in front of every path.

  • On a server you host: your subs serve address, such as http://localhost:8080. All four APIs share it.
  • In the cloud: the control plane, /api/v1, is https://api.substructure.ai. The client and machine APIs are on your app's own engine URL. subs open shows that URL, and you give it to clients with their token.

Headers

HeaderWhen
Authorization: Bearer <token>Every request. Each API takes a different token. See its section.
Content-Type: application/jsonAny request with a body.
Accept: text/event-streamThe streaming and AG-UI endpoints.

Errors

A failure returns a status and a JSON body. Most endpoints use the first shape. The machine submit and settle endpoints use the second.

{ "error": "session not found" }
{ "ok": false, "error": "effect is not pending" }
StatusMeaning
202The engine accepted the input.
400The request is malformed, or it failed validation.
401The credential is missing or invalid.
403Wrong tenant, not the session owner, or the wrong caller for this effect.
404Unknown session or effect.
409Conflict. The session is interrupted, the effect is not open, or a turn is already running or complete.
422The JSON body is not one the engine knows.

Streaming

The .../events/stream endpoints return text/event-stream. To resume after a drop, pass ?after_seq=<n>.

For every event type and the frame format, see Events.

The AG-UI endpoints send AG-UI protocol events instead. See AG-UI events.

Client API

For end-user clients: a browser, or your app's frontend.

Authentication. Authorization: Bearer <client-jwt>. A client token is short-lived. Your backend creates it through the machine API's client-tokens endpoint. Never send an API key to a browser.

The token sets the tenant and the owner. A client acts only as itself.

This API answers CORS preflight. Call it from a browser.

curl $BASE/api/client/sessions/input \
    -H "Authorization: Bearer $CLIENT_JWT" \
    -H "Content-Type: application/json" \
    -d '{"input":{"type":"client.message","agent_id":"my-agent","message":{"role":"user","content":"hi"}}}'

POST /api/client/sessions/input

Submit a ClientInput: a message, an action, an interrupt resume, or the result of a client tool. If you omit session_id, the engine creates one. If you send a turn_id that is complete, the engine returns that turn instead of running it again.

Headers Authorization: Bearer <client-jwt>, Content-Type: application/json

Request body

{
  "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "input": {
    "type": "client.message",
    "agent_id": "my-agent",
    "message": { "role": "user", "content": "what time is it?" }
  }
}

Response 202

{
  "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "turn_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a60",
  "queued": false
}

queued is true when the input asked to queue and the engine took the turn but has not started it.

GET /api/client/sessions/{session_id}

The session. It holds the status at the head, every open interrupt, messages, and the full message tree. messages is the active conversation, from the root to the head, ready to render. The tree is for clients that show branches. This is everything a client needs to rebuild a conversation when the page loads.

Headers Authorization: Bearer <client-jwt>

Path parameters

ParameterDescription
session_idThe session to fetch.

Response 200

{
  "session_id": "sess-1",
  "agent_id": "my-agent",
  "status": "idle",
  "interrupts": [],
  "messages": [
    { "id": "m1", "role": "user", "content": "hi" },
    { "id": "m2", "role": "assistant", "content": "Hello!" }
  ],
  "message_tree": {
    "head_id": "m2",
    "nodes": [
      { "message": { "id": "m1", "role": "user", "content": "hi" } },
      { "parent_id": "m1", "message": { "id": "m2", "role": "assistant", "content": "Hello!" } }
    ]
  }
}

status is idle, interrupted, or done, read at the active branch. It is interrupted only when an open interrupt pauses the head path. interrupts lists every open interrupt on any branch, as { interrupt_id, origin, reason, anchor? }. anchor is the message that the interrupt attaches to. Without it, the interrupt pauses every branch. A session that has never received input returns 404.

GET /api/client/sessions/{session_id}/events/stream

Stream a session's events.

Headers Authorization: Bearer <client-jwt>, Accept: text/event-stream

Path parameters

ParameterDescription
session_idThe session to stream.

Query parameters

ParameterTypeDescription
turn_idstringOptional. Limit to one turn. The stream closes when that turn completes.
after_seqintegerOptional. Resume after this seq.

Response text/event-stream. See Streaming.

POST /api/client/sessions/{session_id}/interrupt

Pause the session's active branch. The interrupt attaches to the current head. Other branches keep running. See Interrupts. If you omit interrupt_id, the engine creates one.

Headers Authorization: Bearer <client-jwt>, Content-Type: application/json

Path parameters

ParameterDescription
session_idThe session to interrupt.

Request body

{
  "reason": "confirm",
  "payload": { "message": "Send the email?" }
}

Response 200

{ "ok": true, "interrupt_id": "int-1" }

POST /api/channels/ag-ui/agents/{agent_id}/run

Run a turn from an AG-UI client.

Headers Authorization: Bearer <client-jwt>, Content-Type: application/json, Accept: text/event-stream

Path parameters

ParameterDescription
agent_idThe agent to run.

Request body: a RunAgentInput.

{
  "threadId": "sess-1",
  "runId": "turn-1",
  "messages": [{ "id": "m1", "role": "user", "content": "hi" }],
  "tools": [],
  "context": [],
  "state": {},
  "resume": []
}

Response text/event-stream of AG-UI events.

event: RUN_STARTED
data: {"type":"RUN_STARTED","threadId":"sess-1","runId":"turn-1"}

event: TEXT_MESSAGE_CONTENT
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"m2","delta":"Hi"}

event: RUN_FINISHED
data: {"type":"RUN_FINISHED","threadId":"sess-1","runId":"turn-1"}

POST /api/channels/ag-ui/agents/{agent_id}/connect

Send the session as one AG-UI snapshot. Use it to reconnect.

Headers Authorization: Bearer <client-jwt>, Content-Type: application/json, Accept: text/event-stream

Path parameters

ParameterDescription
agent_idPresent to match the other route. Only threadId in the body is used.

Request body: threadId names the session. runId is optional and labels the snapshot frames.

{ "threadId": "sess-1" }

Response text/event-stream: RUN_STARTED, MESSAGES_SNAPSHOT, RUN_FINISHED, then close.

Machine API

For your own backend and for worker callbacks. Server side only.

Authentication. Authorization: Bearer <api-key>, the app's API key. It has full tenant privileges, so keep it on your server and never send it to a browser. This API has no CORS. Use it to create client tokens, to submit for a user, and to end async calls.

curl $BASE/api/machine/client-tokens \
    -H "Authorization: Bearer $API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"identity":{"id":"user_42"}}'

POST /api/machine/client-tokens

Create a client JWT for a browser session.

Headers Authorization: Bearer <api-key>, Content-Type: application/json

Request body. identity.id is required. ttl_seconds defaults to 600.

{
  "identity": { "id": "user_42", "metadata": { "plan": "pro" } },
  "ttl_seconds": 600
}

Response 200. expires_at is Unix seconds.

{ "token": "eyJhbGciOiJIUzI1NiJ9...", "expires_at": 1784000000 }

POST /api/machine/sessions/submit

Submit a ClientPayload from your backend. The owner comes from your key and from the request's identity.

Headers Authorization: Bearer <api-key>, Content-Type: application/json

Request body. agent_id and identity.id are required. Set queue: true at the top level to hold a message payload for the next turn. Without it, a submit that arrives while a turn runs is refused.

{
  "agent_id": "my-agent",
  "identity": { "id": "user_42" },
  "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "payload": {
    "type": "client.message",
    "message": { "role": "user", "content": "hi" }
  }
}

Response 202

{
  "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "turn_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a60",
  "queued": false
}

POST /api/machine/sessions/{session_id}/calls/settle

End a worker-handled tool or LLM call by its effect id, outside the decision that started it. This is how an async call reports its result later.

Headers Authorization: Bearer <api-key>, Content-Type: application/json

Path parameters

ParameterDescription
session_idThe session the call belongs to.

Request body. One of four shapes. response is an LlmResponse. code is an ErrorCode:

{ "type": "tool.result", "id": "call_abc", "result": { "content": [{ "type": "text", "text": "sunny" }] } }
{ "type": "tool.error",  "id": "call_abc", "error": "upstream 503", "retryable": true, "code": "provider_error" }
{ "type": "llm.result",  "id": "llm_abc",  "response": { "model": "gpt-4", "content": "…" } }
{ "type": "llm.error",   "id": "llm_abc",  "error": "rate limited", "retryable": true }

Response 200

{ "ok": true }

POST /api/machine/workers/submit

Send a DecisionResponse from your worker into the engine, keyed by session_id and decision_id, instead of answering the POST the engine made.

There is no trigger here to fill in defaults. Each action that ends a call must name its id. Each llm.call must name its model.

Headers Authorization: Bearer <api-key>, Content-Type: application/json

Request body

{
  "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "decision_id": "dec_abc",
  "actions": [{ "type": "tool.result", "id": "call_abc", "result": { "content": [{ "type": "text", "text": "sunny" }] } }]
}

Response 200

{ "ok": true }

GET /api/machine/sessions/{session_id}/events/stream

The session event stream, with API key auth.

Headers Authorization: Bearer <api-key>, Accept: text/event-stream

Path parameters

ParameterDescription
session_idThe session to stream.

Query parameters

ParameterTypeDescription
turn_idstringOptional. Limit to one turn.
after_seqintegerOptional. Resume after this seq.

Response text/event-stream. See Streaming.

Admin API

Read sessions on a server you host. These routes only read.

Authentication. Authorization: Bearer <api-key>, the same key as the machine API. Results cover only the key's tenant. These routes exist only on a subs serve host, not in the cloud.

curl $BASE/api/admin/sessions -H "Authorization: Bearer $API_KEY"

GET /api/admin/sessions

List sessions, newest first.

Headers Authorization: Bearer <api-key>

Query parameters

ParameterTypeDescription
top_levelbooleanDefault true. Leaves out subagent sessions.
sortstringlast_event_desc (default), first_event_asc, first_event_desc, or wake_at_asc.
limitintegerPage size.
cursorstringPage cursor. Send next_cursor from the previous page.
session_idstringFilter to one session.
agent_idstringFilter by agent.

Response 200

{
  "items": [
    {
      "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "tenant_id": "default",
      "seq": 12,
      "first_event_at": "2026-07-14T10:00:00Z",
      "last_event_at": "2026-07-14T10:03:00Z",
      "wake_at": null,
      "top_level": true,
      "agent_id": "my-agent",
      "cost": "0.0012",
      "subagent_cost": "0",
      "status": "idle",
      "turn_id": null
    }
  ],
  "next_cursor": null
}

GET /api/admin/sessions/{session_id}

Fetch one session's current state. state is the engine's internal session record: the owner, the status, the cost so far, the message tree, and the open calls. Use it for diagnostics only. It might gain fields.

Headers Authorization: Bearer <api-key>

Path parameters

ParameterDescription
session_idThe session.

Response 200

{
  "seq": 12,
  "first_event_at": "2026-07-14T10:00:00Z",
  "last_event_at": "2026-07-14T10:03:00Z",
  "state": {
    "status": "idle",
    "owner": { "tenant_id": "default", "id": "user_42" },
    "cost": "0.0012",
    "message_tree": { "nodes": [], "head_id": "m2" }
  }
}

GET /api/admin/sessions/{session_id}/events

Fetch stored Events.

Headers Authorization: Bearer <api-key>

Path parameters

ParameterDescription
session_idThe session.

Query parameters

ParameterTypeDescription
after_seqintegerReturn events after this seq.
limitintegerPage size.

Response 200

[
  {
    "id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a70",
    "tenant_id": "default",
    "session_id": "0193a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
    "seq": 3,
    "occurred_at": "2026-07-14T10:00:03Z",
    "payload": {
      "type": "message.new",
      "message": { "id": "m2", "role": "assistant", "content": "Hi" }
    }
  }
]

GET /api/admin/sessions/{session_id}/events/stream

The same events over SSE. after_seq defaults to 0, which sends the full history.

Headers Authorization: Bearer <api-key>, Accept: text/event-stream

Path parameters

ParameterDescription
session_idThe session to stream.

Query parameters

ParameterTypeDescription
after_seqintegerDefault 0. Resume after this version.
limitintegerOptional limit on how much history is sent.

Response text/event-stream. See Streaming.

POST /api/admin/sessions/{session_id}/ag-ui/connect

Send the session as an AG-UI snapshot.

Headers Authorization: Bearer <api-key>, Content-Type: application/json, Accept: text/event-stream

Path parameters

ParameterDescription
session_idThe session to replay.

Request body: a RunAgentInput.

Response text/event-stream of AG-UI events.

Cloud API

The hosted control plane at https://api.substructure.ai. The subs cloud commands use it. See CLI.

Authentication. Authorization: Bearer <token>. Use your subs login token against the cloud, or a project API key against a local server.

A local subs serve serves the same routes as a single tenant. It ignores the {org} and {app} path segments. Every endpoint that writes returns 400:

{ "error": { "code": "unsupported_on_local", "message": "this operation is not supported on a local server; it only exists in the hosted cloud" } }
curl https://api.substructure.ai/api/v1/orgs -H "Authorization: Bearer $TOKEN"

GET /api/v1/orgs

List the orgs that you are a member of.

Headers Authorization: Bearer <token>

Response 200

[{ "id": "org_1", "name": "Acme", "role": "owner" }]

GET /api/v1/orgs/{org}/projects

List an org's projects. Returns an array of Project.

Headers Authorization: Bearer <token>

Path parameters

ParameterDescription
orgThe org ID.

GET /api/v1/projects/{project}

Fetch one project.

Headers Authorization: Bearer <token>

Path parameters

ParameterDescription
projectThe project ID.

Response 200. A Project. A local server omits balanceUsd and sessionCount.

{
  "id": "proj_abc",
  "organizationId": "org_1",
  "name": "my-bot",
  "createdAt": "2026-07-01T00:00:00Z",
  "balanceUsd": "42.50",
  "sessionCount": 1280
}

POST /api/v1/orgs/{org}/projects

Create a project. Hosted only. Use subs apply instead. It creates the project from the file and then applies everything that the file declares.

Headers Authorization: Bearer <token>, Content-Type: application/json

Path parameters

ParameterDescription
orgThe org to create the project in.

Request body

{ "name": "my-bot" }

Response 200

{
  "project": { "id": "proj_abc", "organizationId": "org_1", "name": "my-bot" }
}

This returns no secret. Each agent has its own signing secret, created by the first apply that gives that agent a worker.

PATCH /api/v1/projects/{project}

Rename a project. Hosted only. subs apply renames from the file instead.

Headers Authorization: Bearer <token>, Content-Type: application/json

Path parameters

ParameterDescription
projectThe project ID.

Request body: { "name": "new-name" }. Response 200: the updated Project.

DELETE /api/v1/projects/{project}

Delete a project. Hosted only. This deletes its keys, agents, sessions, and grants. It keeps the configuration history.

Headers Authorization: Bearer <token>

Path parameters

ParameterDescription
projectThe project ID.

GET /api/v1/projects/{project}/agents

List the agents the project's manifest declares. This never returns a signing secret. A secret exists for exactly the agents that have a workerUrl, so nothing reports its presence separately.

Headers Authorization: Bearer <token>

Path parameters

ParameterDescription
projectThe project ID.

Response 200

[
  {
    "id": "support",
    "config": { "llm": "claude", "model": "claude-sonnet-4-5" },
    "workerUrl": null
  },
  {
    "id": "triage",
    "config": null,
    "workerUrl": "https://my-worker.example.com/agent"
  }
]

workerUrl selects who decides. When it is set, the engine POSTs decisions there. When it is null, the engine decides for that agent by accepting its own proposal.

GET /api/v1/projects/{project}/agents/{agent}

Fetch one agent. This does not return its signing secret.

Headers Authorization: Bearer <token>

Response 200

{
  "id": "triage",
  "config": null,
  "workerUrl": "https://my-worker.example.com/agent"
}

GET /api/v1/projects/{project}/agents/{agent}/secret

Read one agent's signing secret. Asking for a secret is its own request, so no other response carries one. Open to any member of the organization: whoever can deploy the project can run its worker.

Headers Authorization: Bearer <token>

Response 200

{
  "signingSecret": "9f3c1a…"
}

Returns 409 if the agent has no worker. An engine-hosted agent signs nothing, so there is no secret to read.

POST /api/v1/projects/{project}/agents/{agent}/rotate-secret

Create a new signing secret. Open to any member of the organization. It takes no body and answers with the new secret alone. The old secret stops working immediately.

Response 200

{
  "signingSecret": "2b7e04…"
}

Returns 409 if the agent has no worker. An engine-hosted agent signs nothing.

GET /api/v1/projects/{project}/llm

List the [llm.*] blocks the manifest declares, and whether each one has a key. This never returns a key.

Response 200

[{ "name": "claude", "type": "anthropic", "baseUrl": null, "keyBound": true }]

PUT /api/v1/projects/{project}/llm/{block}/key

Set the customer key for one block. This endpoint only writes.

Request body

{ "key": "sk-…" }

Response 204. Returns 400 if the file does not declare the block, or if it is a worker block, which holds no key.

DELETE /api/v1/projects/{project}/llm/{block}/key

Remove the key. Calls on the block fail until you set another key.

Response 204.

GET /api/v1/projects/{project}/api-keys

List client API keys. Hosted only.

Headers Authorization: Bearer <token>

Response 200

[{ "key_id": "key_1", "label": "web-frontend", "created_at": "…", "last_used_at": null }]

POST /api/v1/projects/{project}/api-keys

Create a client API key. Hosted only.

Headers Authorization: Bearer <token>, Content-Type: application/json

Request body: { "label": "web-frontend" }.

Response 200. The key is shown one time only.

{ "api_key": "sk_live_…" }

DELETE /api/v1/projects/{project}/api-keys/{key_id}

Revoke a key. Hosted only.

Headers Authorization: Bearer <token>

Path parameters

ParameterDescription
projectThe project ID.
key_idThe key to revoke.

GET /api/v1/projects/{project}/sessions…

Session reads. They are the same as the Admin API, and a local server ignores the {app} segment: /sessions, /sessions/{id}, /sessions/{id}/events, /sessions/{id}/events/stream, and /sessions/{id}/ag-ui/connect.

Objects

The types used in the request and response bodies. ? marks an optional field. unknown is any JSON value. These match schemas/protocol.schema.json. See Typed bindings to generate them.

Messages

type Role = "system" | "user" | "assistant" | "tool"
type Content = string | (StoredContent | ContentPart)[]
// Send either shape. A `data:<mime>;base64,…` URI in any part is stored and
// recorded as a `blob://` ref; a URL is recorded as a `link`.
type StoredContent =
    | { type: "text"; text: string }
    | { type: "blob"; uri: string }
    | { type: "link"; uri: string; name?: string; mimeType?: string }
    | { type: "attachment"; id: string; mime: string; size: number; uri: string }
type ContentPart =
    | { type: "text"; text: string }
    | { type: "image_url"; image_url: { url: string } }
    | { type: "file"; file: { filename: string; file_data: string } }
    | { type: "input_audio"; input_audio: { data: string; format: string } }
    | { type: "video_url"; video_url: { url: string } }
type ToolCall = { id: string; type: string; function: { name: string; arguments: string } }
// A message. The id is optional when you submit one; the engine assigns it.
type Message = {
    id?: string
    role: Role
    content?: Content
    tool_calls?: ToolCall[]
    tool_call_id?: string
    name?: string
}

Client input

The body of POST /api/client/sessions/input is { session_id?, input: ClientInput }.

type ClientInput =
    | { type: "client.message"; agent_id: string; turn_id?: string; message: Message; stream?: boolean; queue?: boolean }
    | { type: "client.messages"; agent_id: string; turn_id?: string; messages: Message[]; stream?: boolean; client?: ClientContext }
    | { type: "client.append"; agent_id: string; turn_id?: string; messages: Message[]; stream?: boolean; client?: ClientContext; queue?: boolean }
    | { type: "client.action"; agent_id: string; turn_id?: string; name: string; args?: unknown }
    | { type: "interrupt.resume"; interrupt_id: string; payload?: unknown }
    | { type: "tool.result"; id: string; attempt?: number; result: ToolResult }
    | { type: "tool.error"; id: string; error: string; retryable: boolean; attempt?: number }

// Tools and context from the browser, added to the agent for this turn.
type ClientContext = {
    tools?: AgentTool[]
    context?: unknown[]
    state?: unknown
    forwarded_props?: unknown
}

AgentTool is the worker protocol's own type. See Agent config. The engine gives every tool a client submits handler: "client", whatever the tool says.

turn_id is an idempotency key. If you submit one that is complete, the engine returns that turn.

A session runs one turn at a time. The engine refuses a submit that arrives during a turn with turn_already_active.

queue: true takes the message instead. The engine starts it as the next turn when the running turn completes. The response carries queued: true while the message waits. Queued turns run in the order they arrived. Only client.message and client.append accept the flag.

client.messages replaces the conversation with the view that you submit. It branches where the two differ.

client.append adds its messages at the session head. It never branches. An append queued behind a running turn lands after that turn's reply. The engine drops any message whose ID it already recorded. Use it to sync new messages from an outside conversation.

Client payload

The payload of POST /api/machine/sessions/submit. It is a client input without the addressing fields, which the machine sends separately.

type ClientPayload =
    | { type: "client.message"; message: Message; stream?: boolean }
    | { type: "client.messages"; messages: Message[]; stream?: boolean; client?: ClientContext }
    | { type: "client.append"; messages: Message[]; stream?: boolean; client?: ClientContext }
    | { type: "client.action"; name: string; args?: unknown }

Decision response

The body of POST /api/machine/workers/submit.

type DecisionResponse = {
    messages?: Message[]        // messages to record
    actions?: Action[]          // what the engine does next
    state?: unknown             // omitted or null keeps the current state
    agent?: AgentConfig         // omitted keeps the current config
}

Action, AgentConfig, AgentTool, McpServer, McpTools, AgentPlugin, and LlmTool are the worker protocol's own types, and this endpoint takes them unchanged. See Response fields, Actions, and Agent config.

LLM response

Used by the llm.result action and by settle. LlmResponse and Usage are worker protocol types. See Model requests and responses.

Retries and errors

RetryPolicy, RetryOverride, RetryConfig, and ErrorCode are worker protocol types. See Retries and Errors, and Retries and timeouts for what the engine does with them.

Events

A stored event, from the event stream and the .../events endpoints. The type field in payload says what it is. See Events.

type Event = {
    id: string
    tenant_id: string
    session_id: string
    seq: number                  // the sequence in this session; the SSE frame id
    occurred_at: string          // RFC 3339
    payload: { type: string; [field: string]: unknown }
}

type StreamDelta = {             // the llm.token.delta data
    type: "llm.token.delta"
    session_id: string
    call_id: string
    seq: number
    text?: string
    reasoning?: string
    tool_calls?: { id: string; name?: string; arguments?: string }[]
    finish_reason?: string
}

Project

The Cloud Project resource. One project is one subs.toml. A local server omits balanceUsd and sessionCount.

type Project = {
    id: string
    organizationId: string
    name: string
    createdAt?: string
    balanceUsd?: string          // decimal string
    sessionCount?: number
}

RunAgentInput

The AG-UI request body. Its fields are camelCase.

type RunAgentInput = {
    threadId: string             // the session id
    runId: string                // the turn id
    messages: { id?: string; role: string; content?: string; toolCallId?: string; toolCalls?: unknown[] }[]
    state?: unknown
    tools?: { name: string; description?: string; parameters?: unknown }[]
    context?: unknown[]
    forwardedProps?: unknown
    resume?: { interruptId: string; status: "resolved" | "cancelled"; payload?: unknown }[]
}

AG-UI events

The AG-UI run and connect streams send these. In each frame, event is the type and data is a camelCase JSON object.

RUN_STARTED, RUN_FINISHED, RUN_ERROR, MESSAGES_SNAPSHOT, TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, TEXT_MESSAGE_END, TOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_END, TOOL_CALL_RESULT, REASONING_START, REASONING_MESSAGE_START, REASONING_MESSAGE_CONTENT, REASONING_MESSAGE_END, REASONING_END. RUN_FINISHED also reports an interrupt when the agent paused. See AG-UI.

Next steps

  • Events: every event type on the stream.
  • Authentication: tokens, keys, and identity.
  • CLI: the commands that drive the cloud API.
  • Typed bindings: generate these objects in your language.