# Runtime HTTP API (https://condensate.dev/reference/http/)

> Every runtime HTTP route, grouped by task, with its error codes.

The runtime serves HTTP on `127.0.0.1:4741` by default. Set `CONDENSATE_AGENTS_PORT` to change the port. The listener binds to loopback only. Remote callers reach it through Tailscale Serve or another local front.

This page lists the runtime's HTTP routes, grouped by what they do, and the error codes a program can branch on. The [SDK](https://condensate.dev/guides/sdk/) wraps most of them and decodes their responses, so prefer it when you can. The request bodies and response schemas are defined in `packages/agents-host/src/http.ts` and in the SDK. [HTTP route index](https://condensate.dev/reference/http-routes/) lists every route on the runtime's listener, generated from the routers.

The optional `engine` field on a spawn selects the agent that runs the thread: Claude Code (`claude`), Codex (`codex`), or Condensate's own agent (`pi`). Without it, the host picks the engine from the model.

## Errors

Refusals return JSON with an `error` message. The refusals a program needs to branch on also carry a `code`, listed below. When a `code` is present, branch on it, not on the message text. Simple input errors, such as a bad `limit` or cursor, return a 400 with only `error`.

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `spawn-cwd-required` | A build thread has no `cwd`. Pass a worktree. |
| 400 | `spawn-cwd-fenced` | The `cwd` sits inside a checkout the host fences off. |
| 400 | `account-not-in-roster` | The spec names an account outside the workspace's account list, or, with no workspace, an account a workspace claims. The body lists the workspace's accounts. |
| 400 | `model-not-allowed` | The model is outside the workspace's allowed list. The body lists the allowed models. |
| 409 | `not-a-head` | A role change names a thread that is not a head. |
| 429 | `session-cap` | The host is at its live session cap. Retry after `Retry-After`. |
| 429 | `thread-list-rate-limited` | The client has spent its budget for first-page listings. Retry after `Retry-After`. |
| 503 | `draining` | The host is draining for a restart. Retry after `Retry-After`. |
| 503 | `spawns-paused` | New spawns are paused because free disk space is low. Retry after `Retry-After`. |

A path no route claims returns a 404 with `{ "error", "build" }`. `build` is the commit the host runs, the same one `GET /health` reports, so a route missing from an older build is easy to spot.

## Read threads

| Method | Route | Purpose |
| --- | --- | --- |
| GET | `/threads` | A page of thread summaries. Query: `limit` (default 50; values above 200 are clamped to 200), `after` (the `nextAfter` cursor), `sort` (`created`, the default, or `recent`), `archived` (`live`, the default, `archived` or `all`), `role`, `type`, `workspace`, `parent` (one thread's children), `attached` (threads held by an attached seat), and `seats` (`open`, the default, or `all`). |
| GET | `/threads/:id` | The thread row, its stage, the cost so far and the archive flag. |
| GET | `/threads/:id/tail` | A page of the durable event log. `after=<seq>` reads forward. `before=<seq>` reads older events, newest first. `limit` defaults to 200 and caps at 1000. `wait=<ms>` holds the request open for new events, up to 30 seconds. |
| GET | `/threads/:id/follow` | Server-sent durable events. Resumes from `after`, or from the `Last-Event-ID` header. |
| GET | `/threads/:id/stream` | Server-sent live deltas. Nothing is replayed, and a slow reader loses its oldest frames. |
| GET | `/threads/:id/agui` | The thread as an AG-UI event stream, from `after=<seq>`. |
| GET | `/threads/:id/agui-page` | One reverse page of the log as AG-UI frames, for chat clients that open at the end and page back. |
| GET | `/threads/:id/turn` | Whether a turn is running. |
| GET | `/threads/:id/head` | The thread's type and metadata, including its seat lease. |
| GET | `/threads/:id/steering` | Every steering turn in the thread, oldest first. |
| GET | `/threads/:id/attachments` | The thread's attachments. |
| GET | `/threads/:id/memory` | The memory recorded for the thread. |
| GET | `/threads/:id/tool-results/:seq` | The full body of one tool result. |
| GET | `/threads/:id/operations`, `/operations/:operation` | Delegations, subscription edges and long actions, or one of them. `POST` on the same paths starts an operation or reports its outcome. A second outcome for a finished operation returns 409 `operation-settled`. |
| GET | `/threads/:id/subscriptions` | The thread's subscriptions. |
| GET | `/threads/:id/compact` | Compaction status. |

Use `after` for forward reads and `before` for older history, and keep the two cursors apart. A forward page returns `events`, `nextAfter` and `hasMore`. A reverse page returns `prevBefore` and `hasMore`.

Poll the tail or follow route for changes in one thread. To watch the list, follow `GET /workspaces/:name/changes` and re-read only the threads that changed. The first page of `GET /threads` is rate-limited per client, keyed on the caller's address and its `x-condensate-client` header (or User-Agent), so repeated full-list polling gets a 429.

```sh
curl -fsS 'http://127.0.0.1:4741/threads?limit=20'
curl -fsS 'http://127.0.0.1:4741/threads/THREAD_ID/tail?limit=20'
```

Replace `THREAD_ID` with an ID from a listing response.

## Start work

| Method | Route | Purpose |
| --- | --- | --- |
| POST | `/spawn` | Create a thread from a spawn spec. Returns `{ threadId }`. Refused while the host is draining or spawns are paused. |
| POST | `/threads/:id/messages` | Append a message and return `202` with its `seq` and `ts`, before any model runs. Set `hold: true` to keep the message held for editing with `PATCH` or `DELETE /threads/:id/messages/:seq`. Use `from` to name a program that sent text no person typed. |
| POST | `/threads/:id/send` | Send a message and wait for the reply. With `async: true` it behaves like `messages` and returns `202`. |
| POST | `/threads/:id/fork` | Branch the conversation. The child continues the parent as it stood at `atSeq`. |
| POST | `/threads/:id/loop`, `/loop/continue`, `/loop/stop` | Start, continue or stop a loop on the thread. `GET /threads/:id/loop` reports its state. |
| GET, POST | `/threads/:id/seats` | List the thread's seats, or join a new one. `DELETE /threads/:id/seats/:name` removes a seat. |
| POST | `/threads/:id/seats/replace` | Change a seat's engine, model or placement. The seat keeps its name and its history. Returns `changed: false` when nothing differs. |
| PUT | `/threads/:id/seat-lease` | Record the lease that holds the thread. Body `{ "seatLease": "<id>" }`, or `null` to clear it. |
| POST | `/threads/:id/handoff` | Offer the thread to another seat. Then read `GET /threads/:id/handoffs`, and answer with `POST /threads/:id/handoffs/:fromSeq/accept` or `/decline`. |
| POST | `/threads/:id/compact` | Compact the thread's context now. |
| POST | `/threads/:id/events`, `/gate`, `/merge` | Write cycle events for review and merge flows. |
| POST | `/threads/:id/subscriptions` | Subscribe to the thread's events. The body can name a `filter`, a `note` and a `deliver` target. `DELETE /threads/:id/subscriptions/:watch` removes one. |
| POST | `/threads/:id/role` | Retire a head to `chat`. No other role change is accepted. |
| POST | `/threads/:id/ui/generate` | Generate an interface for the thread. |

These routes have side effects, and each has its own body schema. Use the SDK methods, or read the schema in `http.ts`, rather than guessing a body from the route name.

Acceptance means the host recorded the request. It does not mean the work succeeded. Read the durable log after an accepted request to see the outcome.

## Control a turn

| Method | Route | Purpose |
| --- | --- | --- |
| POST | `/threads/:id/interrupt` | Stop the running turn. |
| POST | `/threads/:id/approvals/:approvalId` | Resolve a tool approval. Body `{ "decision": "allow" }` or `{ "decision": "deny" }`. An approval that is already closed answers with `code: approval-closed`. |
| GET | `/threads/:id/questions/:requestId` | The status of a question the seat asked. |
| POST | `/threads/:id/questions/:requestId/answer`, `/dismiss`, `/continue` | Answer a question, dismiss it, or continue without an answer. |
| POST | `/threads/:id/recover` | Recover a thread. |
| DELETE | `/threads/:id?reason=` | Retire a thread. Nothing is deleted. The host appends one `archive` event to the thread's log. |
| POST | `/threads/:id/restore` | Bring a retired thread back. Returns 409 when the workspace's head slot was taken in the meantime. |

## Workspaces and memory

| Method | Route | Purpose |
| --- | --- | --- |
| GET | `/workspaces` | The configured workspaces. |
| GET | `/workspaces/:name` | One workspace, with its GitHub slug. |
| PUT | `/workspaces/:name/policy` | Replace the workspace policy. |
| GET | `/workspaces/:name/head` | The thread that holds the workspace's head role. |
| GET | `/workspaces/:name/approvals` | Every tool call waiting on approval in the workspace, each with the thread it stops. |
| GET | `/workspaces/:name/questions` | Open questions across the workspace. |
| GET | `/workspaces/:name/events` | Server-sent workspace events. |
| GET | `/workspaces/:name/changes` | Server-sent changes to the workspace's threads. Name the threads to watch with `threads`. |
| GET | `/workspaces/:name/tail` | Recent events across the workspace. |
| POST | `/workspaces/:name/memory/recall`, `/retain`, `/reflect` | Recall, store and reflect on workspace memory. |
| GET | `/workspaces/:name/memory` | Browse workspace memory. |

A workspace has one head. A `POST /spawn` that asks for a second head in the same workspace is refused with a 409.

## Review rooms and pull requests

| Method | Route | Purpose |
| --- | --- | --- |
| POST | `/workspaces/:name/rooms` | Arm a review room for a PR, or adopt an existing one by key. |
| GET | `/rooms/:id` | The room's state and its seats. |
| GET | `/workspaces/:name/rooms?repo=&pr=` | Find a PR's room and its newest round. |
| GET | `/rooms/capabilities` | Whether the verdict poster is available. |
| POST | `/rooms/:id/rounds` | Fire a review round. Body `{ "prompt": "...", "seat": "..." }`, both optional. Returns 409 when the room is not armed, or a round is running or the reviewer is busy. |
| GET | `/rooms/:id/rounds/:n` | One round. |
| POST | `/rooms/:id/finalize` | Post a round's verdict as a GitHub review. Returns 409 when the verdict is not ready, has been superseded, or is stale. |
| POST | `/workspaces/:name/prs/:number/agents` | Start a build thread on the PR's branch, in a worktree the runtime owns. |
| GET | `/workspaces/:name/prs/:number/receipt?repo=` | A read-only merge receipt: the verdict and CI at the exact head, the base chain, stacked children and the merge plan. |

## Code and GitHub reads

| Method | Route | Purpose |
| --- | --- | --- |
| GET | `/code/roots`, `/code/outline`, `/code/files`, `/code/read` | Browse and read the registered repositories. |
| POST | `/code/search` | Search code with a pattern. This is a POST because patterns do not fit a query string well. |
| GET | `/github/:owner/:repo/prs` | The repository's pull requests. |
| GET | `/github/:owner/:repo/prs/:number/diff`, `/comments`, `/checks` | The diff, the comments and the checks for one PR. |
| POST | `/github/overlap` | Run one duplicate-work pass. It is a dry run unless `CONDENSATE_PR_OVERLAP=on`. |

## Completions endpoint

`POST /v1/chat/completions` and `GET /v1/models` expose an OpenAI-compatible completions endpoint. Each call is one model call. It runs on one of the workspace's accounts, and fails over to another account if the first one refuses.

A completions call is not an agent thread. It has no tools and no durable multi-turn state. For agent work, use `POST /spawn`.

Each call writes one `completion` event to the workspace's completions thread, so the usage is recorded there.

| Header | Purpose |
| --- | --- |
| `x-condensate-workspace` | The workspace the call is charged to. Required, unless the host sets `CONDENSATE_COMPLETIONS_DEFAULT_WORKSPACE`. |
| `x-condensate-session` | An optional session label, up to 128 characters. |
| `x-condensate-traffic` | `work` (the default) or `smoke`. |

Set `stream: true` in the body to receive server-sent deltas. Image content is not supported in this version. `GET /v1/models` lists the models the endpoint can route, without the deprecated ones.

Errors use OpenAI's shape, `{ "error": { "message", "type", "code" } }`. Codes include `unknown_workspace` (400), `model_not_found` (404), `no_eligible_account` and `no_spendable_account` (409), `accounts_exhausted` (429) and `rate_limited` (429, with `Retry-After` when the provider gave a wait).

## Operating the host

| Method | Route | Purpose |
| --- | --- | --- |
| GET | `/health` | The build commit, the fields `/spawn` accepts, and any open memory alert. |
| GET | `/live` | Live threads, and any open account-pool exhaustion episodes. |
| GET | `/limits` | The build seat limit, read from the hub. |
| GET | `/host-load` | Load averages, CPU count and the booted iOS Simulators on the host. |
| GET | `/timings` | Per-request timings. |
| GET | `/metrics`, `/metrics/history`, `/metrics/estate`, `/metrics/keeper`, `/metrics/cost-per-merged-pr` | Runtime metrics. |
| GET, POST, DELETE | `/drain` | Read, arm or disarm a drain for a restart. `/drain/claim` manages claims. |
| GET | `/keeper/spawn-pause`, POST `/keeper/spawn-pause/clear` | Read or clear the spawn pause. |
| GET | `/attention`, `/attention/changes`, `/attention/follow`, `/attention/:id` | Items that need a person. |
| GET | `/incidents` | Failed-turn incidents. |
| GET | `/peers`, POST `/peers/external`, DELETE `/peers/external/:name` | Peers, and external peers that record threads without a local engine. |
| POST | `/previews`, GET `/previews`, DELETE `/previews/:id` | Local server previews. |

Account routes under `/accounts` are covered in [Accounts](https://condensate.dev/guides/accounts/).

Some operator routes check the `Host` and `Origin` headers and refuse callers that are not local. These include the Codex reset-credit route and the model-refusal clear.

During a drain or a spawn pause, new threads are refused with a 503 and a `Retry-After` header. Running turns continue. A draining response reports how many threads are still mid-turn.
