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) andPOST /v1/accounts(sign up) answer with atoken(64 hex characters) and also set it as thewp_sessioncookie (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:
| Needs | Owner | Admin | Operator | Developer | Viewer | Billing |
|---|---|---|---|---|---|---|
| read: projects, environments, builds, zones, secrets' names | yes | yes | yes | yes | yes | no |
| upload: builds | yes | yes | yes | yes | no | no |
| deploy: deploy and stop | yes | yes | yes | dev, test | no | no |
| manage secrets | yes | yes | dev, test | dev, test | no | no |
| administer: members, keys, projects, environments | yes | yes | no | no | no | no |
| read billing: usage | yes | yes | no | no | no | yes |
| manage billing: checkout, portal, spend limit | yes | no | no | no | no | yes |
| read audit | yes | yes | no | no | no | no |
| own: add or remove owners | yes | no | no | no | no | no |
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"}
| Status | Means |
|---|---|
400 | A value is wrong (a slug, a map path, a size); the sentence says which. |
401 | No or unknown token or key (sign in, or send an API key), or a wrong password or code. |
402 | Payment needed: no active subscription (uploading, deploying, waking), deploys on hold for an unpaid invoice, or the spend limit reached (deploying). See billing. |
403 | Your 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. |
404 | no such tenant, project, environment, build, key, member or secret, or a path the API doesn't have. |
409 | It 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). |
429 | Too many wrong sign-ins for that e-mail; the sentence says how many minutes to wait. |
500 | The control plane couldn't store it, or a zone didn't start. |
501 | Password resets on a control plane that sends no e-mail. |
502, 503 | Stripe, 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#
- GET/v1/health
- POST/v1/accounts
- POST/v1/sessions
- DELETE/v1/sessions/current
- POST/v1/password-resets
- POST/v1/password-resets/confirm
- GET/v1/me
- POST/v1/me/totp
- POST/v1/me/totp/confirm
- POST/v1/me/totp/remove
- GET/v1/regions
- POST/v1/tenants
- GET/v1/tenants/{tenant}
- GET…/members
- POST…/members
- DELETE…/members/{email}
- GET…/keys
- POST…/keys
- DELETE…/keys/{id}
- GET…/audit
- GET…/downloads/unreal-plugin
- GET/v1/downloads/cli/{platform}
- GET…/usage
- POST…/billing/checkout
- POST…/billing/portal
- PUT…/spend-limit
- GET…/projects
- POST…/projects
- GET…/environments
- POST…/environments
- GET…/environments/{env}
- PATCH…/environments/{env}
- POST…/{env}/deploy
- POST…/{env}/stop
- GET…/{env}/secrets
- PUT…/{env}/secrets/{name}
- DELETE…/{env}/secrets/{name}
- GET…/{env}/coordinator/v0/live
- GET…/{env}/coordinator/v1/{path}
- GET…/builds
- POST…/builds
- GET…/builds/{build}
- POST…/builds/{build}/archive
- POST…/builds/{build}/image
- POST…/builds/{build}/uploads
- POST…/uploads/complete
- POST/v1/play/…/accounts
- POST/v1/play/…/sessions
- POST/v1/play/…/wake
- POST/v1/billing/stripe/webhook
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#
| Field | Type | Default | Meaning |
|---|---|---|---|
map required | string | The Unreal package path every server opens, starting with /, such as /Game/Maps/World. No : or \. | |
name | string | main | The zone's name in the environment: a slug. |
half_size_m | number | 1000 | How far the zone reaches from the map's origin along x and y, metres. Above 0. |
min_servers | integer | 1 | At least 1. |
max_servers | integer | 4 | At least min_servers. |
server_capacity | number | 100 | The load one server carries. |
server_size | string | standard | standard, plus or max (large and xlarge are read as plus and max). |
warm_servers | integer | 1 | Booted, idle servers kept ready. |
sleep_after_minutes | integer or null | null | Minutes with no players before the zone sleeps; null never. |
extra_args | string[] | [] | 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#
/v1/health- Auth
- None.
- Response
200, the textok(not JSON).
curl -s "$WP/v1/health"
Sign up#
/v1/accounts- Auth
- None.
| Body field | Type | Meaning |
|---|---|---|
email required | string | An e-mail address. |
name required | string | Not empty. |
password required | string | At 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.
400not an e-mail address, password too short, no name403sign-up is closed on this control plane409an 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#
/v1/sessions- Auth
- None.
| Body field | Type | Meaning |
|---|---|---|
email required | string | |
password required | string | |
code optional | string or null | The authenticator's 6-digit code, once the account has one. |
Response 200: the same as sign up, with the cookie set.
401wrong e-mail or password;enter the 6-digit code from your authenticator app(none sent: send it again withcode); a wrong code429too 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#
/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#
/v1/password-resets- Auth
- None.
- Body
emailrequired 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"}
202accepted501this 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#
/v1/password-resets/confirm- Auth
- None: the token from the link (
#/reset/<token>).
| Body field | Type | Meaning |
|---|---|---|
token required | string | From the link. Works once. |
password required | string | At least 10 characters. |
Sets the password and ends every session the account had.
204done400password 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#
/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"]
}
200401not signed in
curl -s "$WP/v1/me" -H "Authorization: Bearer $WP_TOKEN"
Set up an authenticator#
/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"}
200401403a key
curl -s -X POST "$WP/v1/me/totp" -H "Authorization: Bearer $WP_TOKEN"
Confirm the authenticator#
/v1/me/totp/confirm- Auth
- A person's session.
- Body
coderequired string: the code the app shows now.
From now on, signing in needs a code.
204set up400nothing pending; the code doesn't match401,403
curl -s "$WP/v1/me/totp/confirm" -H "Authorization: Bearer $WP_TOKEN" \
-H "Content-Type: application/json" -d '{"code": "123456"}'
Remove the authenticator#
/v1/me/totp/remove- Auth
- A person's session.
- Body
coderequired string: a current code.
204removed400none set up; the code doesn't match401,403
curl -s "$WP/v1/me/totp/remove" -H "Authorization: Bearer $WP_TOKEN" \
-H "Content-Type: application/json" -d '{"code": "123456"}'
Regions#
/v1/regions- Auth
- Session or key.
The regions environments may run in.
{"regions": [{"id": "us-east-2", "name": "US East (Ohio)"}]}
200401
curl -s "$WP/v1/regions" -H "Authorization: Bearer $WP_TOKEN"
Organisations, members and keys#
Create an organisation#
/v1/tenants- Auth
- A person's session; the caller becomes its owner.
| Body field | Type | Meaning |
|---|---|---|
slug required | string | 2 to 18 lowercase letters, digits or -, starting with a letter; not platform, worldplane, system, kube, aws or local. |
name required | string | Its display name. |
Response 201: the Tenant.
400bad slug401403a key409the 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#
/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"}]}
200401,403404no such tenant
curl -s "$WP/v1/tenants/mystudio" -H "Authorization: Bearer $WP_TOKEN"
List members#
/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"}]}
200401,403
curl -s "$WP/v1/tenants/mystudio/members" -H "Authorization: Bearer $WP_TOKEN"
Add or invite a member#
/v1/tenants/{tenant}/members- Auth
- administer; adding an owner needs own.
| Body field | Type | Meaning |
|---|---|---|
email required | string | Their e-mail. |
role required | string | owner, admin, operator, developer, viewer or billing. |
project optional | string or null | A 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.
201added202invited400not an e-mail address401,403404no such project422unknown 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#
/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.
204removed401,403404no such member409an 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#
/v1/tenants/{tenant}/keys- Auth
- administer.
Response 200: an array of ApiKey, revoked ones included. Never a secret.
200401,403
curl -s "$WP/v1/tenants/mystudio/keys" -H "Authorization: Bearer $WP_TOKEN"
Make an API key#
/v1/tenants/{tenant}/keys- Auth
- administer.
| Body field | Type | Meaning |
|---|---|---|
name required | string | What uses it, such as ci. Not empty. |
role required | string | Any role but owner. |
project optional | string or null | Limit 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"}
201400an owner key; no name401,403404no such project422unknown 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#
/v1/tenants/{tenant}/keys/{id}- Auth
- administer.
- Path
id: the key's id (not its secret or prefix).
204revoked401,403404no such key
curl -s -X DELETE "$WP/v1/tenants/mystudio/keys/77d1..." -H "Authorization: Bearer $WP_TOKEN"
Audit log#
/v1/tenants/{tenant}/audit- Auth
- read audit (owners and admins).
| Query | Type | Meaning |
|---|---|---|
before optional | integer | Only records with a seq lower than this: pass the last seq you got for the next page. |
limit optional | integer | Default 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.
200401,403
curl -s "$WP/v1/tenants/mystudio/audit?limit=50" -H "Authorization: Bearer $WP_TOKEN"
Downloads#
Download the Unreal plugin#
/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.
200401,403402: no active subscription404: 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#
/v1/downloads/cli/{platform}- Auth
- Anyone signed in, or an API key. No organisation or subscription needed.
- Path
platform:windowsorlinux(x86-64).
Response 200: application/octet-stream, the worldplane CLI, saved as worldplane.exe or worldplane.
200401404: 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#
/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.
200401,403
curl -s "$WP/v1/tenants/mystudio/usage" -H "Authorization: Bearer $WP_TOKEN"
Start a subscription (Stripe Checkout)#
/v1/tenants/{tenant}/billing/checkout- Auth
- manage billing (owners and billing members).
- Headers
Originrequired: 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.
400no usableOrigin:set up billing from the panel401,403404no such tenant409already subscribed; billing runs in estimate mode502Stripe 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)#
/v1/tenants/{tenant}/billing/portal- Auth
- manage billing.
- Headers
Originrequired, 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.
400no usableOrigin401,403409billing not set up yet; estimate mode502Stripe
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#
/v1/tenants/{tenant}/spend-limit- Auth
- manage billing.
- Body
max_per_monthnumber 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}.
200400negative or not a number401,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#
/v1/tenants/{tenant}/projects- Auth
- Session or key, with a role in the organisation (
403otherwise). 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#
/v1/tenants/{tenant}/projects- Auth
- administer.
- Body
slugrequired string (slug rules);namerequired string.
Response 201: the Project.
400bad slug401,403409exists
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#
/v1/tenants/{tenant}/projects/{project}/environments- Auth
- read on the project.
Response 200: an array of Environment.
200401,403404no such project
curl -s "$WP/v1/tenants/mystudio/projects/mygame/environments" -H "Authorization: Bearer $WP_TOKEN"
Create an environment#
/v1/tenants/{tenant}/projects/{project}/environments- Auth
- administer.
| Body field | Type | Meaning |
|---|---|---|
slug required | string | Slug rules; not smoke. dev and test are where developers may deploy. |
name required | string | Its display name. |
region required | string | An id from GET /v1/regions. Can't change later. |
zone required | object | ZoneSettings; only map is required in it. |
max_per_hour optional | number or null | Budget, USD an hour, 0 or more. Absent or null: none. |
Response 201: the Environment, stopped.
400bad slug, unknown region, bad budget,smoke, bad zone (map, name, size, server counts)401,403404no such project409exists
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#
/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.
200401,403404no such environment
curl -s "$WP/v1/tenants/mystudio/projects/mygame/environments/test" -H "Authorization: Bearer $WP_TOKEN"
Change an environment#
/v1/tenants/{tenant}/projects/{project}/environments/{env}- Auth
- administer.
| Body field | Type | Meaning |
|---|---|---|
name optional | string | A new display name. |
max_per_hour optional | number or null | A new budget; null removes it; absent leaves it. |
zone optional | object | The 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.
200400bad budget or zone401,403404422an 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#
/v1/tenants/{tenant}/projects/{project}/environments/{env}/deploy- Auth
- deploy on the environment.
- Body
buildrequired 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.
200started401,403402no active subscription; deploys on hold for an unpaid invoice; this month's spend has reached the limit404no such environment or build409the build isn't ready; the server arguments use{env:NAME}with no such secret500the zone did not start (the environment is thenfailed)
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#
/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).
204stopped401,403404
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#
/v1/tenants/{tenant}/projects/{project}/environments/{env}/secrets- Auth
- read on the project.
[{"name": "API_TOKEN", "updated": 1793250000}]
200401,403404no such environment
curl -s "$WP/v1/tenants/mystudio/projects/mygame/environments/test/secrets" -H "Authorization: Bearer $WP_TOKEN"
Set a secret#
/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 withWP_.- Body
valuerequired string: 1 byte to 4 KiB, no NUL. No other field.
Response: {"name": "API_TOKEN", "updated": 1793250000}.
201new200replaced400bad name or value, or a body that isn't{"value": "..."}(the answer never quotes it)401,403404no 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#
/v1/tenants/{tenant}/projects/{project}/environments/{env}/secrets/{name}- Auth
- manage secrets on the environment.
204deleted401,403404no 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)#
/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.
101switching protocols401,403,404502the coordinator didn't answer503the 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#
/v1/tenants/{tenant}/projects/{project}/environments/{env}/coordinator/v1/{path}- Auth
- read on the environment.
- Path
path: one of the coordinator's own/v1GET paths, such aszone,servers,fleet,routes,telemetry,history,replayorentities/{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,403404no such environment, or a refused path502,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#
/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#
/v1/tenants/{tenant}/projects/{project}/builds- Auth
- upload, and an active subscription.
| Body field | Type | Meaning |
|---|---|---|
label required | string | Your version label. |
kind required | string | archive: 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 optional | string | For image (the image) and local (exe|first arguments). |
Response 201: the Build.
201400an unknown kind; no image or program; local builds not allowed401,403402no active subscription404no 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#
/v1/tenants/{tenant}/projects/{project}/builds/{build}- Auth
- read on the project.
200the Build401,403404
curl -s "$WP/v1/tenants/mystudio/projects/mygame/builds/3f2a..." -H "Authorization: Bearer $WP_TOKEN"
Upload the archive in one request#
/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.
200401,403,404409this build takes no upload (not an archive, or notuploading)
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#
/v1/tenants/{tenant}/projects/{project}/builds/{build}/uploads- Auth
- upload.
- Body
sizerequired 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.
200400bad size401,403,404409this control plane takes the archive itself (use.../archive); the build takes no upload502storage
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#
/v1/tenants/{tenant}/projects/{project}/builds/{build}/uploads/complete- Auth
- upload.
- Body
partsrequired: 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.
200401,403,404409no upload of this build is under way502storage
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#
/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.
200401,403,404409only 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#
/v1/play/{tenant}/{project}/{env}/accounts- Auth
- None.
| Body field | Type | Meaning |
|---|---|---|
email required | string | The player's e-mail. |
name required | string | 1 to 32 characters. |
password required | string | At 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}
200400e-mail, password or name404no such game environment409a 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#
/v1/play/{tenant}/{project}/{env}/sessions- Auth
- None.
- Body
emailrequired string;passwordrequired string.
Response 200: as for player sign-up. Throttled per e-mail and environment, as people's sign-ins are.
200401wrong e-mail or password404429
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#
/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 is | Answer |
|---|---|
sleeping | 202 {"state": "starting"}, and it starts (needs an active subscription, else 402) |
starting, or running with its gateway not up yet | 202 {"state": "starting"} |
running | 200 {"state": "running", "gateway": "203.0.113.10:7777"} |
stopped or failed | 409: not deployed; the studio deploys a build (wake never starts a stopped zone) |
404no such environment
curl -s -X POST "$WP/v1/play/mystudio/mygame/test/wake"
Stripe webhook#
/v1/billing/stripe/webhookNot 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.completedrecords the organisation's subscription (billing.subscribe);customer.subscription.updatedrecords its status (billing.subscription); a canceled one, likecustomer.subscription.deleted, clears it (billing.unsubscribe) and stops the organisation's running zones;invoice.payment_failedholds deploys (billing.hold);invoice.paidlifts 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.