# HTTP API

What a twin and a served world answer beside the vendor's API. Everything a vendor's SDK does
goes through the vendor's own paths; these endpoints are the world's own.

## A twin

Every running twin answers these on its own URL, whatever the vendor.

| endpoint | answers |
|---|---|
| `GET /twin` | the twin's manifest: its vendor, version, protocol, what it models, the matchers its handlers accept, and whether it has a root |
| `GET /twin/scenario` | the handlers in force, how many times each matched, and the requests none matched |

There are no write endpoints beside the vendor's API. State changes through the vendor's paths;
behavior changes by editing the handlers file and restarting.

## A served world

`volter world serve` puts a world at `http://<host>:<port>/<org>/<world>/`. Each twin's vendor
API is under its vendor id, so an app pointed at `…/acme/team/github` is an app pointed at that
world's GitHub twin, and, when the twin has a root, at GitHub with the keys hidden. The world's
own endpoints are under `/-/<org>/<world>/`.

`GET /<org>/<world>/.well-known/volter-world` answers a token holder with the world's
manifest: `{ name, vendors: { <vendor>: <url> }, ca, proxy, env, streams? }`. `env` holds
`VOLTER_TWINS_KEY` and, for a twin whose client is pointed by an endpoint variable the injector cannot stand in
for, that variable at the twin's URL. An app started with
`VOLTER_WORLD=<the world's URL>` and `VOLTER_WORLD_TOKEN=<token>` under `@volter/world-core/attach`,
or by `volter-world attach <the world's URL> --token <token> -- <command>`, reads it, and its
unmodified SDKs reach the world's twins. A request the app addresses to the world's own paths carries
the token as `x-twins-key`.

A twin whose clients speak a TCP protocol is reached through a **stream**: `streams` names each one
(`smtp`) with the WebSocket that carries it and the variables its client reads (`SMTP_HOST`/`SMTP_PORT`…). The
attacher listens on loopback for each and sets those variables to the listener, so `nodemailer` or Python's `smtplib`
connects to `127.0.0.1` unmodified
(`@volter/world-core/stream-bridge`). The WebSocket is `/-/<org>/<world>/streams/<id>`; its first
message is the world's token (the read token opens no stream), and after it each message is bytes of
the connection, both ways.

Credentials are **tokens** in the `x-volter-token` header. A served world has two: the token,
which opens everything, and a read token, which opens only `GET` on the vendor API and the
endpoints marked below. `serve` prints both and writes them to `.volter/token`. The vendor API
also takes the token where an app presents it: in `x-twins-key`, which the injector sends from
`VOLTER_TWINS_KEY` so the app's own `Authorization` stays the vendor's, or as the app's API key
(`Bearer <token>`, `token <token>`, or the password half of `Basic`).

Every answer on the vendor API of a twin that has a root carries `x-volter-observed-at`: the
instant of the last completed refresh. It is absent until the first refresh completes, so an app
reading the copy can always tell how old the copy is.

### Browsers

These endpoints share the world's existing credentials. Reads take read or write access; mutations take write
access. Their replies are not cached. Names are 1–24 lowercase letters, digits or single hyphens, starting and
ending with a letter or digit. Sites whose browser labels would exceed 63 characters are skipped; a hosted browser
is refused only when no site fits, with the reason.

| endpoint | answers |
|---|---|
| `GET …/browsers` | `{ browsers: [<name>, …] }`, names sorted |
| `GET …/browsers/<name>` | the browser profile below, or `404` |
| `PUT …/browsers/<name>` | replace the whole state; `Content-Type: application/json` required; returns that state. Invalid state/name, a hostname collision or no fitting site label is `400`; a body over 1 MiB is `413`; creating a 33rd browser is `409` |
| `DELETE …/browsers/<name>` | remove it and its local file; `204`, or `404` if absent |

```ts
type BrowserStorageState = {
  cookies: {
    name: string; value: string; domain: string; path: string; expires: number;
    httpOnly: boolean; secure: boolean; sameSite: 'Strict' | 'Lax' | 'None';
    partitionKey?: string; _crHasCrossSiteAncestor?: boolean;
  }[];
  origins: { origin: string; localStorage: { name: string; value: string }[];
    indexedDB?: { name: string; version: number; stores: {
      name: string; autoIncrement: boolean; keyPath?: string; keyPathArray?: string[];
      records: { key?: unknown; keyEncoded?: unknown; value?: unknown; valueEncoded?: unknown }[];
      indexes: { name: string; keyPath?: string; keyPathArray?: string[]; multiEntry: boolean; unique: boolean }[];
    }[] }[];
  }[];
};
type BrowserContext = {
  permissions?: string[];
  geolocation?: { latitude: number; longitude: number; accuracy?: number };
  locale?: string; timezoneId?: string; userAgent?: string;
  viewport?: { width: number; height: number } | null;
  deviceScaleFactor?: number; isMobile?: boolean; hasTouch?: boolean;
  colorScheme?: 'light' | 'dark' | 'no-preference' | null;
  reducedMotion?: 'reduce' | 'no-preference' | null;
  extraHTTPHeaders?: Record<string, string>; offline?: boolean;
};
type BrowserState = { storageState: BrowserStorageState; context: BrowserContext };
```

`context` is the profile subset of Playwright 1.61.1's `BrowserContextOptions`, with its names and value shapes.
Unknown profile and context keys are refused, including execution options such as proxy, base URL, credentials
and recording. Geolocation latitude is between -90 and 90, longitude between -180 and 180, and optional accuracy
is nonnegative; numbers are finite. Viewport dimensions are integers; a null viewport cannot be combined with
deviceScaleFactor or isMobile set to true. Headers have string values valid for HTTP; locale and userAgent are also
validated as header values. Missing `context` reads as `{}`. A legacy top-level `{ cookies, origins }` object is accepted
as storageState and returned inside the profile. Playwright storageState has no session storage in this version.

Unknown storageState schema fields are refused, as are duplicate cookies, origins or local storage names. Domains and
origins are the vendor's real ones; an origin is canonical HTTP or HTTPS, with no path. `expires` is Unix seconds
or `-1` for a session. Cookie values may contain spaces; controls, semicolons and characters above U+00FF
are refused. Cookie paths also require header byte characters. Optional `partitionKey`, Chromium ancestor metadata and IndexedDB snapshots are carried unchanged;
IndexedDB
schema fields are validated and its encoded record values remain opaque. An empty starting browser is
`{ "storageState": { "cookies": [], "origins": [] }, "context": {} }`. The seed uses this same loader. Locally the
current World writes the whole profile in `.volter/browsers/<name>.json`, independent of the env output. Load it
with `browser.newContext({ ...profile.context, storageState: profile.storageState })`, adding the World's proxy
and CA for execution. Browser bookkeeping is inherited
by a branch and recreated by reset;
it is outside the log, diff, changesets and push.

`GET …/sites/go?host=<real-host>&browser=<name>&land=<path>` opens the named browser's site through the
existing page access flow. Hosted addresses are `<browser>--<site>--<world>--<org><suffix>`; local view addresses
are `<browser>--<site>.<world>--<org>.localhost:<port>`, where `<site>` is the real host with dots changed to
hyphens. Sites with overlong browser labels are skipped; PUT refuses the browser only if no site fits. A name
colliding with a claimed site label or another browser address is refused at PUT. Exact claimed-site labels take
precedence when routing, including sites claimed after the browser was made.
First navigation loads matching cookies host-only and local storage from the first real origin with that host
(including a port), once per state version. On replacement, previously loaded cookie names expire at their
recorded paths. Each version retains those paths for all contexts; deletion keeps the paths for the name's
next state, without cookie values. Hosted bootstrap loads cookies and local storage; native Playwright consumers can load IndexedDB.
The World applies the profile's `Accept-Language` from `extraHTTPHeaders` to forwarded requests, falling back to
`locale`. Other context settings, including permissions, time zone, location and device emulation, take effect
where a browser is created from the profile, not inside a board frame.
Relabelled sites keep independent copies; the proxy and CA at real vendor hostnames preserve real cookie
domains. After loading, ordinary requests go to the twin at the caller's World access scope, resolving the real host and
path by the proxy's rules. The page's bearer authorization is retained; redirects outside the World are unchanged.
On a browser site, GET/HEAD navigation admits same-origin, same-site or none; other requests require same-origin.
The site-opening endpoint with `browser` admits same-origin and none only. WorldDoors and the HTTPS redirect and
reflection fronts strip client-supplied browser routing headers before forwarding; the site-link helper ignores
an invalid browser header. A page on one relabelled site calling another site of the same browser has no
page-access session there and is refused. Each site opens through its own World page pass.

`GET …/board` lists a page for each browser whose address can be made, including a signed-out browser.
If any browser address cannot be made, it also lists one plain copy, as it does with no browsers.
A frame labelled with a browser opens at that browser's site address and loads that state. Saved browser-less
frame and section keys alias only the first browser by name; a plain fallback frame retains its own key.
Other browser copies are new frames. Browser frame keys start `~browser:<browser>:<vendor>:`, so colon-containing
frame ids cannot collide with ordinary vendor-prefixed keys. The console groups and labels pages by browser
name, including hidden entries. See [Seed and reset](../guides/seed-and-reset.md#make-browser-states-in-the-seed).

### The history

| endpoint | read token | answers |
|---|---|---|
| `GET …/log/<twin>?after=<position>` | yes | the world's log for that twin from a position: what `volter world fetch` reads |
| `GET …/checkpoint/<twin>[?at=<position>]` | yes | the twin's state at a position, the latest by default: what `clone` starts from |
| `GET …/changesets` and `GET …/changesets/<name>` | yes | the changesets that landed, with their receipts, verification and approvals, its stored `contentHash`, and `currentHash`, the body's hash as it stands (an approval or verification made on another hash is for an earlier version; a stored hash that differs from it is a changeset whose body changed outside the World, which approval refuses), `readiness`: whether it may be deployed under this World's rules, with `approvers` (the different people whose approvals bind to it; a key's counts for its person), `required` and the `reasons` it may not, and `deployed`: each action's receipt on its log entry, one `{ service, actionId, status, at, externalId?, url?, reason? }` per action that has one (`deployed` = performed against the vendor, `landed` = on the parent and waiting, `refused`, `failed`, `skipped`; a settled receipt wins over a `landed` one). A deploy writes these receipts; so does a push, on the pushing World (`landed` while the remote waits, `deployed` when it performed it), beside the changeset's own `applied` (`replayed` at the remote, `confirmed` when the remote performed it) |
| `GET …/requests?hours=24` | yes | the world's request report over the period: per twin and per link, the requests by status class, the response time's p50 and p95, and the top routes; `journalFailures`, the journal lines this World's server failed to keep since it started, for its links and the twins it runs in its own process (a twin in a process of its own says its failures on its stderr) (an audit gap, said, never silent) |

### Pushing and deploying

| endpoint | answers |
|---|---|
| `POST …/push` with a changeset as the body | append the changeset's changes to the world's log if its base is current, else 409 with the drift. Under `deploy: auto` the receipts are in the answer; otherwise each change is `landed`. The answer and the kept changeset record who pushed it (`pushedBy`: a person, a key, or `token`), and `owner`, the pusher's person when it arrived with none and the pusher names one; the owner and cutter it arrives with are the sending World's record. A changeset landed once keeps its first record |
| `POST …/changesets/<name>/verify` | run the world's checks over the changeset and record the result |
| `POST …/changesets/<name>/approve` `{ as, note? }` | sign the changeset's current hash; a browser session signs as the person it was opened for (a platform's pass; on a local World the person signed in with `volter login`, else `this machine`) and its `as` is not read; a key signs as itself (`key:<name> for:<person>`), and readiness counts it for that person; the World's token signs as its `as` |
| `POST …/deploy[/<name>]` | perform landed changes against each twin's root, by its policy; receipts in the answer. A browser's session (no token presented) deploys only what the body names: `{ "confirm": "<name>" }` (the changeset, or the world's name for all), else 400 |

### A twin's root

Opening a seal (`PUT …/twins/<vendor>/root`, `PUT …/twins/<vendor>/credential`, `PUT` or `DELETE …/links/<name>`,
`PUT` or `DELETE …/checks/<name>`, since a local World runs its checks in its own process, and `POST …/credentials/rewrap`)
takes the World's token or a person's session at its page; a key (an app's, a script's) is answered `403`.

| endpoint | answers |
|---|---|
| `GET …/twins` | every twin of the world, each as its status endpoint answers it (read token) |
| `GET …/twins/<vendor>/status` | the twin as a consumer sees it: boot-recorded `identity` (mounted package/version and pinned source), `execution` (real-root connection and deploy policy), recorded `dataOrigin`, protocol, the workspaces it lists (`screens`), root, whether a credential is sealed, the last refresh, the position, the last performed entry's receipt |
| `GET`/`PUT …/twins/<vendor>/root` | `{ url, deploy, refresh }` as in the config; `PUT` with `null` clears it |
| `PUT …/twins/<vendor>/credential` | seal the vendor's real credential, bound to where it goes: the twin's root origin, else the link of that name (`409` with neither). It opens for that origin only: a root or link pointed elsewhere refuses it until it is set again. Never readable back; `GET` answers only that one is sealed, when, by what (`sealedBy`) and for where (`boundTo`). A `signingSecret` field enables webhook ingest |
| `POST …/credentials/rewrap` | after the vault key that wraps this World's credentials is rotated, wrap each one's DEK again under the newest version (the vault's `transit/rewrap`: the DEK never leaves it): `{ rewrapped, current, left }` name the vendors and links that moved, those already current, and those it left: sealed under a host key or by another vault key, a wrapped key whose version could not be read, a record sealed again while it was rewrapped (never overwritten), or one that does not read. Only `current` says a record needs no old version. The World's write access; a key is answered `403` |
| `GET`/`PUT`/`DELETE …/twins/<vendor>/scenario` | a generative twin's scenario: the handlers document it answers its generative surface from, read again on every request, so a `PUT` is live on the next call. A hosted world keeps it in its own state; a local world writes the file its service declares as `colocate.scenarioPath`. `404` for a twin that takes none |
| `POST …/twins/<vendor>/refresh[?force=1]` | observe the root now: the twin observes, the kernel folds what changed; throttled to the root's `refresh.atMost` (else the twin's) unless `force=1` |
| `POST …/session`, `DELETE …/session` | a browser session for this world in the presented token's scope: an HttpOnly cookie holding an opaque session id (never the token), which the vendor API and the World's endpoints accept like the token. Sessions last 30 days and end when the tokens rotate; `DELETE` ends this one |
| `GET …/origin`, `PUT …/origin` `{ url, token }` | the world this one branches from: `PUT` names it (`https://<host>/<org>/<world>`, the token that opens it) and brings its history in, as `volter world clone` does; `GET` answers `{ origin, ahead: { changes, changesets }, behind: { <twin>: n }, asOf }`: the changes not cut and the changesets not pushed, and per twin what the last fetch brought that this branch has not taken, as of that fetch (`asOf`, the origin's `fetchedAt`), as git's ahead and behind (a changeset a push applied in part counts as pushed, as the push endpoint's queue does); a World with no origin answers `{ origin: null }` |
| `POST …/origin/pull` | fetch what the origin has and move this branch onto it, naming conflicts by record and field, as `volter world pull` does |
| `GET …/rules`, `PUT …/rules` `{ changesets: { approvals?, summary? } }` | how this World's changesets are made and readied (its config's `changesets`): read with any access; changed with write access by a person's session or the World's token, never a key (403) |
| `POST …/changesets` `{ message, name? }` | cut the unpushed changes into a changeset, as `volter world changeset` does; `message` is required unless the World's rules generate the summary, and a cut with none and nothing to cut is refused (400); it records who cut it (`cutBy`: `key:<name> for:<person>`, `person:<who>`, `machine`, `person`, `token`) and its `owner`, the person the cutting grant names |
| `POST …/origin/push` | push every unpushed changeset to the origin, oldest first; each answers with its receipts, as `volter world push` does |
| `GET …/links`, `PUT …/links/<name>` `{ origin }`, `DELETE …/links/<name>` | a link: `/<org>/<world>/<name>/…` forwards to the vendor at `origin` (https, a hostname) with the credential sealed as `…/twins/<name>/credential`, whose headers replace any of the same name the caller sent. No tree; each forward is journaled as a twin's request is, under the link's name (`…/requests`, `volter-world tail --requests`). The caller presents the world's token |
| `GET …/checks`, `PUT …/checks/<name>`, `DELETE …/checks/<name>` | the world's checks: JavaScript exporting `check` (or default) as `{ name, run(entry, tree) }`, run over every entry before it is performed. `PUT` refuses a file that exports no check. A hosted world runs each check in an isolate of its own, with no network and nothing of the world's |
| `POST …/twins/<vendor>/ingest` | a vendor webhook, verified with the sealed signing secret, folded into the log. Keyless; the vendor's signature is the credential |

Every served world is capped: request bodies to 4 MB, checkpoints to 24 MB, and a request budget
answered with `Retry-After`.

The status identity is boot evidence, not a fresh lookup of installed dependencies. `identity.mounted` is null when the host has no recorded package name; a recorded package may have a null version. `identity.pinned` names the selection at boot, so editing a config does not rewrite a running host's identity. `execution.rootConnected` and `deploy` describe the configured real root, not proof that a vendor operation was performed. `dataOrigin` is `shared-history` when origin history is recorded, `vendor-refresh` when a refresh marker is recorded, otherwise `unrecorded`; absence of provenance never establishes synthetic data. Older hosts may omit these additive fields.

### Looking into a world

What a person steps into a world through: each vendor's own UI, what the world holds, what happened
in it, its clock and its branches. Every host answers these alike (`volter world serve`,
`volter world view`, world-host, a hosted world); the console reads nothing else. The model is
[Viewing a World](../contributing/architecture.md#viewing-a-world).

| endpoint | read token | answers |
|---|---|---|
| `GET /<org>/<world>/<vendor>/<path>` opened as a page | yes | the vendor's own screens: each workspace the twin's `GET /twin` lists (`screens: [{ id, path, host? }]`, the twin's built workspace screens) is answered by the twin at its path under the twin's place |
| `POST …/session`, `DELETE …/session` | yes | a browser session in the token's scope, `{ world, scope: "read" \| "write", origin? }`. Where the host gives the world an origin of its own (`<world>--<org>.localhost:<port>` locally, `<world>--<org><WORLD_ORIGIN_SUFFIX>` hosted) the session lives only there, in a host-only cookie, and asked for elsewhere answers 409 `{ origin }`. A request the session carries that is not a read must say `Sec-Fetch-Site: same-origin`. A World served on this machine's loopback with an origin of its own (`volter world view`, `volter world serve`) opens a write session with no token for its own page: `POST …/session` with nothing presented, a loopback `Host`, an `Origin` equal to that origin and `Sec-Fetch-Site: same-origin`, answered `{ world, scope: "write", local: true, origin }`. A read session browses every vendor's screens: its reads pass, and a twin that enforces the read scope refuses its writes (at any other, its non-`GET` requests are refused). A named key opens no session, except one that may only read: that is how a shared read-only link opens the World's pages, and its session reads only, lasts no longer than the key and ends when the key is revoked. |
| `POST …/session` with `x-volter-pass` | — | a person a trusted platform signed a pass for (`--trust`, `TRUSTED_ISSUERS`), from the world's own page (`Sec-Fetch-Site: same-origin`) and only where the world has an origin of its own: a session of the pass's scope for 12 hours, `{ world, scope, who, issuer, origin }`. A pass is spent once; one for another world or origin, expired or from an untrusted platform answers 401 with the reason |
| `GET …/keys`, `POST …/keys`, `DELETE …/keys/<id>`, `DELETE …/keys?person=<subject>` | no | the world's named keys, each for what holds it (an app, a CI job), managed with write access by the world's token or a person's browser session, never by a key: `POST { name, scope?: "write" \| "read" }` answers the key (`tok_k_…`) once with its `id`; the world keeps only its hash and who made it, and a key opens the vendor API and the World's endpoints as the token of its scope does, never handing back the world's token. With the world's token, `POST` also takes `for` (the person it is for, by the subject that revokes it), `forName` with it (that person as attribution names them), `expiresAt` and `replace: true` (the key of that name for that person goes). A name holding ` for:` is refused: the caller a write records reads `key:<name> for:<person>`. `GET` lists `{ keys: [{ id, name, scope, createdAt, lastUsedAt, expiresAt, for, person, createdBy }] }`, `person` the name attribution uses; `DELETE` revokes one alone; `DELETE ?person=` (the world's token) revokes every key made by or for a person. New tokens (rotate) end every key |
| `GET …/map` | yes | every twin and what it holds: `{ twins: [{ twin, position, screens, root, resources: [{ type, count }] }] }` |
| `GET …/timeline?limit=&before=&twin=&trace=` | yes | the twins' logs merged, newest first, by each entry's `occurredAt`, then twin, then position: `{ entries: [{ twin, position, entry, caller? }], next }`. `caller` is who made the request the entry was written in, as the World named it on the way to the twin (`key:<name>`, `person:<who>`, `token`, `token:read`); what the vendor makes on its own while serving it (an entry whose actor is the system's, a due renewal) carries none: the World replaces any `x-volter-caller` a caller sends and continues the request's trace (or starts one) under a parent-id it mints, which joins the entry to the twin's request journal line; the twin's handler never sees the header. `next` is the `before` cursor of the page after; `twin` narrows to one twin, `trace` to one W3C trace id. Entries at one instant are not ordered finer than the clock |
| `GET …/diff` | yes | this world's own changes since its base, what a changeset would cut: `{ base: { id, at }, changes: [{ twin, entry }] }` |
| `GET …/clock` | yes | `{ at, frozen }`: the instant every twin stamps from, or the wall clock when none is set |
| `PUT …/clock` `{ at }`, `POST …/clock/advance` `{ by: "<N>(s\|m\|h\|d)" }` | | set or advance it. `409` when it would move back: before its current instant, or, unset, before the newest entry. Advancing needs a set clock. Twins catch up on their next request |
| `GET …/history?at=<instant>` | yes | each twin's history cut at the instant, `{ views: { <twin>: { view, position } } }`: what a branch as of that instant clones |
| `GET …/branches` | yes | the host's branches of this world: `[{ name, from, at, createdAt, expiresAt }]`. `404` where the host makes none (`volter world serve`) |
| `POST …/branches` `{ at?: { instant }, ttl?, live?, label? }` | | a branch as of the instant (now, without one): another world, cloned from this one's history cut there, its clock frozen at the instant (`live: true`, for a branch as of now only: its clock keeps this world's time, as a pull request's preview does; `label` names the branch `<world>-<label>-<4 hex>`), removed after `ttl` seconds (60 to 2592000) when given. `201 { name, token, readToken, expiresAt }` |
| `DELETE …/branches/<org>/<world>` | | remove one of this world's branches |

On the vendor wire, the read token's `GET` and `HEAD` pass as before, and anything else is refused, except at a twin whose
manifest names `requestScopes: ["read"]`: its other requests reach it with `x-volter-read-only: 1`,
and the twin refuses the ones that write (an append, stored bytes, a git ref) while the vendor's own moves (a renewal falling due) still
happen. A twin records the incoming W3C `traceparent` on the entries a request causes and carries it,
continued, on the webhooks those entries cause; the timeline's `trace` follows it.

## The hosting product

`@volter/world-host` serves many worlds under one URL by `<org>/<world>`, each with its own tokens.
It mounts every bare world under a directory (`<dir>/<org>/<world>/`, made by `volter world init
--bare <org>/<world>`) and answers the names of its worlds at `GET /-/ping`, unauthenticated.

Its own endpoints open to the **admin token**, minted once and kept at `<dir>/.volter-host/admin`,
printed by `volter-host serve`. The admin token opens nothing under a world, and a world's token
opens nothing here.

| endpoint | answers |
|---|---|
| `GET /-/worlds` | every world: `{ org, world, name, base, owner?, token, readToken, twins: [{ vendor, protocol, root }] }` |
| `GET /-/console/` | the console, when `@volter/world-console` is installed beside the host: one page for every world above, reading these endpoints and a world's with the token you type |
| `POST /-/worlds` `{ org, world, vendors, owner? }` | a new bare world, mounted at once, with its two tokens; `owner` records the org that owns it, as its platform names the org |
| `PUT /-/worlds/<org>/<world>/owner` `{ owner }` | record the owning org of a world made without one (a claim); `409` when another org is recorded |
| `GET /-/vendors` | the twins this host can make a world with: `{ vendors: [vendor, …] }` |
| `POST /-/worlds/<org>/<world>/rotate` | new tokens; the old ones die with the response |
| `DELETE /-/worlds/<org>/<world>` | remove the world and its tree |

A world served by the host answers exactly what a world served by `volter world serve` answers.

### Hosted on Cloudflare

`apps/cloud` is the same hosting product as a Cloudflare Worker: one Durable Object per world, each
world in an isolate of its own, its state in the object's SQLite and its blobs in R2. A world
answers exactly what a served world answers, and the admin endpoints answer as a world-host's do (the same
inventory row, the admin token as `x-volter-token` or a bearer), so a platform provisions onto either.
The admin token is the Worker's `ADMIN_TOKEN` secret.

| endpoint | answers |
|---|---|
| `GET /-/worlds` | every world as an inventory row: `{ org, world, name, base, owner?, token, readToken, twins: [{ vendor, protocol, root }] }` |
| `POST /-/worlds` `{ org, world, vendors, from? }` | with `from: "<org>/<world>"`, a branch of that world: its origin is the parent, at the parent's current position. A world with branches is not removed |
| `POST /-/worlds` `{ org, world, vendors, token?, readToken?, owner? }` | a new world, answered as its inventory row (`owner`: the org that owns it); `409` when the name is taken. `token`/`readToken` adopt a credential the world's callers already hold (24-256 URL-safe characters) instead of minting one |
| `POST /-/worlds/<org>/<world>/rotate` | new tokens, `{ name, token, readToken }`; the old ones are refused from the next request |
| `PUT /-/worlds/<org>/<world>/owner` `{ owner }` | record the owning org of a world made without one (a claim); `409` when another org is recorded |
| `GET /-/vendors` | the twins this host can make a world with: `{ vendors: [vendor, …] }` |
| `DELETE /-/worlds/<org>/<world>` | remove the world: its state, its blobs, its refresh schedule |
| `PUT /-/worlds/<org>/<world>/import` `{ twin, files }` | place a twin's state files, `{ ".volter/world/<state>/<file>": content }`: moving a v1 twins-cloud namespace into a world |

A rooted twin refreshes on the world's own schedule (`root.refresh.every`, else the twin's), and its
sealed credential is wrapped by the transit key of the org slug it is served under (`<VAULT_TRANSIT_KEY>-org-<slug>`) when the deployment names a vault (`VAULT_ADDR`,
`VAULT_TOKEN`, `VAULT_TRANSIT_KEY`, optionally `VAULT_TRANSIT_MOUNT`, `VAULT_FINGERPRINT_KEY` (default
`<VAULT_TRANSIT_KEY>-fingerprint`, one transit key for every org, never rotated) and `VAULT_NAMESPACE`; company decision
0034), else by the Worker's `SEALING_KEY` secret. A record sealed under `SEALING_KEY` is sealed again by the
vault the first time it opens (a root's credential, a link's, a signing secret); `GET …/credential` says
`sealedBy` (`vault` or `key`), and `SEALING_KEY` can be removed once no record says `key`. A Worker accepts no inbound TCP
connection, so the TCP twins (smtp, PlanetScale's MySQL wire) are reached through their streams; a
twin that answers a WebSocket upgrade on its vendor API (tunnel's control socket) answers it here too. A world names the code it ran on in
`x-volter-world-code` on the answers of its vendor API and endpoints (not on an upgrade). Not hosted: the real engines a twin can run locally beside its twinned control
plane (fly's Docker execution plane), which a hosted world serves virtually, and a vendor's own server a local World
runs itself (livekit-server), which a hosted world does not run.
