Skip to main content
A session is one directly-controlled run of an agent. You create it, send input, stream events, interrupt it, and cancel it. Sessions are durable — they can run for a long time and sit idle at storage cost only.

The session object

string
Unique id (e.g. ses_01H...).
string
The agent this session runs.
integer
The pinned agent version.
string | null
The deployment that created this session, if any.
string | null
The persona this session acts as, or null when the create call named none.
string | null
The computer this session provisioned for its filesystem and shell tools, or null when its toolset needs none. It is a real computer — it lists under GET /v1/computers as session {id} and takes exec, snapshot and fs/* — but it belongs to the session: DELETE on it is refused while the session can still run, and it is destroyed when the session ends.
string
Lifecycle state — see below.
string | null
Why the last turn yielded control — see below. Read it whenever the session is not running; do not treat idle as success.
string
Effective completion window for this session.
integer
Per-session cap, in micro-USD. Always set: it falls back to the agent’s budget.max_task_micro_usd.
integer
Cumulative spend on this session so far, in micro-USD.
object
Cumulative token counts across five billed tiers.
object[]
What the session is parked on. One array, two kinds of row, distinguished by kind — which defaults to "tool", so a row without it is a held tool call.A kind: "tool" row is resolved with tool confirmations and accompanies stop_reason: "awaiting_approval". A kind: "question" row is resolved with answers and accompanies awaiting_answer. A session parked on both reports awaiting_approval and carries both.
object | null
JSON Schema the final result must satisfy, inherited from the agent unless overridden at create.
object | null
The typed result, populated when the session goes idle satisfying output_schema. See Structured outputs.
integer
Highest event seq emitted so far. See Events.
object
Arbitrary key/value pairs.
string | null
When the session first started running.
string | null
When the session reached a terminal state (completed, failed, cancelled).
string
Creation timestamp.

Lifecycle

A session moves through these status values: Whenever a turn yields control, stop_reason is set to one of:
idle is resumable and completed is terminal — both are non-running. Always branch on stop_reason rather than treating idle as success. Anything worth keeping is written to Files; scratch work stays in the sandbox.

Create a session

POST /v1/sessions → 201 Created. Creates and starts the session. Inherits the agent’s configuration; you may override the window, set a per-session budget, and provide an initial message.
string
required
The agent to run.
integer
Pin a specific version. Defaults to the agent’s current_version.
string
required
Initial input. Starts the first turn immediately.
string
Override the completion window (immediate | priority | loose).
integer
Per-session cap in micro-USD. Work pauses at the cap.
string
Attribute this session to a deployment, so its spend shows under that schedule in the ledger. Defaults to null.
string
The persona this session acts as — an idn_ id or the persona’s name. The agent must hold a grant to it. Defaults to null.
object
JSON Schema the final result must satisfy. Overrides the agent default. See Structured outputs.
boolean
When true, a session that cannot produce a conforming object goes idle with stop_reason: "error". Overrides the agent default; requires an output_schema on the session or the agent.
object
Arbitrary metadata.

Retrieve a session

GET /v1/sessions/{id} → 200 OK with the current status, stop_reason, and usage.
Response

List sessions

GET /v1/sessions → 200 OK, cursor-paginated. Filter with ?agent_id=, ?deployment_id=, and ?status=. See Pagination. ?handoff_from=ses_… lists the top-level sessions that root handed off — the room’s member work — newest first. A thread the root delegated to is not a handoff; those are GET /v1/sessions/{id}/threads. ?handoff_key=room:ses_… lists the top-level sessions handed off under that exact key — the room’s seated members, whoever seated them — newest first.

Hand off to other agents

POST /v1/sessions/{id}/handoffs → 200 OK. Scope sessions:write. The session {id} — top-level and not yet ended — hands message to one or more agents of your organization, each started as a top-level session of its own marked metadata.handoff: { from_session, from_agent, key, depth }. Who may be reached is the sender’s agent’s handoffs policy, exactly as it governs the model’s own send_to_agent; the receiver’s budget is capped at its agent’s per-task cap.
string[]
required
One to six targets, each an agt_ id or an agent name. Duplicates collapse to one.
string
required
The task. The receiver sees it prefixed with the sending agent and session, so it can tell a handoff from a person.
string
De-duplicates per target: a target still working under this key answers already_running with that session instead of starting another; send follow-ups to that session.
integer
Per-receiver cap in micro-USD; clamped to the target agent’s max_task_micro_usd.
A refused target is a row, not an error: one target the policy does not allow does not stop the others.
Response
Only the sender’s own faults answer with the error envelope: a sender that is a thread (403 forbidden), has ended (409 session_terminal), or is not yours (404 not_found). A chain of handoffs is at most four deep.

Send input

POST /v1/sessions/{id}/messages → 202 Accepted. Sends input to a session and produces events. The reply is the acceptance, not a session snapshot: the turn has not run yet.
string
required
The message text to send.
boolean
When true, steer a running session in one call: the current turn stops at its next commit boundary and the new message is applied. This is the common case. Defaults to false.
boolean
When true, a send to a running session is accepted and held: the current turn finishes on its own and the message becomes the input of the turn that follows. On a session that is not running it is an ordinary send. Cannot be combined with interrupt. Defaults to false.
string
Accepted and ignored. De-duplicate a retried send with the Idempotency-Key header — see Idempotency; this body field does nothing.
Without interrupt or queue, sending to a running session returns 409 with code session_running — interrupt first, set interrupt: true to steer it, or queue: true to wait for it.
Response
accepted_seq is the session’s event high-water mark at the moment the send was accepted: every event this turn produces has seq > accepted_seq. That is what to wait on — poll GET /v1/sessions/{id}/events?after_seq=143, or wait for the session’s last_seq to pass it. Do not wait on status alone: the send returns before the first event is committed, so a status read taken immediately after can still describe the previous turn.

Interrupt a session

POST /v1/sessions/{id}/interrupt → 200 OK. Stops the current turn at its next commit boundary, preserving history and the sandbox, and returns the session idle with stop_reason: "interrupted". An optional message is applied as the next input. Unlike cancel, the session stays resumable.
string
Optional input to apply after interrupting.
Response

Respond to a tool confirmation

When a tool with permission: "ask" runs, the session goes idle with stop_reason: "awaiting_approval" and the blocking calls appear in pending_actions. Resolve each with: POST /v1/sessions/{id}/tool_confirmations → 202 Accepted, carrying the full session object with the resolved call removed from pending_actions. Like a send, the reply is the acceptance: the tool has not run yet.
string
required
The id from pending_actions[].tool_call_id (also carried on the tool.confirm event).
string
required
allow to run the tool, or deny to reject it.
string
On deny, fed back to the model as the tool result.
string
default:"call"
How far the allow reaches. call — the default, and what an omitted scope means — approves this one call. session grants the call’s tool for the rest of this session: every later call to that tool name runs without a confirmation. allow only; session with deny is rejected with validation_failed (400) naming scope.
A session grant is deliberately narrow. It covers one tool name, in one session, and it is never implied — you ask for it per grant. It lives in that session’s transcript, so it disappears when the session ends, is invisible to every other session, and is never written onto the agent. It does not apply to a parked question, and an explicit deny for a particular call still stands even while its tool is granted. When the parked call is the platform’s tool invoker (tools_execute, which runs any catalogue tool named in its tool argument), the grant is for that target tool: later tools_execute calls with the same tool run without a confirmation, and a different tool through the same invoker is confirmed as usual. The target is read from the parked call — the body carries nothing extra — and a grant for the invoker with no target grants nothing.
Response

Answer a question

An agent that needs something from a person parks the turn: the session goes idle with stop_reason: "awaiting_answer" and the question appears in pending_actions as a row with kind: "question", carrying a prompt and one to three fields. Answer it with: POST /v1/sessions/{id}/answers → 202 Accepted, carrying the full session object with the answered question removed from pending_actions. Scope sessions:write.
string
required
The id from pending_actions[].tool_call_id on the row you are answering.
object
required
One entry per field, keyed by fields[].key. A string, or an array of strings for a choice field with multiple: true. Every field must be answered, except one the question marked optional: true — say “no answer” by leaving that key out of the object. A key that is present is validated either way, so "" and [] are validation_failed, not a way to skip.
This is a separate route from tool_confirmations, not a variant of it. A held tool call is answered with a verb and no payload; a question is answered with a payload and no verb. Posting an answer for a kind: "tool" row is rejected with validation_failed (400) naming tool_call_id, and points you at the other route. A choice field accepts anything the person types unless it sets other: false, in which case only its listed options are accepted. The values are read back by the model as words, so send the option text and not an index.
Response

Raise the budget

PATCH /v1/sessions/{id}/budget → 200 OK. Scope sessions:write. A session that hit its cap stops with stop_reason: "budget_paused". Raise the cap and it becomes resumable.
integer
required
The new per-session cap, in micro-USD.
Raise only. A cap at or below what the session has already consumed is rejected with validation_failed (400) — lowering it would be retroactive, charging a limit against spend that already happened.
Returns the full session object with the new budget_micro_usd. Errors — validation_failed (400) for a cap that is not a raise; not_found (404); insufficient_credits (402) when the organization cannot cover the new cap.

Cancel a session

POST /v1/sessions/{id}/cancel → 200 OK. Terminally stops the session and moves it to cancelled. Any in-flight turn is stopped. Use interrupt instead if you want to keep the session resumable.
Response

Download a workspace

GET /v1/sessions/{id}/workspaces/{phase} → 200 OK with the archived working directory of a session’s sandbox, as it was at one boundary of its work: end (the session finished, or its idle sandbox was reclaimed), or card_start / card_end (it moved a board card to doing / done). start has none — a fresh sandbox’s disk is its base image. The body is a tar.gz; its sha-256 is in the x-workspace-sha256 header, and an unchanged workspace archives to the same hash. ?kind=diff returns the git diff HEAD of the repository in the workspace instead, when there was one. ?card_id=crd_… picks that card’s boundary; without it the newest one of the phase answers. Credential files (.env* and the platform’s own) and re-derivable caches such as node_modules are never archived. Needs audit:read, like stored proxy calls: it is captured content. A boundary with nothing stored — or a session in another organization — is 404 not_found.

Stream events

List and stream a session’s events, resumable by seq.