The HTTP surface at a glance
The full request surface, grouped by the three request spaces.
Every /api/engine/* route requires a Firebase bearer; /api/bot/action is
HMAC-authenticated; the web routes are public.
This page is the map. For per-operation request/response schemas, see the
generated HTTP API reference, or read
the openapi.json spec directly.
Client API: /api/engine
Reads (D1-only, never wake a DO):
| Method + path | Purpose |
|---|---|
GET /lobby | Public joinable games, newest first |
GET /me/sync?finishedAfter= | Everything a device keeps about the caller, in one read |
GET /games/{id} | One game's summary (capability read; never state) |
GET /games/{id}/frames?from=&to= | Version-range frames: live gap recovery and finished-game replay |
GET /players?ids= | Batch public identity (≤ 50), never email |
GET /bots | The bot catalog |
GET /me · GET /me/games/finished?cursor= | The caller's own profile / history older than a device holds |
GET /friends/games | Joinable games created by the caller's friends |
GET /users/search?q= | Friend-picker search (registered only) |
Game lifecycle (Commands to the DO; policy at the edge, integrity in the DO):
| Method + path | Purpose |
|---|---|
POST /games · POST /games/solo | Create (Worker-direct D1) · create-and-start vs bots |
POST /games/{id}/join · POST /games/join-by-code | Join |
POST /games/{id}/leave · /cancel · /add-bot · /start | Waiting-room commands |
POST /games/{id}/action · /forfeit | Active play (carry the caller's seat) |
POST /games/{id}/socket-ticket | Mint a signed, game-scoped ticket valid for 60 seconds |
GET /games/{id}/session | The caller's current session snapshot, from the game's own Durable Object |
GET /games/{id}/socket | WebSocket upgrade (?ticket= auth); per-seat session snapshots |
Offline play (creator-only, local-origin only):
| Method + path | Purpose |
|---|---|
POST /games/local | Register and start a game already played on the device |
POST /games/{id}/local/transitions | Append a batch of the device's transition log against its current version |
GET /games/{id}/local | The device's stored log (seed + raw state and actions), to resume on another device |
Profile / account / devices / social writes:
| Method + path | Purpose |
|---|---|
PUT /me/username · PUT /me/avatar · DELETE /me | Rename · upload avatar · delete account |
PUT /me/devices · DELETE /me/devices/{fid} | FCM device register / deregister |
POST /friends/requests · /requests/{id}/accept · DELETE /friends/{id} | Friend request / accept / remove |
POST + DELETE /friends/{id}/block | Block / unblock |
Bot webhook: /api/bot
POST /api/bot/action lets an external bot submit a move, authenticated by the
Eigen-Signature HMAC over the exact body. See
External-bot HMAC.
Game and external-bot JSON request bodies are limited to 64 KiB before auth or parsing. Game configuration and action payloads must fit inside that bound.
Public web
GET /.well-known/assetlinks.json · apple-app-site-association ·
GET /join/:shortCode · GET /game/:gameId (share/landing) ·
GET /avatars/:uid (when avatars enabled).
When site is configured: GET / (landing) · GET /terms · GET /privacy ·
GET /delete-account · GET /sitemap.xml · GET /robots.txt ·
GET /site.webmanifest. Each is overridden by a matching public/ file.
GET /health is always mounted and is deliberately absent from openapi.json;
see Deploying.
The error model
Every failure is one JSON shape, { error, code? }, with the HTTP status
carrying the coarse class and the optional stable code carrying the machine
reason a client keys retry/resync UX off. Handlers only ever return their
declared 200 shape; a failure is an HttpError throw (or a kernel/lobby
rejection converted to one) rendered by the app-level error handler.
| Status | Meaning | Representative codes |
|---|---|---|
| 400 | Client mistake | invalidPayload, illegalMove, notLocalBot |
| 401 | Missing/invalid token | none |
| 403 | Ownership/permission refusal | notCreator, notParticipant, localOnly |
| 404 | No such game/user | unknownGame |
| 409 | Stale view, lifecycle conflict, or version mismatch | stateUpdated, notActive, notReady, gameFull, clientUpdateRequired, serverUpdateRequired |
| 413 | Game/bot JSON or avatar too big | none or imageTooLarge |
| 415 | Avatar type not accepted | unsupportedImageType |
| 422 | A creation assertion disagrees with the authoritative game policy | none |
| 429 | Rate limited | rateLimited |
| 500 | Server fault (game-hook bug, storage) | none |
| 502 | Account deletion upstream failure (intact; retry) | none |
Mutations use operation-specific correctness
There is no generic mutation key or permanent command-receipt protocol. Actions carry the authoritative game version they were based on and stale actions are rejected. Join and leave express a desired membership state; start and cancel are idempotent lifecycle transitions; bot seating is guarded by the lobby version it was chosen from.
Creation is the one deliberately ambiguous case. If the connection fails before the response arrives, the client reloads its game list and the player may create again. The platform does not retain permanent receipts for this rare early-stage case.
Two reject codes are not errors and never reach the client as failures:
abstain (a system timeout that lost its race, a clean no-op) and the
accepted-lobby-staleness codes, which a client resolves by resyncing. Kernel
rejections are values, not exceptions.