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.
GET /admin/scenarios/:name
Auth: operator
Read a library scenario's exact document for validated previews and frozen starting setups. 404 when unavailable.
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. |
POST /admin/worlds/:id/commerce/runs
Auth: world
Start a commerce purchaser run.
Reserve an isolated commerce world and mint one purchaser capability. Only the trusted supervisor receives this credential.
Body
| field | type | required | description |
|---|---|---|---|
account_id |
string | required | Account in this world's commerce fixture. |
POST /admin/worlds/:id/commerce/runs/:runId/seal
Auth: world
Revoke purchaser admission and drain admitted commerce commands. The world stays reserved until finalization.
POST /admin/worlds/:id/commerce/runs/:runId/finalize
Auth: world
Freeze commerce evidence and release the world.
Seal purchaser admission, settle admitted commands independently of HTTP transmission, persist one frozen evidence bundle, then release the world reservation. Repeated finalize returns the original bundle.
Body
| field | type | required | description |
|---|---|---|---|
reason |
"completed" | "controller_lost" | "cancelled" | optional | |
completion |
any | optional | Private structured completion evidence; absent means communication remains unverified. |
GET /admin/worlds/:id/commerce/runs/:runId/evidence
Auth: world
Read the immutable persisted evidence bundle. Unfinished runs are recovered on runtime restart with unverified completion.
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. Profile worlds additionally carry their declared markers in create, info and diff responses: zendesk_profile: native-v1, sap_s4_profile: sap-s4-cloud-public-2608-v1, dataverse_profile: dataverse-sales-customer-service-v1, confluence_profile: confluence-cloud-pages-v1, freshservice_profile: freshservice-itsm-incidents-v1, jsm_profile: jsm-cloud-requests-v1, and commerce_profile: commerce-delivery-v1. Absent profiles omit their marker. When Freshservice ingress is configured, supported Freshservice worlds additionally advertise freshservice_base_url in create, info, list and reset responses. It is the full configured native base, including its mount; it is omitted when the ingress or profile is absent and is never persisted or exported in seeds.
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/jsm/keys
Auth: world
Mint an immutable principal-bound JSM credential. Requires a JSM world and an active configured principal; native JSM routes are a separate capability.
Mint a fresh JSM credential bound to this principal. Returns {api_key, provider: 'jsm', principal_id}. No query parameters are accepted. Reset retains the association; seed export omits credentials and bindings.
Body
| field | type | required | description |
|---|---|---|---|
principal_id |
effects | required | Existing active synthetic JSM principal account ID. |
POST /admin/worlds/:id/agentforce/keys
Auth: world
Mint an immutable principal-bound Agentforce credential. Requires an Agentforce world and an active configured principal; native Agentforce routes are a separate capability.
Mint a fresh Agentforce credential bound to this principal. Returns {api_key, provider: 'agentforce', principal_id}. No query parameters are accepted. Reset retains the association; seed export omits credentials and bindings.
Body
| field | type | required | description |
|---|---|---|---|
principal_id |
effects | required | Existing active synthetic Agentforce principal ID. |
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 and whether guarded advances are supported (supports_expected_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. |
expected_clock |
number | optional | Advance only if the logical clock still equals this value. A mismatch returns 409 without mutation. |
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). |