HTTP routes
"Auth" means Authorization: Bearer <access token> is required; a request without a valid token
answers 401 (unauthorized / token_expired) or 403 banned before anything else happens.
Every route can also answer 429 rate_limited, 500 internal and 503 unavailable; the tables
list the other answers. Schemas are written as JSON with ? marking optional fields.
Shared shapes#
Ack {}
Account {"id":int, "email":string|null, "email_verified":bool, "display_name":string|null,
"roles":[string], "identities":[LinkedIdentity], "created_at":ms}
LinkedIdentity {"provider":"steam", "subject":string}
TokenPair {"token_type":"Bearer", "access_token":string, "access_expires_at":ms,
"refresh_token":string, "refresh_expires_at":ms}
AuthSession {"account":Account, "tokens":TokenPair}
StorageObject {"collection":string, "key":string, "owner":int, "value":JSON, "version":int,
"write":"owner"|"server", "updated_at":ms}
StorageObjectInfo {"collection":string, "key":string, "version":int, "write":"owner"|"server",
"size_bytes":int, "updated_at":ms}
ObjectAck {"collection":string, "key":string, "version":int, "updated_at":ms}
RoomInfo {"id":int, "kind":"room"|"dm"|"group", "key"?:string, "name"?:string,
"member_count"?:int, "max_members"?:int, "peer"?:int}
ChatMessage {"id":int, "room":int, "sender":int, "sender_name"?:string|null, "text":string,
"sent_at":ms, "nonce"?:string}
Page<T> {"items":[T], "next_cursor"?:string}
RoomInfo.kind:room(public),dm(direct messages, withpeer= the other user),group(members only).member_countcounts users online in the room now;max_membersis the cap in connections.StorageObject.write:owner(the player may write it) orserver(server-locked: player writes get 403).
Server#
| Method | Path | Auth | Request | Answer |
|---|---|---|---|---|
| GET | /v1/info |
no | – | {"protocol":1,"min_protocol":1,"modules":[string]} |
| GET | /healthz |
no | – | {"status":"ok"} |
| GET | /readyz |
no | – | {"status":"ready"}; 503 when not ready |
| GET | /v1/openapi.json |
no | – | the OpenAPI document (when enabled) |
| GET | /v1/asyncapi.json |
no | – | the AsyncAPI document (when enabled) |
| GET (upgrade) | /v1/ws |
handshake header or first message | – | the WebSocket |
Auth#
POST /v1/auth/register#
Create an account with email and password; it is logged in at once.
- Auth: no.
- Body:
{"email":string, "password":string, "display_name"?:string}. - 200:
AuthSession. A verification mail is sent. - Errors: 403
forbidden(registration closed, or refused by a server rule); 409email_taken; 422validation_failed(details per field); 429rate_limited.
POST /v1/auth/login#
- Auth: no.
- Body:
{"email":string, "password":string}. - 200:
AuthSession. - Errors: 401
invalid_credentials; 403banned(detailsuntil),email_not_verified, or a server rule's refusal; 429rate_limited(too many attempts from this address or for this account); 503unavailable(password hashing is saturated: retry shortly).
POST /v1/auth/steam#
- Auth: no; optional Bearer of a recent login to link Steam to that account.
- Body:
{"ticket_hex":string, "identity":string}(the ticket hex-encoded, at most 8192 characters; the identity string the ticket was requested for). - 200:
AuthSession. - Errors: 401
steam_auth_failed, or the Bearer token sent istoken_expired/unauthorized; 403banned, a refused borrowed copy, orreauthentication_required(linking needs a login younger than 10 minutes); 404 Steam login is not enabled on this server; 409conflict(the Steam account is linked to another account, or this account already has one).
POST /v1/auth/refresh#
- Auth: no.
- Body:
{"refresh_token":string}. - 200:
TokenPair(a new pair; the old refresh token is used up). - Errors: 401
unauthorized(invalid, expired or revoked) orrefresh_token_reused(the session was revoked); 403banned.
POST /v1/auth/logout#
Revoke this session, or every session of the account.
- Auth: the Bearer token or
refresh_tokenin the body. - Body:
{"everywhere"?:bool, "refresh_token"?:string}(send{}for "this session, by Bearer"). - 200:
{}. - Errors: 401
unauthorized(neither a valid access token nor a valid refresh token).
POST /v1/auth/email/verify#
- Auth: no.
- Body:
{"token":string}(the one-time token from the mail). - 200:
{}. - Errors: 400
invalid_token.
POST /v1/auth/email/resend#
Send the verification mail again (nothing happens when the address is confirmed already).
- Auth: yes. No body.
- 200:
{}. - Errors: 401; 429
rate_limited.
POST /v1/auth/password/forgot#
- Auth: no.
- Body:
{"email":string}. - 200:
{}, always the same whether or not the address has an account (a mail is sent if it does). - Errors: 429
rate_limited.
POST /v1/auth/password/reset#
- Auth: no.
- Body:
{"token":string, "new_password":string}. - 200:
{}. Every session of the account is revoked. - Errors: 400
invalid_token; 422validation_failed.
Account#
GET /v1/account#
- Auth: yes.
- 200:
Account.
PATCH /v1/account#
Change the caller's account (absent fields stay).
- Auth: yes.
- Body:
{"display_name"?:string}. - 200: the changed
Account. - Errors: 422
validation_failed.
POST /v1/account/password#
Change the password, knowing the current one; the other sessions are revoked (this one stays).
- Auth: yes.
- Body:
{"current_password":string, "new_password":string}. - 200:
{}. - Errors: 401
invalid_credentials(the current password is wrong); 422validation_failed.
DELETE /v1/account/identities/{provider}#
Unlink a login provider (steam) from the caller's account.
- Auth: yes, with a login younger than 10 minutes.
- 200:
{}. - Errors: 403
reauthentication_required(log in again first); 404not_found(no such linked provider); 409conflict(it is the account's only way to log in).
Storage#
Every route addresses the caller's own objects. See Storage flows.
GET /v1/storage/{collection}#
A page of the caller's objects in a collection, without values, ordered by key.
- Auth: yes.
- Query:
cursor?,limit? (1–100, default 50). - 200:
Page<StorageObjectInfo>. - Errors: 400
bad_request(an invalid name or cursor).
GET /v1/storage/{collection}/{key}#
- Auth: yes.
- 200:
StorageObject; headerETag: "<version>". - Errors: 404
not_found.
PUT /v1/storage/{collection}/{key}#
Write one object. Without a condition the last write wins.
- Auth: yes.
- Body:
{"value":JSON, "if_version"?:int}.if_version: Nwrites only over version N;if_version: 0writes only if the object does not exist. - Headers (alternative to
if_version):If-Match: "N",If-None-Match: *. - 200:
ObjectAck(with the newversion); headerETag: "<version>". - Errors: 400
bad_request(an invalid name, a malformed condition header, or a header that disagrees withif_version); 403forbidden(the object or its collection is written by the server only),quota_exceeded(too many objects or bytes), or a server rule's refusal; 409version_conflict(details{"current_version":N}); 413payload_too_large; 422validation_failed(the value is too large); 429rate_limited(detailsretry_after_ms).
DELETE /v1/storage/{collection}/{key}#
Deleting an object that does not exist answers {} too, unless a version is named.
- Auth: yes.
- Query:
if_version? (only if the stored version is this one). Header alternative:If-Match: "N". - 200:
{}. - Errors: 403
forbidden(server-locked) or a server rule's refusal; 409version_conflict; 429rate_limited.
POST /v1/storage/_batch/get#
Read several objects at once.
- Auth: yes.
- Body:
{"objects":[{"collection":string, "key":string}]}(1–16 distinct objects). - 200:
{"objects":[StorageObject]}; missing objects are simply absent. - Errors: 413
payload_too_large(more than 4 MiB of values: read fewer per batch); 422validation_failed.
POST /v1/storage/_batch/put#
Write several objects in one transaction: all or nothing.
- Auth: yes.
- Body:
{"objects":[{"collection":string, "key":string, "value":JSON, "if_version"?:int}]}(1–16 distinct objects, at most 4 MiB of values together). - 200:
{"objects":[ObjectAck]}, in request order. - Errors: 403
forbidden/quota_exceeded/ a server rule's refusal (details:indexof the failing item); 409version_conflict(details:indexandcurrent_versionof the first failing item); 422validation_failed; 429rate_limited(a batch counts as one write).
Chat#
Joining, sending and live messages use the WebSocket; these HTTP routes list rooms, read history, open direct messages and delete messages. See Chat flows.
GET /v1/chat/rooms#
The public rooms, with online counts and caps.
- Auth: yes.
- Query:
cursor?,limit?. - 200:
Page<RoomInfo>.
GET /v1/chat/rooms/{room}/messages#
A page of a room's history, newest first.
- Auth: yes. Public rooms: any player; group and DM rooms: their members.
- Query:
cursor? (older messages),limit?. - 200:
Page<ChatMessage>. - Errors: 403
not_a_member; 404not_found.
DELETE /v1/chat/rooms/{room}/messages/{message}#
Delete a message: its sender, or a moderator (roles admin / moderator by default). The room
gets a chat.deleted push and the message leaves the history.
- Auth: yes.
- 200:
{}. - Errors: 403
forbidden(neither its sender nor a moderator); 404not_found(no such message, or deleted already).
POST /v1/chat/dm#
Open (or find) the direct-message room with another user.
- Auth: yes.
- Body:
{"user":int}. - 200:
RoomInfo(kind: "dm", withpeer). - Errors: 400
bad_request(yourself); 403 a server rule's refusal (for example a block); 404not_found(no such account); 429rate_limited(detailsretry_after_ms).
GET /v1/chat/dms#
The caller's direct-message rooms, newest activity first.
- Auth: yes.
- Query:
cursor?,limit?. - 200:
Page<RoomInfo>(each withpeer).
Admin (role admin only)# admin only
Every admin route needs the Bearer token of an account with the admin role; others get 403
forbidden. Every admin action is recorded in the audit log. These routes are for operator tools,
never for player-facing pages.
AdminUser {"account":Account, "active_sessions":int, "last_seen_at"?:ms, "ban"?:BanInfo}
BanInfo {"banned_at":ms, "until"?:ms, "reason"?:string}
AuditEntry {"id":int, "action":string, "actor"?:int, "target_type"?:string, "target_id"?:string,
"ip"?:string, "request_id"?:string, "data"?:object, "created_at":ms}
| Method | Path | Request | Answer | Errors |
|---|---|---|---|---|
| GET | /v1/admin/users |
query q? (part of the email, case-insensitive, or display name, or an id), cursor?, limit? |
Page<AdminUser>, newest first |
|
| GET | /v1/admin/users/{user} |
– | AdminUser |
404 |
| POST | /v1/admin/users/{user}/ban |
{"until"?:ms (in the future; absent = until lifted), "reason"?:string (≤ 255 characters)} |
{}; sessions revoked, sockets closed with 4003 |
404; 409 conflict (yourself, or an admin: remove the role first); 422 |
| POST | /v1/admin/users/{user}/unban |
– | {} |
404 |
| DELETE | /v1/admin/users/{user}/sessions |
– | {}; every session revoked |
404 |
| DELETE | /v1/admin/users/{user}/identities/{provider} |
– | {} (unlink, e.g. steam) |
404 |
| PUT | /v1/admin/users/{user}/roles/{role} |
– | {} (granted, or already held) |
404 |
| DELETE | /v1/admin/users/{user}/roles/{role} |
– | {} (revoked, or not held) |
409 conflict (your own admin role, or the last admin) |
| GET | /v1/admin/audit |
query user? (acted or was the target), action? (one action, or a prefix ending in . such as admin.), cursor?, limit? |
Page<AuditEntry>, newest first |
|
| GET | /v1/admin/users/{user}/storage/{collection} |
query cursor?, limit? |
Page<StorageObjectInfo> |
|
| GET | /v1/admin/users/{user}/storage/{collection}/{key} |
– | StorageObject |
404 |
| PUT | /v1/admin/users/{user}/storage/{collection}/{key} |
{"value":JSON, "if_version"?:int, "write"?:"owner"\|"server"} (write absent: unchanged; new objects owner) |
ObjectAck |
404 (no such account); 409 version_conflict; 422 |
| DELETE | /v1/admin/users/{user}/storage/{collection}/{key} |
query if_version? |
{} (also for a server-locked object) |
409 version_conflict |
An admin storage write ignores the owner's write lock and the owner's quotas.
Generated from API.md (repository commit e25edb9, file sha256 3db87f7bba70).