$POME_API_KEY for a team key (pme_…) and $POME_AUTH_TOKEN for
a sandbox’s agent_token. They are different credentials for different hosts —
see Authentication.
Identity and quota
GET /v1/me
Returns who the key belongs to. Takes no inputs. Returns —{ user, team, api_key }. user is { id, email }; team is
{ id, slug, plan_tier }; api_key is { id, name } for the key you presented,
so you can tell which of several keys is in play.
Errors — 401, 403. Nothing else: this is the cheapest way to prove a key
works before doing anything that costs.
Example
GET /v1/usage for what the team may currently do.
GET /v1/usage
Returns the live concurrency snapshot — how many sandboxes are open now, against the plan’s ceiling. Takes no inputs. Returns —{ period_start, period_end, sessions_used, sessions_quota, sessions_remaining, plan_tier }. sessions_used counts sandboxes open right
now, not sandboxes started this month, so it falls when you stop one.
sessions_remaining is clamped at zero rather than going negative.
sessions_quota comes from your plan, so read it here rather than assuming a
number — the values below are illustrative.
Errors — 401, 403.
Example
402 from POST /v1/sandboxes is this
number reaching zero.
Seeds
A seed is the world a digital twin starts from: the repositories, messages, customers, or threads your agent will find when it looks. Seeding is how a task becomes about your product rather than about our sample data. Two rules decide everything about the shape. A seed replaces the twin’s default world. It does not merge into it. Seed your own GitHub world and the twin’s sampleacme/api repository is gone —
GET /repos/acme/api answers 404. Anything your agent needs must be in the
seed you send.
Whether the seed is wrapped is decided by twins, never by its contents. One
twin takes the flat domain object that twin’s own parser owns. More than one
takes a per-twin envelope keyed by twin id:
twins array and nothing else. A twin
you name in twins but omit from the envelope boots its own default world; a key
naming a twin the sandbox does not have is a 422.
The seed file the CLI reads is always keyed by twin, even for one twin,
because the file travels on its own and has to say what it is for. The wire shape
above is different, and that is deliberate:
twins is right there in the request
body, so nothing has to be inferred. If you are converting a seed file for one
twin into a request, unwrap it — send the value, not the { "github": … }
wrapper.POST /v1/seeds/validate
Answers “would this world boot, and if not, which field?” — without starting a sandbox, reserving quota, or spending anything.
Returns —
{ valid: true, checked: [...], unchecked: [...] }. checked
lists twins whose parser ran and accepted the world. unchecked lists twins the
control plane has no parser for — those boot anyway, and saying so is more honest
than reporting valid: true as though we had looked.
This runs the twin’s own boot-time parser, the same function the pod calls
when it starts. It cannot accept a seed the pod would reject, and it cannot
reject one the pod would accept.
Errors — 422 with details.error of invalid_seed (with twin,
issue_count, and issues[], each carrying the path of a failing field),
invalid_seed_envelope, or unknown_twins. 400 if the body is not JSON.
413 over 4 MiB.
Example
details block from
POST /v1/sandboxes. One formatter, two callers — so a
client branches on one shape.
See also — POST /v1/sandboxes, which runs this same
check before it spends anything.
Sandboxes
A sandbox is what you start: one id, one 30-minute lifetime, and one to three digital twins your agent talks to. It is also the billing unit, so the plan limit counts sandboxes open at once.On the wire a sandbox is spelled
session — ses_ ids, session_id,
/v1/sessions. Both spellings of every path work permanently and hit the same
implementation; /v1/sandboxes is the current name and the one this page uses.POST /v1/sandboxes
Starts a sandbox and returns everything an agent needs to reach its twins.
Returns —
201 with the sandbox and its connection details.
The seed is checked before a sandbox is provisioned, so an unbootable world
costs you a
422 in well under a second rather than a spawn that fails.
Errors — 422 with details.error of invalid_seed,
invalid_seed_envelope, malformed_scenario_seed, unknown_twins,
too_many_twins, or twin_snapshot_unavailable; 422 with no details.error
when the body missed the schema, including the task_source refusal above.
402 quota_exceeded when too many sandboxes are already open — details.usage
carries the numbers. 413 over 4 MiB. 503 when a sandbox could not be
provisioned; retry.
Example
POST /v1/seeds/validate to check a world
first, and POST /v1/sandboxes/:id/finalize to
have the run graded.
GET /v1/sandboxes
Lists your sandboxes, newest first.
Returns — an array of sandboxes:
id, twins, state, twin_url,
created_at, ready_at, expires_at, closed_at. Compact rows only — no
agent_token, which is why this is the safe one to log.
Errors — 401, 403.
Example
GET /v1/sandboxes/:id for one, with its token.
GET /v1/sandboxes/:id
Returns one sandbox in full.
Returns — the list shape plus
agent_token, last_request_at, and the
task markdown the sandbox was started with. This response carries a live
bearer; treat it the way you treat the create response.
Errors — 404 when the id is unknown or belongs to another team. The two
are never distinguished, so a 404 is not evidence that an id does not exist.
Example
POST /v1/sandboxes/:id/heartbeat
to keep it alive.
POST /v1/sandboxes/:id/heartbeat
Resets the idle timer on a sandbox you are still using.
Returns —
204, with no body. Calls through the twin already reset the idle
timer, so this is only needed when your agent is thinking rather than calling —
a long model turn between tool calls, for instance.
This does not extend the 30-minute wall-clock lifetime. Nothing does. A
sandbox dies at expires_at however busy it has been.
Errors — 404 unknown or another team’s. 410 session_expired when the
sandbox is already closed, which is the signal to stop sending heartbeats.
Example
DELETE /v1/sandboxes/:id
Stops a sandbox.
Pome creates the run row at finalize, so a sandbox that is still open holds a
run nobody has graded. Deleting it throws that away. An unconfirmed delete of an
open sandbox is therefore refused rather than performed.
Returns —
200 with the closed sandbox, state now expired and
closed_at set.
Errors — 409 conflict with details.reason of ungraded_session on an
open sandbox. That body carries open_seconds, the task_name, and a
discard_token; repeat the call with ?confirm_discard=<token> to mean it. To
keep the run instead, finalize first — that grades
it and closes the sandbox, and no delete is needed afterwards. 404 unknown or
another team’s.
Example
POST /v1/sandboxes/:id/finalize.
Grading vocabulary
Pome grades[code] criteria by binding an English sentence to a declared check
and running that check’s predicate against the twin’s final state and its
recorded tape. The sentence has to match a declared template exactly, so these
two endpoints exist to keep you from hand-writing one that binds to nothing.
GET /v1/checks
Returns the closed set of[code] checks a twin declares.
Returns — for each check: its
id, the English template it renders, what the
predicate compares, the substrate it reads (final state, or the tape), and each
parameter with a valid example. Also a digest of the vocabulary, so a client can
tell whether its own pin agrees with the server’s.
The set is a property of the pinned grading package, identical for every team,
and it is what pome checks and the MCP list_checks tool read.
Errors — 422 when twin is missing or is not a mounted twin. A misspelled
twin and a twin that declares nothing are deliberately different answers.
Example
POST /v1/checks/render to fill one in.
POST /v1/checks/render
Turns picked checks plus arguments into the exact sentences the grader binds.
Returns —
{ twin, lines }, one rendered sentence per item in order. Pass a
line straight into a finalize criterion’s text.
Errors — 422 for an unknown twin, an unknown check id, a missing argument,
or a non-string argument value. The message names the path.
Example
POST /v1/checks/bind for the other
direction.
POST /v1/checks/bind
Answers the inverse question: does this sentence bind, and to what?
Returns —
{ twin, results }, one result per input in order. A sentence that
binds carries check_id. One that does not carries check_id: null and a
nearest list of the closest declared templates — usually enough to see what you
mis-typed.
This runs the same binding code the grader runs, so a check_id here is a
promise that the criterion will bind at finalize. Use it to lint criteria you
inherited or wrote by hand.
Errors — 422 for an unknown twin or a malformed body.
Example
[code] criterion it would score
unmatched. Write it as [model] if you want the narrator to comment on it, and
expect no score either way.
See also — Authoring tasks for what makes a
criterion discriminate rather than merely bind.
Runs and evidence
There are two ways to get evidence out of Pome, and they answer different questions. While the sandbox is alive, the twin serves its own state and its own tape, and both are free:/_pome/events is the tape: one row per HTTP call the agent made, with method,
path, request_body, the response, and whether the call mutated state.
Authorization headers arrive [REDACTED]. This is what a bring-your-own-eval
harness reads — no run row and no grading needed. The twin
quickstarts work through querying it.
After you finalize, Pome’s own verdict is on the run: a score, a per-criterion
pass or fail, and the reason each verdict was reached. That is what the rest of
this section covers.
The tape does not disappear when the sandbox does. trace_s3_key,
state_s3_key and events_jsonl_url on a run are storage keys — paths in a
private bucket, not URLs — and
GET /v1/runs/:id/trace and
GET /v1/runs/:id/state exchange one for a short-lived
signed download. That is the after-the-fact path: two curls, no dashboard.
POST /v1/sandboxes/:id/finalize
Grades the run and closes the sandbox. Synchronous and idempotent.
Send
Prefer: respond-async to queue the evaluation instead of waiting, then
poll GET /v1/sandboxes/:id/evaluation.
Returns — 200 with run_id, score out of 100, criteria_results (one
entry per criterion with passed, skipped, and a reason),
criteria_breakdown, all_skipped, judge_model, provenance, and a
dashboard_url. A second finalize on the same sandbox returns the same run
rather than grading twice.
Errors — 422 empty_criteria when criteria is empty; a run with nothing
to check is never graded. 404 unknown or another team’s. 409 conflict with
details.reason of capture_incomplete when the tape could not be read. 413
over 256 KiB — blobs never travel in this body.
Example
POST /v1/checks/render to write a
criterion that binds, and GET /v1/runs/:id to read the verdict
back later.
GET /v1/sandboxes/:id/evaluation
Polls an asynchronous finalize.
Returns —
{ evaluation_id, run_id, status } while the job runs. On
status: "completed" the body also carries result, identical to what a
synchronous finalize would have returned. On status: "failed" it carries
error.
Errors — 404 when no evaluation exists for that sandbox — which is also
what you get if you never sent Prefer: respond-async.
Example
POST /v1/sandboxes/:id/finalize.
GET /v1/runs
Lists runs, newest first.
Returns — compact rows:
id, session_id, task_name, task_hash,
satisfaction_score, group_id, environment, agent_model, created_at,
finished_at. The full run is on GET /v1/runs/:id.
Filtering by group_id is how you read a trial group back: several runs of one
task, whose spread is the agent’s reliability rather than its best day.
Errors — 401, 403.
Example
GET /v1/runs/:id/report.md for a
readable version of any row.
GET /v1/runs/:id
Returns one run in full.
Returns — the whole run. The fields worth reading:
Errors —
404 unknown or another team’s.
Example
GET /v1/runs/:id/report.md for the same
data rendered.
GET /v1/runs/:id/report.md
Returns the run as a rendered markdown report.
Returns —
text/markdown, not JSON: the score with its denominator, a
criteria table with a reason on every row, the agent and judge models, the twin
runtime, and a link to the run in the dashboard. Readable by a person and by an
agent, which is why it is the one to paste into a report or hand to a coach.
Errors — 404 unknown or another team’s.
Example
excluded as already true in the seed is not a grading failure. A criterion the
seed already satisfies cannot tell a working agent from a do-nothing one, so it
leaves the denominator instead of handing out a free point. Mark a criterion
always_scored at finalize when being already-true is the whole point — an
inverse task, where the agent’s job is to leave something alone.GET /v1/runs/:id/trace
Mints a short-lived signed download URL for the run’s raw tape.
Returns —
{ blob, key, url, expires_in, expires_at }. key is the same
string the run carries as trace_s3_key; url is a signed GET on it, good for
expires_in seconds — 300 today, and read the field rather than hardcoding the
number. The URL carries its own authorization, so the download itself needs no
Authorization header; equally, anyone who gets hold of it can read the blob
until it expires, which is why the response is no-store and the TTL is short.
Mint it when you are ready to fetch.
The blob is events.jsonl — the tape: one JSON object per HTTP call your agent
made against the twins, the same rows /_pome/events served while the sandbox
was alive. This is the file to diff between two runs, or to feed your own
analysis.
Errors — 404 not_found for an unknown run or another team’s; the two are
never distinguished. 404 with details.reason: "blob_absent" when the run is
yours but the tape is not retrievable — a self-hosted run, --no-upload, a row
older than the upload path, or a blob no longer in storage. Branch on
details.reason, not on the message. 503 downstream_unavailable when
evidence storage cannot be reached; retry.
Example
GET /v1/runs/:id/state for the world that
tape acted on, and the digital twin quickstarts for
reading the same rows live off /_pome/events, for free, while the sandbox runs.
GET /v1/runs/:id/state
Mints a short-lived signed download URL for the twin’s final state.
Returns — the same envelope as
/trace, for
state_s3_key. The blob is state_final.json: the twin’s whole world as it
stood when the run was graded, which is the snapshot every [code] criterion
was evaluated against. Reading it is how you check a verdict yourself.
A multi-twin run also carries per_twin — one { key, url } per twin, keyed by
twin id. The top-level key names the primary twin’s blob only, so on a
two-twin sandbox that field alone would show you half the world. A twin whose
state blob is missing is left out of the map rather than listed with a null, so
the keys of per_twin are exactly the twins you can read.
Errors — the same three as /trace. Here details.reason: "blob_absent"
means the run stored no final state, which is what a run graded from
pre-uploaded blobs with no state upload looks like.
Example
GET /v1/runs/:id/trace for what the agent
did, and Digital twins for the shape each twin’s state
takes.
See also
- REST API overview — base URL, authentication, the error table, and the limits.
- Digital twins — what each twin serves and how its world is shaped.
- CLI reference — the same loop from a terminal.
- MCP reference — the same loop driven by a coach agent.