Volter World

Platform API

The hosted product's own endpoints, under /-/ on the platform's address. A twin's vendor API and a world's own endpoints are under the world's address; see HTTP API. This page is generated from the endpoint table (apps/platform/src/doors.ts); GET /-/openapi.json is the same table as OpenAPI, and platform-openapi.json is that document committed, for tools.

Who opens an endpoint: anyone; a person signed in, or a token with the scope named; an admin of the org; the operator with the platform's token. A token without the scope is refused with 403 naming it; a held org answers 403 with the reason; everything above the rate limit answers 429 with Retry-After. A change made with the browser's session (not a token) must come from the platform's own pages (403 otherwise) with a JSON body (415 otherwise).

The platform

endpoint who scope what it does body → answer
GET /-/health anyone — Whether the platform is up, and which hosts it knows. { ok, hosts[] }
GET /.well-known/jwks.json anyone — The public keys this platform signs passes with, for the hosts that trust it. { keys[] }
GET /-/platform anyone — What the pages need before anyone signs in: the access provider people continue with (and their account page there, when it has one), whether the platform bills, whether the Help form is on, and the product site's address when there is one. { name, provider: { kind, name, account? }, billing, support, site? }
GET /-/status anyone — The status page's source: each host up or down, the operator's notice, the clock's last runs. { ok, platform, hosts[], notice, clock, askedAt }
POST /-/status/notice operator — Set or clear the notice the status page shows. `{ text
GET /-/openapi.json anyone — This table as an OpenAPI 3.1 document. OpenAPI
GET /-/vendors person — The twins a new World may have: what the host Worlds are made on serves, as it answers. { host, vendors[] }

Signing in

endpoint who scope what it does body → answer
GET /-/sign-in anyone — Start signing in at the access provider (the authorization code with PKCE); ?next= names a path of this platform to return to. 302 to the provider
POST /-/sign-out anyone — End your session here (not at the provider), from the platform's own page (a GET is refused with 405). 302 to the front page

Signing the volter command in

endpoint who scope what it does body → answer
POST /-/cli/device anyone — Begin signing the volter command in through the browser (the device authorization grant): a code for the person to approve, and where. { name? } → { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval }
GET /-/cli/approve person — The page where a signed-in person approves or refuses a code the volter command shows (signing in first when needed). a page
POST /-/cli/approve person — Approve or refuse a code, from the approval page itself in a signed-in browser (never with a token, never a support session). form: code, decision (approve or deny) → a page
POST /-/cli/token anyone — The volter command collects its personal token once the person approved its code (30 days, named for the machine, scoped orgs:read, worlds:read and worlds:write); authorization_pending until then. { device_code } → { token, name, expiresAt, person }

Worlds

endpoint who scope what it does body → answer
GET /-/worlds person worlds:read The worlds your orgs hold, with their addresses and twins. { worlds[] }
POST /-/worlds person worlds:write Provision a world into an org, on an enrolled host; where the platform bills, refused at the plan's limits with 402. { org, world, vendors[] } → { name, base, host }
POST /-/worlds/sample person worlds:write Make the org's sample World (<org>/sample, Stripe and Slack where the host serves them) and seed it through the vendors' own APIs: a World like any other, metered and deletable; 409 when it exists. { org } → `{ name, base, host, seeded: { : { seeded[] }
DELETE /-/worlds/{org}/{world} admin worlds:write Delete a world through its host. { deleted }
GET /-/worlds/{org}/{world}/open person worlds:read Open the world as yourself: a pass for it, signed for you and for the world's own origin, spent on a session there (write for a member, read for a support session or a token without worlds:write); 409 for a world whose host gives it no origin of its own. A redirect to the page with the pass in its fragment; JSON asks for the address. 303 to the world's page, or { url, scope, expiresIn }
GET /-/worlds/{org}/{world}/previews person worlds:read The world's previews: named branches made for CI (a pull request each), with where each lives and a link that opens it. { world, previews: [{ label, branch, expiresAt, open, live, base?, origin? }] }
PUT /-/worlds/{org}/{world}/previews/{label} person worlds:write Make a preview: a branch of the world named label (pr-12), made with a key the platform mints in the world for the caller (for the person; recorded against the token that asked, so logout, revoking the token or leaving the org ends the preview); ends after ttlDays (7 by default, at most 7). Answers a key to the branch for a token (never the branch's own token; none for a browser session). Asked again, the same preview and a fresh key, unless replace asks for a fresh one; at most 20 previews of a world at once (409). { ttlDays?: 1-7, replace?: true } → 201 { label, branch, base, origin?, open, token?, expiresAt, created: true }, or 200 with created: false
DELETE /-/worlds/{org}/{world}/previews/{label} person worlds:write Remove a preview and its branch (a pull request closed). { removed, branch }
GET /-/worlds/{org}/{world}/previews/{label}/open person worlds:read Open a preview as yourself: a pass for its branch, as opening the world gives one; 410 once it has ended. 303 to the preview's page, or { url, scope, expiresIn }
POST /-/worlds/{org}/{world}/keys person worlds:write Make a key in a World of your org, for an app or a job, shown once: for you, ending in 90 days unless you say otherwise, and revoked when you leave the org; made with a personal token, it ends no later than the token and is revoked with it. A browser session on the World itself makes no keys. `{ name, scope?: read
GET /-/worlds/{org}/{world}/access admin members:read Who reaches the world: the whole org, or only the listed members (admins always). A world kept from the caller answers 404. { world, restricted, members: [userId] }
PUT /-/worlds/{org}/{world}/access admin members:write Keep the world to named members, or open it to the whole org again. Keeping it revokes, at once, the world keys held there by the members it leaves out (and the previews those keys made). Webhook: world.access_changed. { restricted: boolean, members?: [userId] } → { world, restricted, members, revokedFor }
GET /-/worlds/{org}/{world}/token person worlds:read A named key for the volter command (volter remote add), made in the world and shown once: for a token only, never a browser or a support session; write for a token with worlds:write, else read. Listed and revoked alone in the world's Settings. { name, base, scope, token, keyId }

Orgs, members, billing, security, activity

endpoint who scope what it does body → answer
GET /-/orgs person orgs:read Who you are and the orgs you belong to, each with its worlds, security switches and whether it is held, and the invitations to your address still pending, where the provider keeps them. { person, orgs[], invitations[] }
POST /-/invitations/{id}/join person orgs:write Join the org a pending invitation to your address names, in its role, when your identity owns that address. { joined, role }
POST /-/orgs person orgs:write Create an org, you its admin; where the platform has a site, its terms accepted. A name the pages use (account, help, new, …) is refused. { name, accepted? } → { id, slug, name }
GET /-/orgs/{org} person orgs:read The org and its worlds. { id, slug, name, role, worlds[] }
PATCH /-/orgs/{org} admin orgs:write Rename the org (the display name only; the address stays). { name } → { id, name }
DELETE /-/orgs/{org} admin orgs:write Delete the org; refused while it holds worlds unless ?everything=1, which deletes them through their hosts first. { deleted }
GET /-/orgs/{org}/billing person billing:read Where the platform bills: the org's plan, usage this period (and by day), restriction, pending checkout, the plans on offer. { plan, plans[], usage, usageByDay[], restriction, pendingCheckout, paid, lastTickAt }
PATCH /-/orgs/{org}/billing admin billing:write Where the platform bills, the spend cap (Team): on, the plan's hours restrict after the grace; off, hours beyond the plan are billed at the metered price. { spendCap: boolean } → { spendCap }
POST /-/orgs/{org}/checkout admin billing:write Where the platform bills: open (or answer the open) Polar checkout for the Team plan. { id, url, pending? }
POST /-/orgs/{org}/billing-portal admin billing:write Where the platform bills: a session for Polar's customer portal: invoices, payment method, cancellation. { url }
GET /-/orgs/{org}/members person members:read The members and their roles, and the pending invitations. { members[], pending[] }
POST /-/orgs/{org}/members admin members:write Add a member the directory knows, or invite an address it does not (pending until that address signs in; mailed), as a member or (by an admin) an admin. Members may invite by email when the security page allows; adding by id is an admin's, and answers to the approved domains by the person's address. Not both at once. `{ email, role? }
PATCH /-/orgs/{org}/members/{userId} admin members:write Change a member's role; the org's only admin may not become a member (409). `{ role: admin
DELETE /-/orgs/{org}/members/{userId} admin members:write Remove a member (an admin), or leave the org yourself (any member); the org's only admin cannot be removed or leave. { removed }
POST /-/orgs/{org}/invitations/{id}/resend admin members:write Send a pending invitation again: a fresh one to the same address and role, for another week (its id may change). An admin, or a member where the security page lets members invite, for a member's invitation only; the approved domains apply. { invited, pending, id, role, expiresAt }
DELETE /-/orgs/{org}/invitations/{id} admin members:write Revoke a pending invitation. { revoked }
GET /-/orgs/{org}/tokens admin tokens:read The org's tokens (for CI), without their secrets. { tokens[] }
POST /-/orgs/{org}/tokens admin tokens:write Make an org token: bound to the org, scoped (read only by default), expiring; shown once. { name, scopes?, expiresInDays? } → { token, id, scopes, expiresAt }
DELETE /-/orgs/{org}/tokens/{id} admin tokens:write Revoke an org token. { revoked }
GET /-/orgs/{org}/security person orgs:read The org's security switches. { security }
PATCH /-/orgs/{org}/security admin orgs:write Set the switches: members may invite, personal tokens allowed, approved email domains. { membersCanInvite?, membersCanUsePersonalTokens?, approvedEmailDomains? } → { security }
GET /-/orgs/{org}/support-access person orgs:read Whether, and until when, support may open the org. { supportAccess }
DELETE /-/orgs/{org}/support-access admin orgs:write Revoke support access. { revoked }
GET /-/orgs/{org}/labs person orgs:read The early-access features, which are on for the org, and the early-adopter switch. { features[], earlyAdopter, flags }
PATCH /-/orgs/{org}/labs admin orgs:write Turn an early-access feature on or off, or turn on every new one with the early-adopter switch. { flags?: { key: boolean }, earlyAdopter? } → { flags, earlyAdopter }
GET /-/orgs/{org}/webhooks admin orgs:read The org's webhook endpoints and their state, and the event names. { webhooks[], events[] }
POST /-/orgs/{org}/webhooks admin orgs:write Add an endpoint: a URL and the events it wants; the secret is answered once (Standard Webhooks signing). `{ url, events?: ["*"
PATCH /-/orgs/{org}/webhooks/{id} admin orgs:write Change an endpoint's events or description, or enable/disable it. { events?, description?, enabled? } → { id, … }
DELETE /-/orgs/{org}/webhooks/{id} admin orgs:write Remove an endpoint. { deleted }
POST /-/orgs/{org}/webhooks/{id}/test admin orgs:write Send a ping event now and answer the delivery with its attempt. { delivery }
GET /-/orgs/{org}/webhooks/{id}/deliveries admin orgs:read The endpoint's last fifty deliveries, each attempt with its status and the response's first 2 KB. { deliveries[] }
GET /-/orgs/{org}/audit admin activity:read The org's activity: every admin act, newest first. { entries[] }
GET /-/orgs/{org}/audit.jsonl admin activity:read The activity as the file kept, one act a line. JSON lines
GET /-/orgs/{org}/export admin activity:read What the platform knows about the org: the record, the worlds, the members, the activity. JSON

Your tokens and support

endpoint who scope what it does body → answer
GET /-/tokens person tokens:read Your personal access tokens, without their secrets. { tokens[] }
POST /-/tokens person tokens:write Make a personal access token: scoped, expiring (30 days by default); shown once. { name, expiresInDays?, scopes? } → { token, id, scopes, expiresAt }
DELETE /-/tokens/{id} person tokens:write Revoke one of your tokens. { revoked }
POST /-/support person orgs:write Write to support; an admin may allow support to open the org for a while. { category, severity, subject, message, org?, world?, allowAccessDays? } → { id, at, emailed, copyTo, access }

The operator's

endpoint who scope what it does body → answer
POST /-/operator/sweep-members operator — Revoke, in every org's Worlds, the keys (and the branches they made) of people who are no longer members of that org: someone removed at the identity service directly, not through the platform. The clock runs it hourly. { revoked }
POST /-/meter/tick operator — Run the hourly billing tick now (the clock runs it on its own); 404 where the platform does not bill. { ticked, worlds, inserted, duplicates, restrictions }
POST /-/backup operator — Archive the platform's state to the bucket now (the clock does it nightly). { key, bytes }
GET /-/backups operator — The archives in the bucket. { keys[] }
POST /-/restore operator — Restore the platform's state from an archive. { key } → { files }
GET /-/hosts operator — The hosts enrolled. { hosts[] }
POST /-/hosts operator — Enroll a host by address: its admin endpoints must answer the token. { id, base, adminToken } → { id, base }
DELETE /-/hosts/{id} operator — Stop provisioning onto a host; its worlds stay where they are. { removed }
GET /-/operator/overview operator — The operator's page source: orgs with plan, restriction, worlds, hosts and consents; support requests; the clock. { orgs[], support[], hosts[], clock, notice }
GET /-/operator/unclaimed operator — The worlds enrolled hosts serve that no org holds (made on a host before the platform, or on the host itself). { worlds: [{ host, name, twins, owner? }] }
POST /-/operator/claim operator — Claim a world an enrolled host serves into an org whose slug it is served under: without confirm: true only when its host already records that org as its owner (slugs are first come), and never when the host records another. The host records the owner; its apps keep their token. Recorded in the org's activity. { world, org, confirm? } → { name, org, host }
POST /-/operator/open-org operator — A one-hour read-only support session for an org that has allowed it, with a category and a reason; recorded in the org's activity. { org, category, reason } → { token, until, console }
PATCH /-/operator/support/{id} operator — Close or reopen a support request. `{ status: open