> ## 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](https://vetta.sh/docs/api/agents.md). You create it, send input, stream [events](https://vetta.sh/docs/api/events.md), 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

**id** `string`

Unique id (e.g. `ses_01H...`).

**agent\_id** `string`

The agent this session runs.

**agent\_version** `integer`

The pinned agent version.

**model** `string | null`

The model the latest [send](#send-input) picked, which the next turn runs on. `null` until a send picks one: the session then runs on its pinned agent version's model.

**mode** `string`

`build` or `plan` — see [Plan mode](https://vetta.sh/docs/concepts/session-operations.md#plan-mode). `build` until the create or a [send](#send-input) picks `plan`.

**deployment\_id** `string | null`

The [deployment](https://vetta.sh/docs/api/deployments.md) that created this session, if any.

**identity\_id** `string | null`

The [persona](https://vetta.sh/docs/api/identities.md) this session acts as, or `null` when the create call named none.

**computer\_id** `string | null`

The [computer](https://vetta.sh/docs/api/computers.md) 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.

**status** `string`

Lifecycle state — see [below](#lifecycle).

**stop\_reason** `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.

**stop\_code** `string | null`

Why a `failed` session stopped: the [error code](https://vetta.sh/docs/api/errors.md) of its last attempt, as its final `session.idle` event carries it in `code`, or `internal_error` when the platform gave the session up. `null` on every session that did not fail; a session that ends `idle` with `stop_reason: "error"` is resumable and has none. When the turn's model call failed, `rate_limited` means the model was busy, `feature_not_configured` means our model service refused the call (not your request; try again later), `validation_failed` means the model could not accept the request, and `internal_error` covers anything else. A code, never a message.

**window** `string`

Effective [completion window](https://vetta.sh/docs/concepts/completion-window.md) for this session.

**budget\_micro\_usd** `integer`

Per-session cap, in micro-USD. Always set: it falls back to the agent's `budget.max_task_micro_usd`.

**consumed\_micro\_usd** `integer`

Cumulative spend on this session so far, in micro-USD.

**token\_usage** `object`

Cumulative token counts across five billed tiers.

**token\_usage**

**input** `integer`

Prompt tokens.

**cache\_write** `integer`

Tokens written to the prompt cache.

**cache\_read** `integer`

Tokens served from the prompt cache.

**output** `integer`

Generated output tokens.

**reasoning** `integer`

Reasoning tokens.

**pending\_actions** `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.

**pending\_actions\[]**

**kind** `string`

`tool` (default) or `question`.

**tool\_call\_id** `string`

The id you answer or confirm this row by.

**name** `string`

The tool that was called — `ask_operator` on a question.

**args** `object`

The call's arguments, verbatim.

**question** `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.

**parked\_seq** `integer`

The event `seq` at the start of the turn that parked this row. `tool_call_id` is derived from the call, so the same call proposed again in a later turn has the same id; `parked_seq` tells the two apart. Absent on rows parked before it was recorded.

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.

**output\_schema** `object | null`

JSON Schema the final result must satisfy, inherited from the agent unless overridden at create.

**structured\_output** `object | null`

The typed result, populated when the session goes idle satisfying `output_schema`. See [Structured outputs](https://vetta.sh/docs/capabilities/structured-outputs.md).

**last\_seq** `integer`

Highest event `seq` emitted so far. See [Events](https://vetta.sh/docs/api/events.md).

**metadata** `object`

Arbitrary key/value pairs.

**started\_at** `string | null`

When the session first started running.

**ended\_at** `string | null`

When the session reached a terminal state (`completed`, `failed`, `cancelled`).

**created\_at** `string`

Creation timestamp.

## 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](https://vetta.sh/docs/team/coordinator.md) 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](https://vetta.sh/docs/api/files.md); 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`.

**agent\_id** `string` *required*

The agent to run.

**agent\_version** `integer`

Pin a specific version. Defaults to the agent's `current_version`.

**message** `string` *required*

Initial input. Starts the first turn immediately.

**window** `string`

Override the completion window (`immediate | priority | loose`).

**budget\_micro\_usd** `integer`

Per-session cap in micro-USD. Work pauses at the cap.

**deployment\_id** `string`

Attribute this session to a [deployment](https://vetta.sh/docs/api/deployments.md), so its spend shows under that schedule in the [ledger](https://vetta.sh/docs/api/credits.md). Defaults to `null`.

**identity** `string`

The [persona](https://vetta.sh/docs/api/identities.md) this session acts as — an `idn_` id or the persona's name. The agent must hold a grant to it. Defaults to `null`.

**output\_schema** `object`

JSON Schema the final result must satisfy. Overrides the agent default. See [Structured outputs](https://vetta.sh/docs/capabilities/structured-outputs.md).

**structured\_output\_required** `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.

**metadata** `object`

Arbitrary metadata.

**mode** `string`

`plan` or `build`. `plan` starts the session in [plan mode](https://vetta.sh/docs/concepts/session-operations.md#plan-mode), so it plans from its first message. The same rules as a [send's](#send-input)`mode`: refused on a harness that cannot hold a call for approval, and only a person sets it. Defaults to `build`.

```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 }'
```

```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,
  "stop_code": 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"
}
```

## 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](https://vetta.sh/docs/api/pagination.md).

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

**agents** `string[]` *required*

One to six targets, each an `agt_` id or an agent name. Duplicates collapse to one.

**message** `string` *required*

The task. The receiver sees it prefixed with the sending agent and session, so it can tell a handoff from a person.

**handoff\_key** `string`

De-duplicates per target: a target still working under this key answers `already_running` with that session instead of starting another. Under a room's key (`room:<sender id>`) the words are delivered into that session for you, and it takes no [send](#send-input) of yours — it answers into the room; under any other key, send follow-ups to it yourself.

**budget\_micro\_usd** `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.

```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](https://vetta.sh/docs/api/errors.md): a sender that is a thread or a member's session in a room (`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](https://vetta.sh/docs/api/events.md). The reply is the **acceptance**, not a session snapshot: the turn has not run yet.

**message** `string` *required*

The message text to send.

**interrupt** `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`.

**queue** `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`.

**model** `string`

A model id from [`GET /v1/models`](https://vetta.sh/docs/api/models). The session runs on it from this message's turn on, and later turns keep it until another send picks again. The send is quoted on it, so an unknown id is `400 validation_failed` and a model that does not serve the session's completion window is `400 window_unavailable`. The agent itself is not changed — update the agent too if new sessions should start on it.

**mode** `string`

`plan` or `build`. `plan` runs the session read-only from this message's turn on: it can research, ask and write a plan, and builds only after a person approves it. `build` leaves plan mode. A session whose harness cannot hold a call for approval refuses `plan` with `400 validation_failed` naming `mode`. Only a person sets it: a send an agent makes through the control-plane tools is `403 forbidden` naming `mode`. See [Plan mode](https://vetta.sh/docs/concepts/session-operations.md#plan-mode).

**idempotency\_key** `string`

Accepted and ignored. De-duplicate a retried send with the `Idempotency-Key`**header** — see [Idempotency](https://vetta.sh/docs/api/overview.md#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.

A member's session in a room — one [handed off](#hand-off-to-other-agents) under the key `room:<id>` — answers into that room, so a send to it (and an interrupt that carries a `message`) returns `403 forbidden`. Speak in the room instead; stopping it, approving its calls and answering its questions work as on any session.

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

**A session started for a board card says when that card has moved on.** A session the [board](https://vetta.sh/docs/team/board.md) (or a schedule) started for a card can still be messaged after the card is closed or another session has been started on it. The send goes through as usual and the session acts on it, but it no longer holds that card. The reply then adds a `hint` naming the card: that it is `done`, or which session holds it now, so you can send your guidance there or put it on the card. Any other send has no `hint`.

```json Response theme={"system"}
{ "session_id": "ses_01H9AB...", "status": "running", "accepted_seq": 143, "hint": "Card crd_01H9CD..., which this session was started for, is now held by session ses_01H9EF...: send guidance for that card there instead." }
```

**A channel's session runs nothing.** A top-level session whose `metadata.room` is its own agent's id is a channel's transcript. A send to it — and the first `message` when it is created — is recorded on its log as the person's message (`message.completed` with `role: "user"`, under the `Idempotency-Key`), and no turn follows. Nothing is quoted, `interrupt` and `queue` change nothing, and `status` stays as it was. To ask a member, [hand off](#hand-off-to-other-agents) from it.

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

**message** `string`

Optional input to apply after interrupting.

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

**tool\_call\_id** `string` *required*

The id from `pending_actions[].tool_call_id` (also carried on the `tool.confirm`[event](https://vetta.sh/docs/api/events.md)).

**decision** `string` *required*

`allow` to run the tool, or `deny` to reject it.

**reason** `string`

On `deny`, fed back to the model as the tool result.

**scope** `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`.

**answers** `object`

A template install's answers as the person edited them, keyed by question `key` (a string, or a list for a multiple choice), at most four — a template asks at most four questions. `allow` only, and only for a call that applies a template: the install runs with these answers and every other argument as proposed. Answers the template's questions refuse, a `deny`, `scope: "session"`, or any other tool are rejected with `validation_failed` (400) naming `answers` or `answers.<key>`.

**shown** `string`

On an `allow` of a `list_as_template` call: the `digest` of the [listing checks](https://vetta.sh/docs/api/apps.md) the card showed. It is filed with the approval, and the listing is refused if what it would list has changed since.

An `allow` of a parked `propose_plan` builds the plan at the version the call names. If the plan has
been written since — by the agent, or by you through `PUT /plan` — it is refused with
`version_conflict` (409) naming `tool_call_id`, and the call stays parked: read the plan, then `deny`
with a `reason` so the agent proposes the version that is there now.

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": [] }
```

### Operator actions

An agent may propose a wallet or card action it cannot run — open a persona's wallet, change its
limits, freeze or unfreeze it, add the testnet faucet's test money, retry or refund a card — as a
parked `operator.request`. An `allow` from a signed-in owner or admin **runs it**, once, before the
reply: the persona must be one the session's agent acts as, or the agent must be the company's CEO,
or the allow is refused with `forbidden` (403) and nothing runs. A refusal from the action itself is
the allow's refusal, and the call stays parked. A repeated allow of the same call replays the first
answer. The agent reads what the action answered.

`GET /v1/sessions/{id}/operator_preview?tool_call_id=…` → `200`, scope `sessions:read`: what an
approval card shows for one parked `operator.request`, read from the account — the persona by name,
current and new limits, a card's label and what a refund returns — with `runnable` and, when it is
`false`, `why`.

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

**tool\_call\_id** `string` *required*

The id from `pending_actions[].tool_call_id` on the row you are answering.

**answers** `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.

```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": [] }
```

## Read the plan

`GET /v1/sessions/{id}/plan` → `200 OK`. Scope `sessions:read`. The session's living plan: `{ "object": "plan", "markdown": "...", "version": 3, "by": "owner" }`. `version` is `0` and `markdown` empty until one is written; `by` is `agent`, `owner`, or `null` at version `0`.

## Edit the plan

`PUT /v1/sessions/{id}/plan` → `200 OK`, carrying the plan. Scope `sessions:write`. The owner's edit, a compare-and-set.

**markdown** `string` *required*

The whole plan, at most 20,000 characters. `- [ ]` and `- [x]` lines are its to-dos.

**base\_version** `integer` *required*

The `version` you edited from.

The write wakes nothing: the agent reads the edit at its next turn.

```bash theme={"system"}
curl -fsSL -X PUT https://api.vetta.sh/v1/sessions/ses_01H9AB.../plan \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "markdown": "- [ ] Draft the brief\n- [ ] Publish it", "base_version": 3 }'
```

```json Response theme={"system"}
{ "object": "plan", "markdown": "- [ ] Draft the brief\n- [ ] Publish it", "version": 4, "by": "owner" }
```

**Errors** — `version_conflict` (409) naming `base_version` when the plan moved on since you read it; read it again and edit from there. `forbidden` (403) on a member's session in a room; `session_terminal` (409) on an ended session.

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

**cap\_micro\_usd** `integer` *required*

The new per-session cap, in micro-USD.

> [!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.

```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](https://vetta.sh/docs/api/proxy.md): 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] [Stream events](https://vetta.sh/docs/api/events.md)
>
> List and stream a session's events, resumable by `seq`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.