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

# REST API

> The api.pome.sh/v1 control plane — authentication, the shape every error takes, and the endpoint index.

## Description

The Pome REST API is the control plane at `https://api.pome.sh/v1`. It starts
sandboxes, seeds them with a world of your own, and hands back the evidence a run
produced. Everything the [CLI](/docs/cli) and the [MCP server](/docs/mcp) do,
they do by calling it.

Reach for it directly when you are bringing your own evaluation harness. Pome
gives you a deterministic world and a recorded tape; your harness decides what
counts as a pass. If you would rather have Pome grade the run too, that works
over the same API — see [finalize](/docs/api/endpoints#post-v1sandboxesidfinalize).

Every response is JSON except [the markdown run
report](/docs/api/endpoints#get-v1runsidreportmd), which is `text/markdown`.

## Base URL and versioning

```text theme={"dark"}
https://api.pome.sh/v1
```

`v1` is the only version. Responses are **tolerant-reader** by design: the
control plane may add fields to a response, so parse the keys you need and ignore
the rest rather than rejecting an unfamiliar body.

Two other hosts appear in responses, and neither is this API:

| Host            | What it serves                                                                                                                                             |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `twins.pome.sh` | The digital twins themselves. A sandbox's `per_twin[].api_url` and `.mcp_url` point here. Your agent talks to this host, with the sandbox's `agent_token`. |
| `app.pome.sh`   | The dashboard. `dashboard_url` in a finalize response links here.                                                                                          |

## Authentication

Every endpoint takes a team API key as a bearer token:

```bash theme={"dark"}
curl https://api.pome.sh/v1/me \
  -H "Authorization: Bearer $POME_API_KEY"
```

Keys are shaped `pme_<id>_<secret>` and are minted in the dashboard, or by
[`pome login`](/docs/cli/auth), which stores one where the CLI can find it. The
key is team-scoped: every sandbox, run, and task it can see belongs to that team.

<Warning>
  A sandbox's `agent_token` is **not** an API key. It is a short-lived bearer,
  scoped to one sandbox and expiring with it, and it authenticates calls to the
  twins on `twins.pome.sh` — never to `api.pome.sh/v1`. Hand that one to your
  agent; keep the `pme_` key out of the agent's environment.
</Warning>

Two failures are deliberately distinguishable:

| Code  | Type           | Meaning                                                                                                                       |
| ----- | -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `401` | `invalid_auth` | Malformed key, unknown id, or wrong secret. All three collapse into one answer so the API never reveals whether an id exists. |
| `403` | `revoked_key`  | The key was valid and has been revoked. Distinct on purpose — "wrong key" and "your key was turned off" need different fixes. |

## How errors arrive

Every error is the same envelope. Sibling pages link back to this table rather
than repeating it.

```json theme={"dark"}
{
  "error": {
    "type": "validation_failed",
    "message": "Seed is not valid for the github twin: repositories[0].name — Invalid input: expected string, received undefined",
    "details": {
      "error": "invalid_seed",
      "twin": "github",
      "issue_count": 1,
      "issues": [
        {
          "path": "repositories[0].name",
          "message": "Invalid input: expected string, received undefined",
          "code": "invalid_type"
        }
      ]
    },
    "request_id": "req_H0iS10S1UhAB"
  }
}
```

`type` is the coarse class and is drawn from a closed set. `details` is where the
specific failure lives — branch on `details.error` when you need to tell two
`validation_failed`s apart. **`request_id` is on every error**; quote it if you
ask us about one.

| Code  | `type`                   | What to do                                                                                                                                                   |
| ----- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400` | `validation_failed`      | The body was not JSON at all. Some endpoints answer `422` for the same thing — treat the two as one case rather than branching on the code.                  |
| `401` | `invalid_auth`           | Check the key.                                                                                                                                               |
| `402` | `quota_exceeded`         | Too many sandboxes open at once. Stop one, or raise the plan. `details.usage` names the numbers.                                                             |
| `403` | `revoked_key`            | The key was revoked.                                                                                                                                         |
| `404` | `not_found`              | No such id, **or** it belongs to another team. The two are never distinguished.                                                                              |
| `409` | `conflict`               | The request collided with the resource's state — an ungraded sandbox you asked to delete, or a capture that could not complete. `details.reason` says which. |
| `410` | `session_expired`        | The sandbox is closed. Nothing is recoverable; start a new one.                                                                                              |
| `413` | `validation_failed`      | The body is over the cap. See [Limits](#limits).                                                                                                             |
| `422` | `validation_failed`      | A field is wrong, and the message names it.                                                                                                                  |
| `500` | `internal_error`         | Ours. Retry once, then quote the `request_id`.                                                                                                               |
| `503` | `downstream_unavailable` | Something we depend on could not answer — a sandbox that would not provision, or evidence storage we could not reach. Retry.                                 |

<Note>
  Quota trips are `402`, never `429`. A `402` will not clear on its own — free a
  sandbox or raise the plan.
</Note>

### The error vocabulary worth branching on

These are the named failures a caller hits in normal use. Most arrive as `422`
under `details.error`. The last three carry a different code and live under
`details.reason` instead — each row says which key and which code.

| `details` key               | Raised by                     | Meaning                                                                                                                                          |
| --------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `invalid_seed`              | seed validate, sandbox create | A twin's own parser refused the world. Carries `twin`, `issue_count`, and up to 25 `issues[]`, each with a `path`.                               |
| `invalid_seed_envelope`     | seed validate, sandbox create | A multi-twin seed was not a per-twin envelope, or named a twin the sandbox does not have. Stray names come back in `stray_twins`.                |
| `malformed_scenario_seed`   | sandbox create                | The seed block inside a `task_source` markdown could not be parsed.                                                                              |
| `unknown_twins`             | seed validate, sandbox create | A twin id that is not mounted. `available` lists the real ones.                                                                                  |
| `too_many_twins`            | sandbox create                | More than three twins in one sandbox.                                                                                                            |
| `twin_snapshot_unavailable` | sandbox create                | A mounted twin has no image configured in this deploy. Ours, not yours.                                                                          |
| `empty_criteria`            | finalize                      | Zero criteria. A run with nothing to check is never graded.                                                                                      |
| `ungraded_session`          | sandbox delete                | `409`, under `details.reason`. You asked to delete a sandbox whose run has not been graded. Carries a `discard_token` to confirm with.           |
| `capture_incomplete`        | finalize                      | `409`, under `details.reason`. The tape could not be read.                                                                                       |
| `blob_absent`               | run trace / state download    | `404`, under `details.reason`. The run is yours, but that blob was never stored — self-host, `--no-upload`, or a row older than the upload path. |

## Endpoint index

Every endpoint is documented on [the endpoints page](/docs/api/endpoints) with
the same shape: what it does, an input table, what comes back, the errors worth
branching on, and one example call.

### Identity and quota

| Endpoint                                           | What it does                                                   |
| -------------------------------------------------- | -------------------------------------------------------------- |
| [`GET /v1/me`](/docs/api/endpoints#get-v1me)       | Who the key belongs to. The cheapest way to check a key works. |
| [`GET /v1/usage`](/docs/api/endpoints#get-v1usage) | How many sandboxes you have open, and how many you may.        |

### Seeds

| Endpoint                                                              | What it does                                                                     |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| [`POST /v1/seeds/validate`](/docs/api/endpoints#post-v1seedsvalidate) | Would this world boot, and if not, which field? No sandbox, no quota, no charge. |

### Sandboxes

| Endpoint                                                                              | What it does                                                                            |
| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| [`POST /v1/sandboxes`](/docs/api/endpoints#post-v1sandboxes)                          | Start a sandbox with one to three digital twins, optionally seeded with your own world. |
| [`GET /v1/sandboxes`](/docs/api/endpoints#get-v1sandboxes)                            | List your sandboxes, newest first.                                                      |
| [`GET /v1/sandboxes/:id`](/docs/api/endpoints#get-v1sandboxesid)                      | One sandbox, including its `agent_token`.                                               |
| [`POST /v1/sandboxes/:id/heartbeat`](/docs/api/endpoints#post-v1sandboxesidheartbeat) | Reset the idle timer on a sandbox you are still using.                                  |
| [`DELETE /v1/sandboxes/:id`](/docs/api/endpoints#delete-v1sandboxesid)                | Stop a sandbox. Refused on an ungraded run unless you confirm.                          |

### Grading vocabulary

| Endpoint                                                            | What it does                                                                 |
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| [`GET /v1/checks`](/docs/api/endpoints#get-v1checks)                | The closed set of `[code]` checks a twin can grade.                          |
| [`POST /v1/checks/render`](/docs/api/endpoints#post-v1checksrender) | Turn a picked check plus arguments into the exact sentence the grader binds. |
| [`POST /v1/checks/bind`](/docs/api/endpoints#post-v1checksbind)     | The inverse: does this sentence bind, and to what?                           |

### Runs and evidence

| Endpoint                                                                              | What it does                                                 |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| [`POST /v1/sandboxes/:id/finalize`](/docs/api/endpoints#post-v1sandboxesidfinalize)   | Grade the run and close the sandbox.                         |
| [`GET /v1/sandboxes/:id/evaluation`](/docs/api/endpoints#get-v1sandboxesidevaluation) | Poll an asynchronous finalize.                               |
| [`GET /v1/runs`](/docs/api/endpoints#get-v1runs)                                      | List runs, newest first.                                     |
| [`GET /v1/runs/:id`](/docs/api/endpoints#get-v1runsid)                                | One run: score, every per-criterion verdict, and its reason. |
| [`GET /v1/runs/:id/report.md`](/docs/api/endpoints#get-v1runsidreportmd)              | The same run as a rendered markdown report.                  |
| [`GET /v1/runs/:id/trace`](/docs/api/endpoints#get-v1runsidtrace)                     | A signed, short-lived download URL for the run's raw tape.   |
| [`GET /v1/runs/:id/state`](/docs/api/endpoints#get-v1runsidstate)                     | The same, for the twin's final state.                        |

## The loop, end to end

Four calls take you from nothing to a running agent in a world you wrote. Each
one links to its full entry.

```bash theme={"dark"}
export POME_API_KEY=pme_...

# 1. Does the world you wrote parse? Free, and no sandbox is started.
curl -sX POST https://api.pome.sh/v1/seeds/validate \
  -H "Authorization: Bearer $POME_API_KEY" -H 'Content-Type: application/json' \
  -d '{"twins":["github"],"seed":{
        "users":[{"login":"northwind","type":"Organization"}],
        "repositories":[{"owner":"northwind","name":"payments","default_branch":"main",
          "issues":[{"number":1,"title":"Refunds fail on partial capture","state":"open"}]}]}}'
# {"valid":true,"checked":["github"],"unchecked":[]}

# 2. Start a sandbox on that world.
curl -sX POST https://api.pome.sh/v1/sandboxes \
  -H "Authorization: Bearer $POME_API_KEY" -H 'Content-Type: application/json' \
  -d '{"twins":["github"],"task_source":"IyAuLgo=","seed":{ ... }}' > sandbox.json

# 3. Point your agent at the twin and let it work. Two values are all it needs.
export POME_GITHUB_REST_URL=$(jq -r .per_twin.github.api_url sandbox.json)
export POME_AUTH_TOKEN=$(jq -r .agent_token sandbox.json)

# 4. Read what your agent actually did, straight off the twin.
curl -s "$POME_GITHUB_REST_URL/_pome/events" -H "Authorization: Bearer $POME_AUTH_TOKEN"
```

Step 4 is the tape: one row per HTTP call the agent made, with method, path,
request body, and what the twin answered. It is live for as long as the sandbox
is, it costs nothing, and it is what a bring-your-own-eval harness scores against.
The [digital twin quickstarts](/quickstart/twins/github) work through reading it.

<Note>
  `task_source` in step 2 is `IyAuLgo=` — base64 for `# ..`, a stub. Today
  `POST /v1/sandboxes` requires a task even when you have brought a seed and will
  never read one. [The full entry](/docs/api/endpoints#post-v1sandboxes) explains
  why, and it is the one wart on this path.
</Note>

To have Pome grade the run instead of scoring it yourself, add a fifth call —
[finalize](/docs/api/endpoints#post-v1sandboxesidfinalize) — while the sandbox is
still live.

## Limits

| Limit                          | Value      | What happens past it                                                    |
| ------------------------------ | ---------- | ----------------------------------------------------------------------- |
| Twins per sandbox              | 3          | `422` `too_many_twins`.                                                 |
| Sandbox lifetime               | 30 minutes | The sandbox expires. Finalize before then, or the run cannot be scored. |
| Idle timeout                   | 30 minutes | Reset by any call through the twin, or by an explicit heartbeat.        |
| `POST /v1/sandboxes` body      | 4 MiB      | `413`.                                                                  |
| `POST /v1/seeds/validate` body | 4 MiB      | `413`.                                                                  |
| Finalize body                  | 256 KiB    | `413`. Blobs never travel in this body.                                 |
| Criteria per finalize          | 200        | `422`.                                                                  |
| Seed issues reported           | 25         | Truncated; `issue_count` always carries the true total.                 |

Concurrent sandboxes are a plan limit rather than a fixed number —
[`GET /v1/usage`](/docs/api/endpoints#get-v1usage) reports yours.

## See also

* [Endpoints](/docs/api/endpoints) — the full reference for every call above.
* [Digital twins](/docs/twins/coverage) — what each twin serves, and the shape of its seed.
* [CLI reference](/docs/cli) — the same loop, driven from a terminal.
* [MCP reference](/docs/mcp) — the same loop, driven by a coach agent.
* [Terminology](/docs/terminology) — what a sandbox, a run, and a criterion are.
