> ## Documentation Index
> Fetch the complete documentation index at: https://vetta.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sessions

> Directly-controlled runs of an agent.

A **session** is one directly-controlled run of an [agent](/docs/api/agents). You create it, send input, stream [events](/docs/api/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

<ResponseField name="id" type="string">Unique id (e.g. `ses_01H...`).</ResponseField>
<ResponseField name="agent_id" type="string">The agent this session runs.</ResponseField>
<ResponseField name="agent_version" type="integer">The pinned agent version.</ResponseField>
<ResponseField name="deployment_id" type="string | null">The [deployment](/docs/api/deployments) that created this session, if any.</ResponseField>
<ResponseField name="identity_id" type="string | null">The [persona](/docs/api/identities) this session acts as, or `null` when the create call named none.</ResponseField>
<ResponseField name="computer_id" type="string | null">The [computer](/docs/api/computers) 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.</ResponseField>
<ResponseField name="status" type="string">Lifecycle state — see [below](#lifecycle).</ResponseField>
<ResponseField name="stop_reason" type="string | null">Why the last turn yielded control — see [below](#lifecycle). Read it whenever the session is not `running`; do not treat `idle` as success.</ResponseField>
<ResponseField name="window" type="string">Effective [completion window](/docs/concepts/completion-window) for this session.</ResponseField>
<ResponseField name="budget_micro_usd" type="integer">Per-session cap, in micro-USD. Always set: it falls back to the agent's `budget.max_task_micro_usd`.</ResponseField>
<ResponseField name="consumed_micro_usd" type="integer">Cumulative spend on this session so far, in micro-USD.</ResponseField>

<ResponseField name="token_usage" type="object">
  Cumulative token counts across five billed tiers.

  <Expandable title="token_usage">
    <ResponseField name="input" type="integer">Prompt tokens.</ResponseField>
    <ResponseField name="cache_write" type="integer">Tokens written to the prompt cache.</ResponseField>
    <ResponseField name="cache_read" type="integer">Tokens served from the prompt cache.</ResponseField>
    <ResponseField name="output" type="integer">Generated output tokens.</ResponseField>
    <ResponseField name="reasoning" type="integer">Reasoning tokens.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="pending_actions" type="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.

  <Expandable title="pending_actions[]">
    <ResponseField name="kind" type="string">`tool` (default) or `question`.</ResponseField>
    <ResponseField name="tool_call_id" type="string">The id you answer or confirm this row by.</ResponseField>
    <ResponseField name="name" type="string">The tool that was called — `ask_operator` on a question.</ResponseField>
    <ResponseField name="args" type="object">The call's arguments, verbatim.</ResponseField>
    <ResponseField name="question" type="object | null">Present only when `kind` is `"question"`: `{ prompt, fields }`, with one to three fields. Each field carries `key`, `label`, `type` and an optional `help` line, plus `optional: true` when it may be left blank. The same content as `args`, parsed, so a renderer need not know that `ask_operator`'s arguments happen to be the question.</ResponseField>
  </Expandable>

  A `kind: "tool"` row is resolved with [tool confirmations](#respond-to-a-tool-confirmation) and accompanies `stop_reason: "awaiting_approval"`. A `kind: "question"` row is resolved with [answers](#answer-a-question) and accompanies `awaiting_answer`. A session parked on both reports `awaiting_approval` and carries both.
</ResponseField>

<ResponseField name="output_schema" type="object | null">JSON Schema the final result must satisfy, inherited from the agent unless overridden at create.</ResponseField>
<ResponseField name="structured_output" type="object | null">The typed result, populated when the session goes idle satisfying `output_schema`. See [Structured outputs](/docs/capabilities/structured-outputs).</ResponseField>
<ResponseField name="last_seq" type="integer">Highest event `seq` emitted so far. See [Events](/docs/api/events).</ResponseField>
<ResponseField name="metadata" type="object">Arbitrary key/value pairs.</ResponseField>
<ResponseField name="started_at" type="string | null">When the session first started running.</ResponseField>
<ResponseField name="ended_at" type="string | null">When the session reached a terminal state (`completed`, `failed`, `cancelled`).</ResponseField>
<ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>

## Lifecycle

A session moves through these `status` values:

| Status      | Meaning                                                                                         |
| ----------- | ----------------------------------------------------------------------------------------------- |
| `queued`    | Accepted, waiting for capacity to start.                                                        |
| `running`   | The agent is actively executing a turn.                                                         |
| `idle`      | Stopped but resumable — waiting for input, approval, or the next fire. Carries a `stop_reason`. |
| `completed` | The agent declared the task done. Terminal.                                                     |
| `failed`    | An unrecoverable error ended the session. Terminal.                                             |
| `cancelled` | The session was cancelled by a caller. Terminal.                                                |

Whenever a turn yields control, `stop_reason` is set to one of:

| `stop_reason`         | Meaning                                                                                                                                                                                                                                                                                                                      |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `end_turn`            | The turn finished normally.                                                                                                                                                                                                                                                                                                  |
| `awaiting_input`      | The agent is waiting for more input.                                                                                                                                                                                                                                                                                         |
| `awaiting_approval`   | A tool needs approval — see `pending_actions`.                                                                                                                                                                                                                                                                               |
| `budget_paused`       | Work paused at the budget cap.                                                                                                                                                                                                                                                                                               |
| `interrupted`         | A caller interrupted the turn.                                                                                                                                                                                                                                                                                               |
| `max_iterations`      | The turn hit its step ceiling.                                                                                                                                                                                                                                                                                               |
| `error`               | The turn ended in error.                                                                                                                                                                                                                                                                                                     |
| `context_exhausted`   | The turn produced neither a message nor a tool call.                                                                                                                                                                                                                                                                         |
| `awaiting_delegation` | A [coordinator](/docs/team/coordinator) yielded because every thread it delegated to is still running. Nobody is being asked for anything — the children finishing is what unblocks it.                                                                                                                                           |
| `awaiting_answer`     | The agent asked a question and cannot continue without the answer — see [Answer a question](#answer-a-question). The question is a `pending_actions` row with `kind: "question"`. Reported only when *every* parked row is a question; a session parked on both reports `awaiting_approval` and still carries the questions. |

<Note>
  `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](/docs/api/files); scratch work stays in the sandbox.
</Note>

## 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`.

<ParamField body="agent_id" type="string" required>The agent to run.</ParamField>
<ParamField body="agent_version" type="integer">Pin a specific version. Defaults to the agent's `current_version`.</ParamField>
<ParamField body="message" type="string" required>Initial input. Starts the first turn immediately.</ParamField>
<ParamField body="window" type="string">Override the completion window (`immediate | priority | loose`).</ParamField>
<ParamField body="budget_micro_usd" type="integer">Per-session cap in micro-USD. Work pauses at the cap.</ParamField>
<ParamField body="deployment_id" type="string">Attribute this session to a [deployment](/docs/api/deployments), so its spend shows under that schedule in the [ledger](/docs/api/credits). Defaults to `null`.</ParamField>
<ParamField body="identity" type="string">The [persona](/docs/api/identities) this session acts as — an `idn_` id or the persona's name. The agent must hold a grant to it. Defaults to `null`.</ParamField>
<ParamField body="output_schema" type="object">JSON Schema the final result must satisfy. Overrides the agent default. See [Structured outputs](/docs/capabilities/structured-outputs).</ParamField>
<ParamField body="structured_output_required" type="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.</ParamField>
<ParamField body="metadata" type="object">Arbitrary metadata.</ParamField>

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/sessions \
    -H "authorization: Bearer sk_live_..." \
    -H "content-type: application/json" \
    -H "idempotency-key: $(uuidgen)" \
    -d '{ "agent_id": "agt_01H8XK...", "message": "Refund order #4821", "window": "immediate", "budget_micro_usd": 2000000 }'
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={"system"}
  {
    "id": "ses_01H9AB...",
    "object": "session",
    "agent_id": "agt_01H8XK...",
    "agent_version": 2,
    "deployment_id": null,
    "identity_id": null,
    "computer_id": "cmp_01H8YZ...",
    "status": "queued",
    "stop_reason": null,
    "window": "immediate",
    "budget_micro_usd": 2000000,
    "consumed_micro_usd": 0,
    "token_usage": { "input": 0, "cache_write": 0, "cache_read": 0, "output": 0, "reasoning": 0 },
    "pending_actions": [],
    "output_schema": null,
    "structured_output": null,
    "last_seq": 0,
    "metadata": {},
    "started_at": null,
    "ended_at": null,
    "created_at": "2026-08-20T17:05:00Z"
  }
  ```
</ResponseExample>

## Retrieve a session

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

```json Response theme={"system"}
{ "id": "ses_01H9AB...", "status": "idle", "stop_reason": "end_turn", "consumed_micro_usd": 41870, "last_seq": 142 }
```

## List sessions

`GET /v1/sessions` → `200 OK`, cursor-paginated. Filter with `?agent_id=`, `?deployment_id=`, and `?status=`. See [Pagination](/docs/api/pagination).

`?handoff_from=ses_…` lists the top-level sessions that root [handed off](#hand-off-to-other-agents) — 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.

<ParamField body="agents" type="string[]" required>One to six targets, each an `agt_` id or an agent name. Duplicates collapse to one.</ParamField>
<ParamField body="message" type="string" required>The task. The receiver sees it prefixed with the sending agent and session, so it can tell a handoff from a person.</ParamField>
<ParamField body="handoff_key" type="string">De-duplicates per target: a target still working under this key answers `already_running` with that session instead of starting another; [send](#send-input) follow-ups to that session.</ParamField>
<ParamField body="budget_micro_usd" type="integer">Per-receiver cap in micro-USD; clamped to the target agent's `max_task_micro_usd`.</ParamField>

A refused target is a **row, not an error**: one target the policy does not allow does not stop the others.

```json Response theme={"system"}
{
  "object": "handoff_batch",
  "handoffs": [
    { "object": "handoff", "agent_id": "agt_01H9AB...", "session_id": "ses_01H9AC...", "status": "queued", "handoff_key": "trailer-cut" }
  ],
  "refusals": [
    { "object": "handoff_refusal", "agent": "producer", "code": "forbidden", "message": "lead may not hand off to producer" }
  ]
}
```

Only the sender's own faults answer with the [error envelope](/docs/api/errors): 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](/docs/api/events). The reply is the **acceptance**, not a session snapshot: the turn has not run yet.

<ParamField body="message" type="string" required>The message text to send.</ParamField>
<ParamField body="interrupt" type="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`.</ParamField>
<ParamField body="queue" type="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`.</ParamField>
<ParamField body="idempotency_key" type="string">Accepted and ignored. De-duplicate a retried send with the `Idempotency-Key` **header** — see [Idempotency](/docs/api/overview#idempotency); this body field does nothing.</ParamField>

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.

```bash theme={"system"}
curl -fsSL https://api.vetta.sh/v1/sessions/ses_01H9AB.../messages \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "message": "Actually, refund order #4822 instead", "interrupt": true }'
```

```json Response theme={"system"}
{ "session_id": "ses_01H9AB...", "status": "running", "accepted_seq": 143 }
```

`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](#cancel-a-session), the session stays resumable.

<ParamField body="message" type="string">Optional input to apply after interrupting.</ParamField>

```bash theme={"system"}
curl -fsSL https://api.vetta.sh/v1/sessions/ses_01H9AB.../interrupt \
  -H "authorization: Bearer sk_live_..."
```

```json Response theme={"system"}
{ "id": "ses_01H9AB...", "status": "idle", "stop_reason": "interrupted" }
```

## 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](#the-session-object) with the resolved call removed from `pending_actions`. Like a send, the reply is the acceptance: the tool has not run yet.

<ParamField body="tool_call_id" type="string" required>The id from `pending_actions[].tool_call_id` (also carried on the `tool.confirm` [event](/docs/api/events)).</ParamField>
<ParamField body="decision" type="string" required>`allow` to run the tool, or `deny` to reject it.</ParamField>
<ParamField body="reason" type="string">On `deny`, fed back to the model as the tool result.</ParamField>
<ParamField body="scope" type="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`.</ParamField>

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.

```bash theme={"system"}
curl -fsSL https://api.vetta.sh/v1/sessions/ses_01H9AB.../tool_confirmations \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "tool_call_id": "call_01H...", "decision": "allow" }'
```

```json Response theme={"system"}
{ "id": "ses_01H9AB...", "object": "session", "status": "running", "pending_actions": [] }
```

## 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](#the-session-object) with the answered question removed from `pending_actions`. Scope `sessions:write`.

<ParamField body="tool_call_id" type="string" required>The id from `pending_actions[].tool_call_id` on the row you are answering.</ParamField>
<ParamField body="answers" type="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.</ParamField>

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.

```bash theme={"system"}
curl -fsSL https://api.vetta.sh/v1/sessions/ses_01H9AB.../answers \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "tool_call_id": "call_01H...", "answers": { "mailbox": "hello@acme.com", "tone": "warm" } }'
```

```json Response theme={"system"}
{ "id": "ses_01H9AB...", "object": "session", "status": "running", "pending_actions": [] }
```

## 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.

<ParamField body="cap_micro_usd" type="integer" required>The new per-session cap, in micro-USD.</ParamField>

<Warning>
  **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.
</Warning>

```bash theme={"system"}
curl -fsSL -X PATCH https://api.vetta.sh/v1/sessions/ses_01H9AB.../budget \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "cap_micro_usd": 5000000 }'
```

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](#interrupt-a-session) instead if you want to keep the session resumable.

```bash theme={"system"}
curl -fsSL https://api.vetta.sh/v1/sessions/ses_01H9AB.../cancel \
  -H "authorization: Bearer sk_live_..."
```

```json Response theme={"system"}
{ "id": "ses_01H9AB...", "status": "cancelled" }
```

## 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](/docs/api/proxy): it is captured content. A boundary with nothing stored — or a session in another organization — is `404 not_found`.

```bash theme={"system"}
curl -fsSL https://api.vetta.sh/v1/sessions/ses_01H9AB.../workspaces/end \
  -H "authorization: Bearer sk_live_..." -o workspace.tar.gz
```

<Card title="Stream events" icon="bolt" href="/docs/api/events">
  List and stream a session's events, resumable by `seq`.
</Card>
