Worlds admin API reference
Generated from the zod schemas in
packages/server/src/admin/schemas.ts— do not edit by hand. Regenerate withpnpm docs:gen.
Plain JSON over HTTP on the same port as the twin (default http://127.0.0.1:4242).
Auth
Two levels, no users, no seats:
| scope | token | where it comes from |
|---|---|---|
| operator | one per server | WORLDS_ADMIN_TOKEN / worlds serve --admin-token, or auto-provisioned (tlop_…, printed by the start that mints it, saved to <data-dir>/operator-token, loaded — and only named — by every later start) when the server is exposed with nothing configured |
| world | any number per world (tlw_<world_id>_…) |
returned once as admin_token by POST /admin/worlds; more via POST /admin/worlds/:id/admin-tokens |
| public | none | GET /admin/health |
Send Authorization: Bearer <token>. An operator token opens every route; a world token opens /admin/worlds/<its id>/** (including destroy) and nothing else. The agent's twin key (sk_twin_…) is never an admin credential.
When is it enforced? Off on a loopback bind (127.0.0.1, localhost, ::1) with no token configured — pnpm dev, worlds serve, tests, worlds demo. Required on any other bind host, whenever WORLDS_ADMIN_TOKEN is set, or with WORLDS_AUTH=required. WORLDS_AUTH=off overrides (and warns). Deployment shapes and TLS: deploy.md.
Auth errors: 401 { "error": { "type": "unauthorized", "message": "..." } } (missing/unknown token) and 403 { "error": { "type": "forbidden", "message": "..." } } (a world token on the wrong scope).
Other errors: non-2xx responses return { "error": { "message": "...", "details"?: [...] } }.
GET /admin/health
Auth: public
Liveness check. Returns {ok: true, worlds:
GET /admin/seeds
Auth: operator
List library seeds with name, version, hash, and object counts.
GET /admin/seeds/:name
Auth: operator
One library seed's document with its hash — what passk --tasks compiles a tasks file against, so the build sees the bytes this server would load. 404 for a name the library does not hold.
POST /admin/seeds/validate
Auth: operator
Validate a seed document.
Validate a seed document (structure + referential integrity) without creating a world.
Body
| field | type | required | description |
|---|---|---|---|
seed_doc |
any | optional | Seed document to validate. |
GET /admin/scenarios
Auth: operator
List library scenarios.
POST /admin/worlds
Auth: operator
Create a world from a seed. Returns world_id, base_url, api_key (for the agent), and admin_token (for the harness — shown once).
Create a world from a seed. Returns world_id, base_url, api_key, seed_hash. base_url is WORLDS_PUBLIC_URL when the server has one, else the origin this request reached the server at (its Host header; X-Forwarded-Host/Proto under WORLDS_TRUST_PROXY=1) — point your Stripe SDK there.
Body
| field | type | required | description |
|---|---|---|---|
seed |
string | optional | Name of a library seed (see GET /admin/seeds). |
seed_doc |
any | optional | Inline seed document (JSON). Mutually exclusive with seed. |
GET /admin/worlds
Auth: operator
List worlds. unreadable names world files this runtime refuses (another schema version, or not a readable world file) and why; destroy them.
GET /admin/worlds/:id
Auth: world
World info: seed, clock, twin keys, admin-token count, webhook config, active scenario.
POST /admin/worlds/:id/reset
Auth: world
Reset the world to its seed. Keeps API keys (their use counters restart), admin tokens, and the webhook endpoint config; wipes objects, events, idempotency records, marks, counters, the scenario, deliveries and the request log. Waits (up to 5 s) for twin requests already in flight, admits new ones only once it is done, and fences off a webhook delivery in flight — so a reset world replays byte-identically to a fresh one, quiescent or not.
DELETE /admin/worlds/:id
Auth: world
Destroy the world and its SQLite file (its tokens die with it).
POST /admin/worlds/:id/keys
Auth: world
Mint an additional twin API key.
Mint an additional twin API key (sk_twin_…) for the world — what the agent under test holds. Never an admin credential.
POST /admin/worlds/:id/admin-tokens
Auth: world
Mint an additional admin token for this world. Returns {admin_token, active}.
Mint an additional admin token (tlw_
DELETE /admin/worlds/:id/admin-tokens/:token
Auth: world
Revoke one of this world's admin tokens. Returns {revoked: true, active}; 404 if it was not active.
GET /admin/worlds/:id/state/:resource
Auth: world
Query live objects of a resource with equality filters.
Query live objects of one resource. Any other query param is an equality filter on a top-level field, e.g. ?customer=cus_x&amount=4000.
Query
| field | type | required | description |
|---|---|---|---|
limit |
number | optional | Max objects to return (default: all). |
GET /admin/worlds/:id/events
Auth: world
The world's event log, in creation order. Filter with ?type=, and ?after_seq= for the events emitted after a mark (the seq a mark reports; objects created in one shift share a clock second, so this order is the only order).
GET /admin/worlds/:id/requests
Auth: world
Every twin API request this world served: method, path, status, whether the response was scenario-injected, whether the route is implemented, and unsupported — the parameter the twin refused on an implemented route (docs/scope.md, Refused parameters), null otherwise. ?unmatched=true returns the coverage view — un-twinned endpoints the agent tried to call and parameters the twin refused, grouped and ranked.
POST /admin/worlds/:id/marks
Auth: world
Set a named mark.
Set a named mark at the world's current state, for later diffing.
Body
| field | type | required | description |
|---|---|---|---|
name |
string | required | Mark name. Re-using a name moves the mark. |
GET /admin/worlds/:id/marks
Auth: world
List marks.
GET /admin/worlds/:id/diff
Auth: world
Diff world state since a mark.
Diff of world state since a mark: {created, updated, deleted} grouped by resource, plus a money block summarizing dollars moved per currency (charges_created, refunds_created, invoices_paid, net_movement — amounts in cents), plus before: for every updated or deleted object, its image at the mark, keyed by resource then id — the material for exact field-delta assertions. Events are excluded — query /events instead.
Query
| field | type | required | description |
|---|---|---|---|
from |
string | required | Mark name to diff from. |
GET /admin/worlds/:id/clock
Auth: world
Read the logical clock.
POST /admin/worlds/:id/clock/advance
Auth: world
Advance the logical clock.
Advance the world's logical clock. Due subscription renewals bill immediately, at most 1000 periods per subscription per advance: renewals_pending counts the subscriptions still due after that cap, and the next advance continues them. A renewal the twin cannot bill — an invoice whose negative total would be a credit in a currency other than the customer's balance (docs/scope.md, semantics simplifications) — rolls the whole advance back and answers 409 naming the subscription. Due webhook retries dispatch. Returns {clock, advanced_from, advanced_by, renewals_billed, renewals_pending, iso}.
Body
| field | type | required | description |
|---|---|---|---|
duration |
string | number | required | Duration to advance: "30d", "12h", "45m", "10s", or a number of seconds. |
POST /admin/worlds/:id/scenario
Auth: world
Apply a failure-injection scenario.
Apply a failure-injection scenario to the world. Replaces any active scenario and resets its counters.
Body
| field | type | required | description |
|---|---|---|---|
name |
string | optional | Name of a library scenario (see GET /admin/scenarios). |
scenario_doc |
object | optional | Inline scenario document. Mutually exclusive with name. |
GET /admin/worlds/:id/scenario
Auth: world
Read the active scenario.
DELETE /admin/worlds/:id/scenario
Auth: world
Clear the active scenario and its counters.
POST /admin/worlds/:id/webhook-endpoint
Auth: world
Configure the webhook endpoint.
Configure the world's webhook endpoint. Returns the signing secret (whsec_twin_...).
Body
| field | type | required | description |
|---|---|---|---|
url |
string | required | URL to POST events to (e.g. your test harness's localhost listener). |
max_retries |
number | optional (default: 3) |
Retry attempts after a failed delivery. Backoff is deterministic in logical time: 60s * 2^(attempt-1). |
auto_deliver |
boolean | optional (default: true) |
Deliver in the background after each request/clock advance. Set false for full manual control via POST .../webhooks/flush. |
signature_time |
"wall" | "logical" | optional (default: "wall") |
What the Stripe-Signature t= carries. "wall": delivery time, so stock verifiers (stripe-node, stripe-python) accept deliveries with their default 5-minute tolerance. "logical": the world clock, replayable across runs — pass a large tolerance to your verifier (stripe-node: the 4th argument of constructEvent, in seconds; stripe-python: tolerance= on Webhook.construct_event). |
DELETE /admin/worlds/:id/webhook-endpoint
Auth: world
Remove the webhook endpoint.
GET /admin/worlds/:id/webhook-deliveries
Auth: world
Webhook delivery queue with statuses and attempt counts.
POST /admin/worlds/:id/webhooks/flush
Auth: world
Synchronously deliver all currently-eligible pending webhooks. Returns per-delivery results.
GET /admin/worlds/:id/export-seed
Auth: world
Export the world's live state back into a valid seed document (fork a world). ?name= sets the seed name (kebab-case; default
Export a world's live state as a seed document.
Query
| field | type | required | description |
|---|---|---|---|
name |
string | optional | Name for the exported seed (kebab-case). Default: |
GET /admin/worlds/:id/dump
Auth: world
Canonical JSON export of all world-visible state (the determinism artifact). Admin tokens and twin keys are not world-visible and never appear here.
Appendix: scenario document schema
Scenarios are JSON or YAML documents (worlds scenario apply <world> <file> accepts both; the admin API takes JSON).
| field | type | required | description |
|---|---|---|---|
name |
string | required | Scenario name (kebab-case). |
description |
string | optional | Human-readable summary, shown by worlds scenario ls. |
rules |
(object | object | object)[] | required | Rules evaluated deterministically per request; counters live in world state. |
Each entry in rules is ONE of the three rule kinds below.
Request rules
Fields
| field | type | required | description |
|---|---|---|---|
match |
object | required | Which twin requests this rule applies to. |
after_requests |
number | optional (default: 0) |
This many matched requests pass through untouched before the rule activates. |
duration_requests |
number | optional | How many matched requests the rule applies to once active. Omitted = forever. |
every_nth |
number | optional | Instead of a contiguous window: apply to every nth matched request (counted after after_requests). |
respond |
object | optional | Inject this error response instead of executing the request. Injection happens BEFORE the handler: state is never half-mutated. |
latency_ms |
number | optional | Delay the request by this many wall-clock milliseconds (the logical clock is unaffected). |
The match object:
Fields
| field | type | required | description |
|---|---|---|---|
method |
"GET" | "POST" | "DELETE" | "PUT" | "PATCH" | optional | HTTP method to match. Omitted = any method. |
path |
string | optional | Path glob, e.g. "/v1/customers/". "" matches exactly one path segment, "**" the rest. Omitted = any path. |
The respond object:
Fields
| field | type | required | description |
|---|---|---|---|
status |
number | required | HTTP status to inject. |
stripe_error |
string | optional (default: "api_error") |
Stripe error kind: rate_limit (a 429 official SDKs surface), lock_timeout (a 429 with Stripe-Should-Retry: true, which official SDKs retry), api_error, invalid_api_key, api_key_expired. |
stripe_error values: rate_limit (429), api_error (5xx), invalid_api_key / api_key_expired (401).
Webhook rules
Fields (under the webhooks key)
| field | type | required | description |
|---|---|---|---|
drop_every_nth |
number | optional | Every nth delivery is silently dropped (never sent, no retries). |
reorder |
"swap_adjacent" | optional | swap_adjacent: within each flush batch, deliveries are dispatched pairwise swapped. |
Auth rules
Fields (under the auth key)
| field | type | required | description |
|---|---|---|---|
expire_after_requests |
number | required | After this many authenticated twin requests, the API key acts expired (401 api_key_expired). |