Reference

API reference

The control plane's HTTP API, at https://app.worldplane.net. The panel, the CLI and CI all use it. Requests and responses are JSON unless a route says otherwise. A machine-readable description is in openapi.json (OpenAPI 3.1).

To drive the API from a coding agent, see Use WorldPlane from Claude Code or Codex.

Conventions#

  • Every route is under /v1. Path parameters are slugs: {tenant} (the organisation), {project}, {env}; {build} and key {id} are 32-character hex ids.
  • Send bodies with Content-Type: application/json.
  • Times are Unix seconds (UTC). Money is USD as a JSON number.
  • The examples use two shell variables:
export WP=https://app.worldplane.net
export WP_TOKEN=...      # a session token, or an API key (wpk_...)

Authentication#

A request authenticates with a secret in the Authorization header, or with the panel's session cookie:

Authorization: Bearer <session token or API key>
  • Session tokens are for people. POST /v1/sessions (sign in) and POST /v1/accounts (sign up) answer with a token (64 hex characters) and also set it as the wp_session cookie (HttpOnly; Secure; SameSite=Strict). A session lasts 30 days, or until signed out or the password is reset.
  • API keys are for CI and tools. An owner or admin makes one with POST /v1/tenants/{tenant}/keys (or in the panel) with a role, optionally on one project. The secret, wpk_ and 64 hex characters, is shown once. A key acts only in its organisation, and can't make organisations, add a second factor, or be an owner.
  • The control plane keeps only a SHA-256 of each token and key.
  • If both a header and the cookie are sent, the header wins.
curl -s "$WP/v1/sessions" -H "Content-Type: application/json" \
  -d '{"email": "you@studio.example", "password": "your password"}'
# {"account": {...}, "token": "5c1e..."}  →  export WP_TOKEN=5c1e...
curl -s "$WP/v1/me" -H "Authorization: Bearer $WP_TOKEN"

The player routes under /v1/play/... and the health check need no authentication. Sign-ins are throttled per e-mail: after 5 wrong passwords or codes within 15 minutes, that e-mail's sign-ins answer 429 until the window passes.

Roles#

A member or key holds a role on the whole organisation or on one project. Each route below names what it needs:

NeedsOwnerAdminOperatorDeveloperViewerBilling
read: projects, environments, builds, zones, secrets' namesyesyesyesyesyesno
upload: buildsyesyesyesyesnono
deploy: deploy and stopyesyesyesdev, testnono
manage secretsyesyesdev, testdev, testnono
administer: members, keys, projects, environmentsyesyesnononono
read billing: usageyesyesnononoyes
manage billing: checkout, portal, spend limityesnonononoyes
read audityesyesnononono
own: add or remove ownersyesnonononono

A role on one project counts only for that project: organisation-wide actions (members, keys, usage, audit) need a role on the whole organisation. The dev and test exceptions go by the environment's slug.

Errors#

A refused request answers with a status and a sentence for people:

{"error": "build 3f2a... is checking, not ready"}
StatusMeans
400A value is wrong (a slug, a map path, a size); the sentence says which.
401No or unknown token or key (sign in, or send an API key), or a wrong password or code.
402Payment needed: no active subscription (uploading, deploying, waking), deploys on hold for an unpaid invoice, or the spend limit reached (deploying). See billing.
403Your roles here don't allow it (your roles here do not allow that), or it needs a person rather than a key. A 403 doesn't say whether the organisation exists.
404no such tenant, project, environment, build, key, member or secret, or a path the API doesn't have.
409It already exists, or the state doesn't allow it (a build not ready, an upload not under way, a secret missing at deploy, the last owner).
429Too many wrong sign-ins for that e-mail; the sentence says how many minutes to wait.
500The control plane couldn't store it, or a zone didn't start.
501Password resets on a control plane that sends no e-mail.
502, 503Stripe, storage or a zone's coordinator didn't answer, or a zone's live map can't be reached.

Every refusal has this shape, including those made before a route runs: 400 for a body that isn't valid JSON, 415 without Content-Type: application/json, 422 for a missing field or a field of the wrong type (an unknown role, say), 405 for a method a path doesn't take, and 413 for a JSON body over 2 MB. A path under /v1 the API doesn't have answers 404 (the API has no /v1/...).

All routes#

Objects#

The shapes that several routes return.

Account#

{"id": "9c0e...", "email": "you@studio.example", "name": "You"}

Tenant and Project#

{"slug": "mystudio", "name": "My Studio"}
{"tenant": "mystudio", "slug": "mygame", "name": "My Game"}

Environment#

{
  "tenant": "mystudio", "project": "mygame", "slug": "test", "name": "Test",
  "region": "us-east-2",
  "max_per_hour": 2.0,          // USD an hour, or null for no budget
  "zone": { ...ZoneSettings },
  "build": "3f2a...",           // the build it runs, or null before the first deploy
  "state": "running"            // stopped | starting | running | sleeping | failed
}

ZoneSettings#

FieldTypeDefaultMeaning
map requiredstringThe Unreal package path every server opens, starting with /, such as /Game/Maps/World. No : or \.
namestringmainThe zone's name in the environment: a slug.
half_size_mnumber1000How far the zone reaches from the map's origin along x and y, metres. Above 0.
min_serversinteger1At least 1.
max_serversinteger4At least min_servers.
server_capacitynumber100The load one server carries.
server_sizestringstandardstandard, plus or max (large and xlarge are read as plus and max).
warm_serversinteger1Booted, idle servers kept ready.
sleep_after_minutesinteger or nullnullMinutes with no players before the zone sleeps; null never.
extra_argsstring[][]Arguments for every server and the gateway, with {index}, {role} and {env:NAME} placeholders.

Build#

{
  "id": "3f2a...", "tenant": "mystudio", "project": "mygame",
  "label": "0.4.1",
  "kind": "archive",            // archive | image | local
  "state": "ready",             // uploading | checking | imaging | starting | ready | rejected | failed
  "reason": null,               // why it was rejected or failed, in plain words
  "size": 512000000,            // bytes of the archive
  "digest": "a1b2...",          // sha256 of the archive
  "program": null,              // the image (image builds, and archives once imaged) or local program
  "uploaded_by": "account:you@studio.example",
  "created_at": 1793250000
}

ApiKey#

{"id": "77d1...", "tenant": "mystudio", "name": "ci", "prefix": "wpk_1a2b3c4d",
 "role": "developer", "project": "mygame", "created_at": 1793250000, "revoked": false}

AuditRecord#

{"seq": 42, "at": 1793250000, "tenant": "mystudio", "actor": "key:ci",
 "action": "environment.deploy", "target": "mygame/test", "detail": "build 3f2a... (0.4.1)"}

actor is account:<email>, key:<name>, stripe or worldplane.

Accounts and sessions#

Health check#

GET/v1/health
Auth
None.
Response
200, the text ok (not JSON).
curl -s "$WP/v1/health"

Sign up#

POST/v1/accounts
Auth
None.
Body fieldTypeMeaning
email requiredstringAn e-mail address.
name requiredstringNot empty.
password requiredstringAt least 10 characters.

Response 200, with the session cookie set:

{"account": {"id": "9c0e...", "email": "you@studio.example", "name": "You"}, "token": "5c1e..."}

Organisations that invited this e-mail before it had an account add it as a member now.

  • 400 not an e-mail address, password too short, no name
  • 403 sign-up is closed on this control plane
  • 409 an account for that e-mail already exists
curl -s "$WP/v1/accounts" -H "Content-Type: application/json" \
  -d '{"email": "you@studio.example", "name": "You", "password": "a long password"}'

Sign in#

POST/v1/sessions
Auth
None.
Body fieldTypeMeaning
email requiredstring
password requiredstring
code optionalstring or nullThe authenticator's 6-digit code, once the account has one.

Response 200: the same as sign up, with the cookie set.

  • 401 wrong e-mail or password; enter the 6-digit code from your authenticator app (none sent: send it again with code); a wrong code
  • 429 too many wrong sign-ins for this e-mail
curl -s "$WP/v1/sessions" -H "Content-Type: application/json" \
  -d '{"email": "you@studio.example", "password": "a long password", "code": "123456"}'

Sign out#

DELETE/v1/sessions/current
Auth
The session to end, as bearer token or cookie. An API key is ignored (revoke keys instead).
Response
204, always, and the cookie cleared.
curl -s -X DELETE "$WP/v1/sessions/current" -H "Authorization: Bearer $WP_TOKEN"

Request a password reset#

POST/v1/password-resets
Auth
None.
Body
email required string.

Mails a link to choose a new password, valid for an hour, if the e-mail has an account. The answer is the same either way. At most 5 links per e-mail in 15 minutes.

{"message": "if that e-mail has an account, a link to choose a new password is on its way"}
  • 202 accepted
  • 501 this control plane sends no e-mail
curl -s "$WP/v1/password-resets" -H "Content-Type: application/json" -d '{"email": "you@studio.example"}'

Set a new password#

POST/v1/password-resets/confirm
Auth
None: the token from the link (#/reset/<token>).
Body fieldTypeMeaning
token requiredstringFrom the link. Works once.
password requiredstringAt least 10 characters.

Sets the password and ends every session the account had.

  • 204 done
  • 400 password too short; the link expired or was used
curl -s "$WP/v1/password-resets/confirm" -H "Content-Type: application/json" \
  -d '{"token": "...", "password": "a new long password"}'

Who am I#

GET/v1/me
Auth
Session or key.
{
  "account": {"id": "9c0e...", "email": "you@studio.example", "name": "You"},   // null for a key
  "actor": "account:you@studio.example",                                       // or "key:ci"
  "grants": [{"tenant": "mystudio", "project": null, "role": "owner"}],
  "tenants": ["mystudio"]
}
  • 200
  • 401 not signed in
curl -s "$WP/v1/me" -H "Authorization: Bearer $WP_TOKEN"

Set up an authenticator#

POST/v1/me/totp
Auth
A person's session (keys get 403). No body.

Makes a new TOTP secret, pending until a code from it is confirmed. Add it to an authenticator app by its uri (as a QR code) or its base32 secret.

{"secret": "JBSWY3DP...", "uri": "otpauth://totp/...", "next": "POST /v1/me/totp/confirm with a code from the app"}
  • 200
  • 401
  • 403 a key
curl -s -X POST "$WP/v1/me/totp" -H "Authorization: Bearer $WP_TOKEN"

Confirm the authenticator#

POST/v1/me/totp/confirm
Auth
A person's session.
Body
code required string: the code the app shows now.

From now on, signing in needs a code.

  • 204 set up
  • 400 nothing pending; the code doesn't match
  • 401, 403
curl -s "$WP/v1/me/totp/confirm" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d '{"code": "123456"}'

Remove the authenticator#

POST/v1/me/totp/remove
Auth
A person's session.
Body
code required string: a current code.
  • 204 removed
  • 400 none set up; the code doesn't match
  • 401, 403
curl -s "$WP/v1/me/totp/remove" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d '{"code": "123456"}'

Regions#

GET/v1/regions
Auth
Session or key.

The regions environments may run in.

{"regions": [{"id": "us-east-2", "name": "US East (Ohio)"}]}
  • 200
  • 401
curl -s "$WP/v1/regions" -H "Authorization: Bearer $WP_TOKEN"

Organisations, members and keys#

Create an organisation#

POST/v1/tenants
Auth
A person's session; the caller becomes its owner.
Body fieldTypeMeaning
slug requiredstring2 to 18 lowercase letters, digits or -, starting with a letter; not platform, worldplane, system, kube, aws or local.
name requiredstringIts display name.

Response 201: the Tenant.

  • 400 bad slug
  • 401
  • 403 a key
  • 409 the slug is taken
curl -s "$WP/v1/tenants" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d '{"slug": "mystudio", "name": "My Studio"}'

Get an organisation#

GET/v1/tenants/{tenant}
Auth
read or read billing.

The organisation and the projects the caller may read.

{"tenant": {"slug": "mystudio", "name": "My Studio"},
 "projects": [{"tenant": "mystudio", "slug": "mygame", "name": "My Game"}]}
  • 200
  • 401, 403
  • 404 no such tenant
curl -s "$WP/v1/tenants/mystudio" -H "Authorization: Bearer $WP_TOKEN"

List members#

GET/v1/tenants/{tenant}/members
Auth
read on the organisation.

One entry per role granted; project is null for the whole organisation. invited lists e-mails that have no account yet.

{"members": [{"email": "you@studio.example", "name": "You", "role": "owner", "project": null}],
 "invited": [{"email": "dev@studio.example", "role": "developer", "project": "mygame"}]}
  • 200
  • 401, 403
curl -s "$WP/v1/tenants/mystudio/members" -H "Authorization: Bearer $WP_TOKEN"

Add or invite a member#

POST/v1/tenants/{tenant}/members
Auth
administer; adding an owner needs own.
Body fieldTypeMeaning
email requiredstringTheir e-mail.
role requiredstringowner, admin, operator, developer, viewer or billing.
project optionalstring or nullA project's slug, to grant the role on that project only.

No response body. Someone with an account gets the role at once; anyone else is invited (and mailed, where the control plane sends mail), and gets the role when they sign up with that e-mail.

  • 201 added
  • 202 invited
  • 400 not an e-mail address
  • 401, 403
  • 404 no such project
  • 422 unknown role
curl -s -i "$WP/v1/tenants/mystudio/members" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d '{"email": "dev@studio.example", "role": "developer", "project": "mygame"}'

Remove a member#

DELETE/v1/tenants/{tenant}/members/{email}
Auth
administer; removing an owner needs own.
Path
email: the member's e-mail, URL-encoded (@ as %40).

Removes every role the person has in the organisation. Pending invitations aren't removed here.

  • 204 removed
  • 401, 403
  • 404 no such member
  • 409 an organisation keeps at least one owner
curl -s -X DELETE "$WP/v1/tenants/mystudio/members/dev%40studio.example" -H "Authorization: Bearer $WP_TOKEN"

List API keys#

GET/v1/tenants/{tenant}/keys
Auth
administer.

Response 200: an array of ApiKey, revoked ones included. Never a secret.

  • 200
  • 401, 403
curl -s "$WP/v1/tenants/mystudio/keys" -H "Authorization: Bearer $WP_TOKEN"

Make an API key#

POST/v1/tenants/{tenant}/keys
Auth
administer.
Body fieldTypeMeaning
name requiredstringWhat uses it, such as ci. Not empty.
role requiredstringAny role but owner.
project optionalstring or nullLimit the key to one project.

Response 201. The secret is shown only here:

{"key": {"id": "77d1...", "tenant": "mystudio", "name": "ci", "prefix": "wpk_1a2b3c4d", "role": "developer",
         "project": "mygame", "created_at": 1793250000, "revoked": false},
 "secret": "wpk_1a2b3c4d...", "note": "shown once: store it now"}
  • 201
  • 400 an owner key; no name
  • 401, 403
  • 404 no such project
  • 422 unknown role
curl -s "$WP/v1/tenants/mystudio/keys" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d '{"name": "ci", "role": "developer", "project": "mygame"}'

Revoke an API key#

DELETE/v1/tenants/{tenant}/keys/{id}
Auth
administer.
Path
id: the key's id (not its secret or prefix).
  • 204 revoked
  • 401, 403
  • 404 no such key
curl -s -X DELETE "$WP/v1/tenants/mystudio/keys/77d1..." -H "Authorization: Bearer $WP_TOKEN"

Audit log#

GET/v1/tenants/{tenant}/audit
Auth
read audit (owners and admins).
QueryTypeMeaning
before optionalintegerOnly records with a seq lower than this: pass the last seq you got for the next page.
limit optionalintegerDefault 100, at most 1000.

Response 200: {"records": [AuditRecord, ...]}, newest first (see AuditRecord). Actions include tenant.create, member.add, member.invite, member.join, member.remove, key.create, key.revoke, project.create, environment.create, environment.change, environment.deploy, environment.stop, environment.sleep, environment.wake, secret.set, secret.delete, build.create, build.upload, build.image, and the billing.* actions.

  • 200
  • 401, 403
curl -s "$WP/v1/tenants/mystudio/audit?limit=50" -H "Authorization: Bearer $WP_TOKEN"

Downloads#

Download the Unreal plugin#

GET/v1/tenants/{tenant}/downloads/unreal-plugin
Auth
Any member of the tenant, including project-scoped members and API keys. Needs an active subscription.

Response 200: application/zip, with Content-Disposition: attachment; filename="WorldPlane-UE5.8-{version}-{revision}.zip". It holds the WorldPlane and WorldPlaneGAS plugins; unzip it into your project's Plugins/ folder (see the Unreal guide). Each download is recorded in the audit log as plugin.download.

  • 200
  • 401, 403
  • 402: no active subscription
  • 404: this control plane has no plugin to serve
curl -fOJ "$WP/v1/tenants/mystudio/downloads/unreal-plugin" -H "Authorization: Bearer $WP_TOKEN"

Download the CLI#

GET/v1/downloads/cli/{platform}
Auth
Anyone signed in, or an API key. No organisation or subscription needed.
Path
platform: windows or linux (x86-64).

Response 200: application/octet-stream, the worldplane CLI, saved as worldplane.exe or worldplane.

  • 200
  • 401
  • 404: another platform, or this control plane has no CLI to serve
curl -fOJ "$WP/v1/downloads/cli/linux" -H "Authorization: Bearer $WP_TOKEN" && chmod +x worldplane

Billing#

Usage this month#

GET/v1/tenants/{tenant}/usage
Auth
read billing.
{
  "since": 1790812800,                 // the first second of this month, UTC
  "environments": [{"project": "mygame", "environment": "test", "server_hours": 41.5, "usd": 8.3}],
  "usd": 8.3,                          // this month's total
  "billing": "stripe",                 // or "estimate" where no Stripe is set up
  "subscribed": true,
  "subscription_status": "active",     // Stripe's status, or null before its first update
  "checkout": false,                   // whether POST .../billing/checkout can start a subscription
  "plan": true,                        // whether the Studio plan is required here
  "active": true,                      // whether uploads, deploys and wakes are allowed
  "portal": true,                      // whether POST .../billing/portal is available
  "max_per_month": 500.0               // the spend limit, or null
}

server_hours are standard-server hours: an hour of a plus server counts 2, of a max one 4.

  • 200
  • 401, 403
curl -s "$WP/v1/tenants/mystudio/usage" -H "Authorization: Bearer $WP_TOKEN"

Start a subscription (Stripe Checkout)#

POST/v1/tenants/{tenant}/billing/checkout
Auth
manage billing (owners and billing members).
Headers
Origin required: the panel's origin (https://...); Checkout returns to {Origin}/#/panel/{tenant}. No body.

Response 200: {"url": "https://checkout.stripe.com/..."}. Open it in a browser. The subscription is recorded when Stripe reports the completed Checkout.

  • 400 no usable Origin: set up billing from the panel
  • 401, 403
  • 404 no such tenant
  • 409 already subscribed; billing runs in estimate mode
  • 502 Stripe refused or didn't answer
curl -s -X POST "$WP/v1/tenants/mystudio/billing/checkout" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Origin: https://app.worldplane.net"

Manage billing (Stripe portal)#

POST/v1/tenants/{tenant}/billing/portal
Auth
manage billing.
Headers
Origin required, as for Checkout. No body.

Response 200: {"url": "https://billing.stripe.com/..."}, Stripe's customer portal: payment method, invoices, billing details and tax ID, cancelling.

  • 400 no usable Origin
  • 401, 403
  • 409 billing not set up yet; estimate mode
  • 502 Stripe
curl -s -X POST "$WP/v1/tenants/mystudio/billing/portal" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Origin: https://app.worldplane.net"

Set the spend limit#

PUT/v1/tenants/{tenant}/spend-limit
Auth
manage billing.
Body
max_per_month number or null: USD a month, 0 or more; null (or absent) removes the limit.

Once this month's spend reaches it, deploys answer 402. Running zones keep running. Response 200: {"max_per_month": 500.0}.

  • 200
  • 400 negative or not a number
  • 401, 403
curl -s -X PUT "$WP/v1/tenants/mystudio/spend-limit" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d '{"max_per_month": 500}'

Projects and environments#

List projects#

GET/v1/tenants/{tenant}/projects
Auth
Session or key, with a role in the organisation (403 otherwise). Lists only the projects the caller may read: an empty array when there are none.

Response 200: an array of Project.

curl -s "$WP/v1/tenants/mystudio/projects" -H "Authorization: Bearer $WP_TOKEN"

Create a project#

POST/v1/tenants/{tenant}/projects
Auth
administer.
Body
slug required string (slug rules); name required string.

Response 201: the Project.

  • 400 bad slug
  • 401, 403
  • 409 exists
curl -s "$WP/v1/tenants/mystudio/projects" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d '{"slug": "mygame", "name": "My Game"}'

List environments#

GET/v1/tenants/{tenant}/projects/{project}/environments
Auth
read on the project.

Response 200: an array of Environment.

  • 200
  • 401, 403
  • 404 no such project
curl -s "$WP/v1/tenants/mystudio/projects/mygame/environments" -H "Authorization: Bearer $WP_TOKEN"

Create an environment#

POST/v1/tenants/{tenant}/projects/{project}/environments
Auth
administer.
Body fieldTypeMeaning
slug requiredstringSlug rules; not smoke. dev and test are where developers may deploy.
name requiredstringIts display name.
region requiredstringAn id from GET /v1/regions. Can't change later.
zone requiredobjectZoneSettings; only map is required in it.
max_per_hour optionalnumber or nullBudget, USD an hour, 0 or more. Absent or null: none.

Response 201: the Environment, stopped.

  • 400 bad slug, unknown region, bad budget, smoke, bad zone (map, name, size, server counts)
  • 401, 403
  • 404 no such project
  • 409 exists
curl -s "$WP/v1/tenants/mystudio/projects/mygame/environments" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d '{
    "slug": "test", "name": "Test", "region": "us-east-2", "max_per_hour": 2.0,
    "zone": {"map": "/Game/Maps/World", "min_servers": 1, "max_servers": 8, "server_size": "standard",
             "warm_servers": 1, "sleep_after_minutes": 15, "extra_args": ["-MyGameFlag"]}}'

Get an environment and its zone#

GET/v1/tenants/{tenant}/projects/{project}/environments/{env}
Auth
read on the project.
{
  "environment": { ...Environment },
  "zone": {
    "running": true,
    "namespace": "wp-mystudio-mygame-test",
    "gateway": "203.0.113.10:7777",    // where players connect, once the gateway is up; else null
    "atlas_live": "/v1/tenants/mystudio/projects/mygame/environments/test/coordinator/v0/live",
    "fleet": {"fleets": [{"role": "zone", "members": [{"name": "...", "state": "Ready"}]}]}
  },
  "players": {
    "accounts": 12,                    // WorldPlane player accounts in this environment
    "public_key": "...",               // the key zones check player tokens with
    "sign_up": "/v1/play/mystudio/mygame/test/accounts",
    "sign_in": "/v1/play/mystudio/mygame/test/sessions"
  }
}

A zone that isn't running answers "zone": {"running": false, ...}. atlas_live is null when the control plane can't reach the zone's live feed.

  • 200
  • 401, 403
  • 404 no such environment
curl -s "$WP/v1/tenants/mystudio/projects/mygame/environments/test" -H "Authorization: Bearer $WP_TOKEN"

Change an environment#

PATCH/v1/tenants/{tenant}/projects/{project}/environments/{env}
Auth
administer.
Body fieldTypeMeaning
name optionalstringA new display name.
max_per_hour optionalnumber or nullA new budget; null removes it; absent leaves it.
zone optionalobjectThe whole ZoneSettings, as at creation: fields left out take their defaults, not their old values.

Other fields are refused (422): the region and slug can't change. A running zone keeps what it was deployed with until the next deploy. Response 200: the Environment.

  • 200
  • 400 bad budget or zone
  • 401, 403
  • 404
  • 422 an unknown field
curl -s -X PATCH "$WP/v1/tenants/mystudio/projects/mygame/environments/test" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d '{"max_per_hour": null, "name": "Playtest"}'

Deploy#

POST/v1/tenants/{tenant}/projects/{project}/environments/{env}/deploy
Auth
deploy on the environment.
Body
build required string: a build id of this project, ready.

Starts the zone with the build and the environment's current settings and secrets, and answers once it has started: 200 with {"zone": ...}, the zone as the coordinator reports it. Deploying an older build rolls back.

  • 200 started
  • 401, 403
  • 402 no active subscription; deploys on hold for an unpaid invoice; this month's spend has reached the limit
  • 404 no such environment or build
  • 409 the build isn't ready; the server arguments use {env:NAME} with no such secret
  • 500 the zone did not start (the environment is then failed)
curl -s "$WP/v1/tenants/mystudio/projects/mygame/environments/test/deploy" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d '{"build": "3f2a..."}'

Stop#

POST/v1/tenants/{tenant}/projects/{project}/environments/{env}/stop
Auth
deploy on the environment. No body.

Stops the zone's coordinator, servers and gateway; players are disconnected. The build and settings stay. A stopped zone starts again with a deploy (the wake endpoint doesn't start it).

  • 204 stopped
  • 401, 403
  • 404
curl -s -X POST "$WP/v1/tenants/mystudio/projects/mygame/environments/test/stop" -H "Authorization: Bearer $WP_TOKEN"

Secrets#

Values your servers get through {env:NAME} in their arguments. No answer, and no audit record, ever carries a value. A change takes effect at the next deploy or wake.

List secrets#

GET/v1/tenants/{tenant}/projects/{project}/environments/{env}/secrets
Auth
read on the project.
[{"name": "API_TOKEN", "updated": 1793250000}]
  • 200
  • 401, 403
  • 404 no such environment
curl -s "$WP/v1/tenants/mystudio/projects/mygame/environments/test/secrets" -H "Authorization: Bearer $WP_TOKEN"

Set a secret#

PUT/v1/tenants/{tenant}/projects/{project}/environments/{env}/secrets/{name}
Auth
manage secrets on the environment.
Path
name: 1 to 64 capitals, digits or _, starting with a letter; not starting with WP_.
Body
value required string: 1 byte to 4 KiB, no NUL. No other field.

Response: {"name": "API_TOKEN", "updated": 1793250000}.

  • 201 new
  • 200 replaced
  • 400 bad name or value, or a body that isn't {"value": "..."} (the answer never quotes it)
  • 401, 403
  • 404 no such environment
printf '{"value": "%s"}' "$API_TOKEN" | curl -s -X PUT \
  "$WP/v1/tenants/mystudio/projects/mygame/environments/test/secrets/API_TOKEN" \
  -H "Authorization: Bearer $WP_TOKEN" -H "Content-Type: application/json" --data-binary @-

Delete a secret#

DELETE/v1/tenants/{tenant}/projects/{project}/environments/{env}/secrets/{name}
Auth
manage secrets on the environment.
  • 204 deleted
  • 401, 403
  • 404 no such environment or secret
curl -s -X DELETE "$WP/v1/tenants/mystudio/projects/mygame/environments/test/secrets/API_TOKEN" \
  -H "Authorization: Bearer $WP_TOKEN"

Atlas and the zone's coordinator#

A hosted zone's coordinator is reachable only inside WorldPlane, so the control plane relays its live feed and its read-only calls to anyone who may read the environment. Atlas uses these.

Live feed (WebSocket)#

GET/v1/tenants/{tenant}/projects/{project}/environments/{env}/coordinator/v0/live
Auth
read on the environment, on the upgrade request (bearer header, or the session cookie from a browser on app.worldplane.net).
Query
Passed on to the coordinator as it is.

A WebSocket upgrade (wss://). Frames are relayed both ways until either side closes; their format is the coordinator's live map feed, which Atlas reads.

  • 101 switching protocols
  • 401, 403, 404
  • 502 the coordinator didn't answer
  • 503 the zone's live map can't be reached from here
websocat -H "Authorization: Bearer $WP_TOKEN" \
  "wss://app.worldplane.net/v1/tenants/mystudio/projects/mygame/environments/test/coordinator/v0/live"

Coordinator reads#

GET/v1/tenants/{tenant}/projects/{project}/environments/{env}/coordinator/v1/{path}
Auth
read on the environment.
Path
path: one of the coordinator's own /v1 GET paths, such as zone, servers, fleet, routes, telemetry, history, replay or entities/{id}. Empty, . and .. segments are refused.
Query
Passed on as it is.

Answers with the coordinator's status, content type and body. history is the zone's recent events (handoffs, servers coming and going), which the coordinator keeps for an hour. There is no endpoint for servers' raw logs.

  • the coordinator's status
  • 401, 403
  • 404 no such environment, or a refused path
  • 502, 503
curl -s "$WP/v1/tenants/mystudio/projects/mygame/environments/test/coordinator/v1/history" \
  -H "Authorization: Bearer $WP_TOKEN"

Builds#

A build is made, then its archive uploaded: in one request, or on app.worldplane.net straight to storage in parts. Then it is checked, imaged and started once, and ends ready, rejected or failed. Poll GET .../builds/{build} while it is checking, imaging or starting.

List builds#

GET/v1/tenants/{tenant}/projects/{project}/builds
Auth
read on the project.

Response 200: an array of Build, newest first.

curl -s "$WP/v1/tenants/mystudio/projects/mygame/builds" -H "Authorization: Bearer $WP_TOKEN"

Create a build#

POST/v1/tenants/{tenant}/projects/{project}/builds
Auth
upload, and an active subscription.
Body fieldTypeMeaning
label requiredstringYour version label.
kind requiredstringarchive: upload the zip next (state uploading). image: a server image already in a registry, named in program by digest (<registry>/<repository>@sha256:<64 hex>); ready at once, or rejected if not by digest. local: a program on the control plane's own machine, only where it allows that (not app.worldplane.net).
program optionalstringFor image (the image) and local (exe|first arguments).

Response 201: the Build.

  • 201
  • 400 an unknown kind; no image or program; local builds not allowed
  • 401, 403
  • 402 no active subscription
  • 404 no such project
curl -s "$WP/v1/tenants/mystudio/projects/mygame/builds" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d '{"label": "0.4.1", "kind": "archive"}'

Get a build#

GET/v1/tenants/{tenant}/projects/{project}/builds/{build}
Auth
read on the project.
  • 200 the Build
  • 401, 403
  • 404
curl -s "$WP/v1/tenants/mystudio/projects/mygame/builds/3f2a..." -H "Authorization: Bearer $WP_TOKEN"

Upload the archive in one request#

POST/v1/tenants/{tenant}/projects/{project}/builds/{build}/archive
Auth
upload.
Body
The zip's bytes, Content-Type: application/octet-stream, up to 20 GiB.

For a control plane that takes archives itself. On app.worldplane.net use uploads in parts instead. The archive is stored and checked before the answer: 200 with the Build, ready, imaging or rejected with its reason.

  • 200
  • 401, 403, 404
  • 409 this build takes no upload (not an archive, or not uploading)
curl -s "$WP/v1/tenants/mystudio/projects/mygame/builds/3f2a.../archive" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/octet-stream" --data-binary @LinuxServer.zip

Start an upload in parts#

POST/v1/tenants/{tenant}/projects/{project}/builds/{build}/uploads
Auth
upload.
Body
size required integer: the archive's bytes, 1 to 20 GiB.
{"part_size": 67108864, "parts": [{"number": 1, "url": "https://...presigned..."}, {"number": 2, "url": "..."}]}

PUT bytes (number − 1) × part_size onward, part_size of them (the last part is shorter), to each part's URL, with no authorization header. The URLs work for six hours. Keep each response's ETag header.

  • 200
  • 400 bad size
  • 401, 403, 404
  • 409 this control plane takes the archive itself (use .../archive); the build takes no upload
  • 502 storage
curl -s "$WP/v1/tenants/mystudio/projects/mygame/builds/3f2a.../uploads" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d "{\"size\": $(stat -c %s LinuxServer.zip)}"

Complete an upload in parts#

POST/v1/tenants/{tenant}/projects/{project}/builds/{build}/uploads/complete
Auth
upload.
Body
parts required: an array of {"number": integer, "etag": string}, every part.

Joins the parts, then stores and checks the archive as a direct upload is. Response 200: the Build.

  • 200
  • 401, 403, 404
  • 409 no upload of this build is under way
  • 502 storage
curl -s "$WP/v1/tenants/mystudio/projects/mygame/builds/3f2a.../uploads/complete" -H "Authorization: Bearer $WP_TOKEN" \
  -H "Content-Type: application/json" -d '{"parts": [{"number": 1, "etag": "\"9b2c...\""}, {"number": 2, "etag": "\"41fe...\""}]}'

Image a failed build again#

POST/v1/tenants/{tenant}/projects/{project}/builds/{build}/image
Auth
upload. No body.

For an archive build that is failed: makes its server image again from the archive it kept, then starts it once. Response 200: the Build, imaging.

  • 200
  • 401, 403, 404
  • 409 only an archive whose imaging failed is imaged again
curl -s -X POST "$WP/v1/tenants/mystudio/projects/mygame/builds/3f2a.../image" -H "Authorization: Bearer $WP_TOKEN"

Players#

Public routes for your game's launcher or login service. They need no WorldPlane authentication.

Player sign-up#

POST/v1/play/{tenant}/{project}/{env}/accounts
Auth
None.
Body fieldTypeMeaning
email requiredstringThe player's e-mail.
name requiredstring1 to 32 characters.
password requiredstringAt least 10 characters.

Player accounts belong to one environment. Response 200: a token, signed with the environment's key, good for an hour (expires in Unix seconds). The client connects to the gateway with ?token=....

{"player": {"id": "c3d4...", "name": "Aria"}, "token": "...", "expires": 1793253600}
  • 200
  • 400 e-mail, password or name
  • 404 no such game environment
  • 409 a player with that e-mail exists
curl -s "$WP/v1/play/mystudio/mygame/test/accounts" -H "Content-Type: application/json" \
  -d '{"email": "player@example.com", "name": "Aria", "password": "a long password"}'

Player sign-in#

POST/v1/play/{tenant}/{project}/{env}/sessions
Auth
None.
Body
email required string; password required string.

Response 200: as for player sign-up. Throttled per e-mail and environment, as people's sign-ins are.

  • 200
  • 401 wrong e-mail or password
  • 404
  • 429
curl -s "$WP/v1/play/mystudio/mygame/test/sessions" -H "Content-Type: application/json" \
  -d '{"email": "player@example.com", "password": "a long password"}'

Wake a sleeping zone#

POST/v1/play/{tenant}/{project}/{env}/wake
Auth
None: waking only starts a zone its studio deployed, which sleeps again when nobody comes. No body.

Call it before connecting, then every few seconds until it answers running. From cold a zone takes a few minutes.

Environment isAnswer
sleeping202 {"state": "starting"}, and it starts (needs an active subscription, else 402)
starting, or running with its gateway not up yet202 {"state": "starting"}
running200 {"state": "running", "gateway": "203.0.113.10:7777"}
stopped or failed409: not deployed; the studio deploys a build (wake never starts a stopped zone)
  • 404 no such environment
curl -s -X POST "$WP/v1/play/mystudio/mygame/test/wake"

Stripe webhook#

POST/v1/billing/stripe/webhook

Not for callers. This is where Stripe sends WorldPlane's billing events. Requests must carry a valid Stripe-Signature for WorldPlane's own endpoint secret, at most five minutes old; anything else gets 400.

What it does with Stripe's events, which you see in the audit log as actor stripe:

  • checkout.session.completed records the organisation's subscription (billing.subscribe);
  • customer.subscription.updated records its status (billing.subscription); a canceled one, like customer.subscription.deleted, clears it (billing.unsubscribe) and stops the organisation's running zones;
  • invoice.payment_failed holds deploys (billing.hold); invoice.paid lifts the hold (billing.resume).

Each event is handled once, by its id. It answers 200 for any verified event, 400 for a bad signature or body, and 404 where billing runs in estimate mode.