REST API
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.
| API | Prefix | Auth | For |
|---|---|---|---|
| Client | /api/client | Client JWT | End-user clients: a browser or an app backend. |
| Machine | /api/machine | API key | Your backend and your worker's callbacks. |
| Admin | /api/admin | API key | Reading sessions on a server you host. |
| Cloud | /api/v1 | API key | The hosted control plane. The subs cloud commands use it. |
Common tasks
| Task | Endpoint |
|---|---|
| Send a message as your backend | POST /api/machine/sessions/submit |
| Send a message as a browser | POST /api/client/sessions/input |
| Mint a token for a browser | POST /api/machine/client-tokens |
| Load a conversation on page load | GET /api/client/sessions/{id} |
| Watch a turn happen | GET /api/client/sessions/{id}/events/stream |
| Answer an async or browser tool | POST /api/machine/sessions/{id}/calls/settle |
| Resume an interrupt | POST /api/client/sessions/{id}/interrupt |
| Drive a chat frontend | POST /api/channels/ag-ui/agents/{id}/run |
| Manage projects and keys | Cloud API |
Base URL
Put your engine's host in front of every path.
- On a server you host: your
subs serveaddress, such ashttp://localhost:8080. All four APIs share it. - In the cloud: the control plane,
/api/v1, ishttps://api.substructure.ai. The client and machine APIs are on your app's own engine URL.subs openshows that URL, and you give it to clients with their token.
Headers
| Header | When |
|---|---|
Authorization: Bearer <token> | Every request. Each API takes a different token. See its section. |
Content-Type: application/json | Any request with a body. |
Accept: text/event-stream | The 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" }| Status | Meaning |
|---|---|
| 202 | The engine accepted the input. |
| 400 | The request is malformed, or it failed validation. |
| 401 | The credential is missing or invalid. |
| 403 | Wrong tenant, not the session owner, or the wrong caller for this effect. |
| 404 | Unknown session or effect. |
| 409 | Conflict. The session is interrupted, the effect is not open, or a turn is already running or complete. |
| 422 | The 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
| Parameter | Description |
|---|---|
session_id | The 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
| Parameter | Description |
|---|---|
session_id | The session to stream. |
Query parameters
| Parameter | Type | Description |
|---|---|---|
turn_id | string | Optional. Limit to one turn. The stream closes when that turn completes. |
after_seq | integer | Optional. 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
| Parameter | Description |
|---|---|
session_id | The 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
| Parameter | Description |
|---|---|
agent_id | The 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
| Parameter | Description |
|---|---|
agent_id | Present 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
| Parameter | Description |
|---|---|
session_id | The 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
| Parameter | Description |
|---|---|
session_id | The session to stream. |
Query parameters
| Parameter | Type | Description |
|---|---|---|
turn_id | string | Optional. Limit to one turn. |
after_seq | integer | Optional. 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
| Parameter | Type | Description |
|---|---|---|
top_level | boolean | Default true. Leaves out subagent sessions. |
sort | string | last_event_desc (default), first_event_asc, first_event_desc, or wake_at_asc. |
limit | integer | Page size. |
cursor | string | Page cursor. Send next_cursor from the previous page. |
session_id | string | Filter to one session. |
agent_id | string | Filter 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
| Parameter | Description |
|---|---|
session_id | The 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
| Parameter | Description |
|---|---|
session_id | The session. |
Query parameters
| Parameter | Type | Description |
|---|---|---|
after_seq | integer | Return events after this seq. |
limit | integer | Page 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
| Parameter | Description |
|---|---|
session_id | The session to stream. |
Query parameters
| Parameter | Type | Description |
|---|---|---|
after_seq | integer | Default 0. Resume after this version. |
limit | integer | Optional 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
| Parameter | Description |
|---|---|
session_id | The 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
| Parameter | Description |
|---|---|
org | The org ID. |
GET /api/v1/projects/{project}
Fetch one project.
Headers Authorization: Bearer <token>
Path parameters
| Parameter | Description |
|---|---|
project | The 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
| Parameter | Description |
|---|---|
org | The 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
| Parameter | Description |
|---|---|
project | The 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
| Parameter | Description |
|---|---|
project | The 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
| Parameter | Description |
|---|---|
project | The 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
| Parameter | Description |
|---|---|
project | The project ID. |
key_id | The 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.