World of Mythos API
REST + Socket.IO reference for World of Mythos clients
Two transports. Accounts, lobbies, inventory, the market and the shops are plain HTTP (JSON bodies, JSON responses). Gameplay inside a lobby runs over a Socket.IO connection: join with join_room, send moves, and receive state_update whenever anything changes. There is no polling endpoint for game state.
Credentials. An account session (session_token from log-in, sent as token) identifies the account on account-scoped routes. A join token (from joining a lobby) identifies a player inside one lobby and is presented once with join_room. The server never trusts a name or admin flag sent by the client.
Errors. Error bodies are {"error": str}, often with a machine-readable code; branch on code, not on the message text. Any route may also answer 429 (too many requests; slow down and retry later) or 500.
Accounts & log-in
A name becomes an account when it is claimed with an email (web) or created with an Apple or Steam account (native apps). Logging in returns an account session (session_token) that the account-scoped routes below take as token. It lasts 30 days from last use.
Emails a verification link. The email is attached only once the link is opened (see /confirm_email_verification). Calling again resends the link.
Request body
| Field | Type | Required | Notes |
name | string | Yes | 1–12 characters; no spaces or / \ @ " ' - |
email | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "pending_verification": bool} | false: already claimed with this verified email |
| 400 | {"error": str} | Invalid name or email |
| 409 | {"error": "Name already claimed."} | Claimed by a different email |
| 502 | {"error": "Could not send verification email."} | Email could not be sent; try again |
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"claimed": bool} | |
| 400 | {"error": "Name is required."} | |
If the account has always verify email on, a one-time code is emailed instead and the client continues with /verify_code.
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
email | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "always_verify_email": bool, "session_token": str} | Logged in |
| 200 | {"success": false, "requires_code": true} | A code was emailed |
| 403 / 404 | {"error": str} | The name and email don't match an account |
| 502 | {"error": "Could not send verification email."} | Email could not be sent; try again |
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
code | string | Yes | From the email |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "always_verify_email": bool, "session_token": str} | Logged in |
| 400 | {"error": str} | Missing fields |
| 403 | {"error": "Wrong code."} | |
| 404 | {"error": "No verification pending."} | |
| 410 | {"error": "Code expired."} | |
| 429 | {"error": "Too many attempts."} | The code is discarded; log in again |
Also extends the session.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session |
Responses
| Status | Body | Meaning |
| 200 | {"name": str, "email": str | null, "always_verify_email": bool, "email_verified": bool} | |
| 401 | {"error": "Invalid or expired session."} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session |
Responses
| Status | Body | Meaning |
| 200 | {"success": true} | Always |
Confirms a claim (name, relic or wheel), or adds web log-in to a store account. Grants any held reward and logs this browser in.
Request body
| Field | Type | Required | Notes |
token | string | Yes | From the emailed link |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "purpose": str, "relic_name": str | null, "session_token": str} | |
| 400 | {"error": "Token is required."} | |
| 404 | {"error": "Invalid or expired link."} | |
| 409 | {"error": str} | Another email claimed the name first |
For when the link is opened on another device. Safe to poll; never sends email.
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
email | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"verified": true, "session_token": str} | |
| 200 | {"verified": false} | Not (yet) verified |
| 400 | {"error": str} | Missing fields |
Always answers the same way, whether or not the email is registered.
Request body
| Field | Type | Required | Notes |
email | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"success": true} | Always, for a well-formed email |
| 400 | {"error": str} | Invalid email |
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
email | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"always_verify_email": bool} | |
| 400 | {"error": str} | Missing fields |
| 403 / 404 | {"error": str} | The name and email don't match an account |
Emails a confirmation link; the change applies when it is opened.
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
email | string | Yes | |
always_verify_email | bool | Yes | The new value |
Responses
| Status | Body | Meaning |
| 200 | {"success": true} | |
| 400 | {"error": str} | Missing fields |
| 403 / 404 | {"error": str} | The name and email don't match an account |
| 502 | {"error": "Could not send verification email."} | Email could not be sent; try again |
Request body
| Field | Type | Required | Notes |
token | string | Yes | From the emailed link |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "always_verify_email": bool} | |
| 400 | {"error": "Token is required."} | |
| 404 | {"error": "Invalid or expired link."} | |
Apple & Steam sign-in
The iOS app and the Steam build sign in with the store account, with no sign-in sheet. Apple routes take app_transaction (StoreKit's signed AppTransaction). Steam routes take ticket (a Web API auth ticket for identity wom-backend) and an optional steam_name. The Apple and Steam versions of each route behave the same. A missing or unverifiable credential answers 400/401 with code invalid_app_transaction or invalid_ticket. Steam routes answer 503 steam_disabled while Steam is off.
Request body
| Field | Type | Required | Notes |
app_transaction / ticket | string | Yes | Store credential (see above) |
Responses
| Status | Body | Meaning |
| 200 | {"status": "ok", "name": str, "session_token": str} | Signed in |
| 200 | {"status": "new"} | No account yet: offer Play now (create) or I already have an account (log in, then link) |
Can also take over a name that was only ever played as a guest.
Request body
| Field | Type | Required | Notes |
app_transaction / ticket | string | Yes | Store credential (see above) |
name | string | Yes | 1–12 characters; no spaces or / \ @ " ' - |
Responses
| Status | Body | Meaning |
| 200 | {"status": "ok", "name": str, "session_token": str} | |
| 400 | {"error": str, "code": "invalid_name"} | |
| 409 | {"error": str, "code": "already_linked" | "name_taken"} | |
Also used as a check-in on every launch with a stored session. A store account can move away from an account it created only if that account has no progress yet.
Request body
| Field | Type | Required | Notes |
app_transaction / ticket | string | Yes | Store credential (see above) |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"status": "ok", "name": str} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 409 | {"error": str, "code": "linked_to_other_apple" | "linked_to_other_steam" | "apple_account_in_use" | "steam_account_in_use"} | |
Must come from the store account itself, and the account needs another way to log in. Ends the calling session.
Request body
| Field | Type | Required | Notes |
app_transaction / ticket | string | Yes | Store credential (see above) |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"status": "ok"} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 409 | {"error": str, "code": "not_linked" | "no_other_login"} | |
Account settings
Every route here takes the account session as token and answers 401 invalid_session without one.
Immediate and permanent. Personal data, items and sessions are deleted, open market listings are cancelled, and the name is freed.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
confirm_name | string | Yes | Must equal the account's name |
Responses
| Status | Body | Meaning |
| 200 | {"status": "deleted"} | |
| 400 | {"error": str, "code": "name_mismatch"} | |
| 409 | {"error": str, "code": "in_game"} | Finish or leave the current game first |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"age_affirmed": true} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"steam": {} | null, "apple": {} | null, "web": {"email": str} | null} | null = not connected |
Today only HD textures, which come with a store copy of the game or any purchase.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"hd": bool} | |
Emails a link; opening it attaches the email and logs that browser in.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
email | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"status": "sent"} | |
| 400 | {"error": str, "code": "invalid_email"} | |
| 409 | {"error": str, "code": "already_connected"} | |
| 502 | {"error": "Could not send verification email."} | Email could not be sent; try again |
Merging accounts
Merges an Apple/Steam account (no email) into a web account, started from the store account. 1) start with the web account's username and email; a code is emailed. 2) verify the code to see both accounts side by side. 3) commit, choosing which username to keep. A verified code is good for 30 minutes.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
name | string | Yes | Web account's username |
email | string | Yes | Web account's email |
Responses
| Status | Body | Meaning |
| 200 | {"status": "sent"} | |
| 400 | {"error": str, "code": "invalid_name" | "invalid_email"} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 / 404 | {"error": str, "code": str} | The username and email don't match an account |
| 409 | {"error": str, "code": "not_a_store_account" | "linked_to_other_apple" | "linked_to_other_steam"} | |
| 502 | {"error": "Could not send verification email."} | Email could not be sent; try again |
Each Summary lists the account's items (skins, wheels, relics, credits, artifact), stats, ranks and open listings.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
code | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"store": Summary, "web": {"player_id": int, "name": str, "summary": Summary}} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": "Wrong code.", "code": "wrong_code"} | |
| 404 | {"error": str, "code": "no_verification"} | |
| 409 | {"error": str, "code": "account_gone" | "linked_to_other_apple" | "linked_to_other_steam"} | |
| 410 | {"error": str, "code": "code_expired"} | |
| 429 | {"error": str, "code": "too_many_attempts"} | |
All or nothing. The kept account gets both accounts' items and log-ins; a receipt is emailed.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
keep | "store" | "web" | Yes | Which username to keep |
artifact_from | "kept" | "left" | If both hold an artifact | Which artifact to keep |
Responses
| Status | Body | Meaning |
| 200 | {"status": "ok", "name": str, "session_token": str} | Now logged in as the kept account |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str, "code": "verification_required"} | |
| 409 | {"error": str, "code": str} | e.g. in_game, artifact_choice_required, account_gone |
Lobbies
Joining a lobby returns a per-lobby join token, handed back once. Present it with the join_room socket event; after that the connection acts as that player. Lobby routes also accept account_token (the account session) to prove a claimed name; a name owned by an Apple/Steam account without email requires it.
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
email | string | If the name is claimed | |
account_token | string | No | Account session that owns name |
Responses
| Status | Body | Meaning |
| 200 | {"lobby_id": str, "token": str} | |
| 400 | {"error": str} | Invalid name |
| 403 | {"error": str} | Name is claimed and not proven |
Joining after the game started makes the player a spectator.
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
email | string | If the name is claimed | |
account_token | string | No | Account session that owns name |
Responses
| Status | Body | Meaning |
| 200 | {"status": "joined", "token": str} | |
| 400 | {"error": str} | Invalid name, or taken in this lobby |
| 403 | {"error": str} | Name is claimed and not proven |
| 404 | {"error": "Lobby not found"} | |
Responses
| Status | Body | Meaning |
| 200 | {"round": int, "gameover": bool, "players": [str]} | |
| 404 | {"error": "Lobby not found"} | |
Responses
| Status | Body | Meaning |
| 200 | {"history": [str]} | |
Only for the player the join token belongs to.
Responses
| Status | Body | Meaning |
| 200 | {"player": str, "messages": [str], "events": [object]} | |
| 403 | {"error": str} | Token missing or not this player's |
| 404 | {"error": str} | Lobby or player not found |
Only for the player the join token belongs to.
Responses
| Status | Body | Meaning |
| 200 | {"player": str, "player_history": [object]} | |
| 403 | {"error": str} | |
| 404 | {"error": str} | |
Boss fight
A shared lobby against Hades on a fixed schedule. Survivors earn relics.
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
account_token | string | No | Account session that owns name |
Responses
| Status | Body | Meaning |
| 200 | {"lobby_id": str, "start_time": ISO8601, "token"?: str} | token only on a new join |
| 403 | {"error": str} | Play a game or create an account first |
Responses
| Status | Body | Meaning |
| 200 | {"start_time": ISO8601} | |
Responses
| Status | Body | Meaning |
| 200 | {"lobby_id": str | null, "round": int, "start_time": ISO8601 | null, "players": [{"name": str, "skin": str | null, "alive": bool, "spectator": bool, "bot": bool}]} | |
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"relics": [{"id", "name", "power_category", "flavour_text", "count", "created_at", "newest_copy_created_at", "copies"?}]} | |
| 404 | {"error": str} | Player not found |
Granted at once to a verified account with this email; otherwise a verification link is emailed and the relic is held.
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
email | string | Yes | |
lobby_id | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "pending_verification": bool, "relic_name"?: str} | |
| 400 | {"error": str} | Missing fields or invalid email |
| 404 | {"error": str} | Lobby or pending reward not found |
| 409 | {"error": str} | Name claimed by a different email |
| 502 | {"error": "Could not send verification email."} | Email could not be sent; try again |
Inventory, wheels & trade-ups
Skins, wheels and the cosmetic. Account-scoped routes take the account session as token.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"name": str, "equipped_skin": str, "equipped_cosmetic": str | null, "skins": [{"skin": str, "count": int}], "wheels": [{"id": int, "kind": "normal" | "special"}], "artifact": Artifact | null, "ai_credits": int} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
skin | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "equipped_skin": str} | |
| 400 | {"error": str} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str} | Not owned |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
cosmetic | string | Yes | "" to remove |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "equipped_cosmetic": str | null} | |
| 400 | {"error": "Unknown cosmetic."} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str} | Not owned |
Same flow as relics: granted at once to a verified account, otherwise held behind an emailed link.
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
email | string | Yes | |
lobby_id | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "pending_verification": bool} | |
| 400 | {"error": str} | Missing fields or invalid email |
| 404 | {"error": str} | Lobby or pending reward not found |
| 409 | {"error": str} | Name claimed by a different email |
| 502 | {"error": "Could not send verification email."} | Email could not be sent; try again |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
wheel_id | int | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "result_skin": str} | |
| 400 | {"error": str} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": "Email not verified."} | |
| 404 | {"error": str} | Not found or already spun |
Responses
| Status | Body | Meaning |
| 200 | {"normal": [WheelOdds], "special": [WheelOdds]} | WheelOdds = {skin, weight, probability} |
Responses
| Status | Body | Meaning |
| 200 | {"rules": {<skin>: {"cost": int, "output_kind": "wheel" | "skin", "output": str}}} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
skin | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "trade_up_id": int, "output_kind": str, "output": str, "remaining": int, "wheel_id"?: int, "equipped_skin"?: str} | |
| 400 | {"error": str, "code": "skin_required" | "not_tradeable"} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str, "code": "email_unverified"} | |
| 409 | {"error": str, "code": "insufficient_copies", "have": int, "need": int} | |
Artifacts
Rare, account-bound finds from winning the Well. The ledger is readable only by players who hold an artifact.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
after | int | No | Ordinal to page after (default 0) |
limit | int | No | Default 100, max 200 |
Responses
| Status | Body | Meaning |
| 200 | {"artifacts": [{"ordinal": int, "finder_name": str, "discovered_at": str | null}], "total": int, "current_chance": float} | |
| 400 | {"error": str} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str} | Caller holds no artifact: show the ledger as sealed |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
artifact_id | int | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"name": str, "transcribed_to": [Transcribed]} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str} | Caller holds no artifact |
| 404 | {"error": "No such artifact."} | |
Same flow as relics and wheels.
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
email | string | Yes | |
lobby_id | string | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "pending_verification": bool} | |
| 400 | {"error": str} | Missing fields or invalid email |
| 404 | {"error": str} | Lobby or pending reward not found |
| 409 | {"error": str} | Name claimed by a different email |
| 502 | {"error": "Could not send verification email."} | Email could not be sent; try again |
| 409 | {"error": str} | Already claimed |
Market
Player-to-player swaps: a listing offers some items (give) for others (want) and is accepted whole. Posting and accepting need a verified email and the accepted market terms (email_unverified / terms_not_accepted). Item = {item_type: "skin"|"relic"|"wheel"|"ai_credits"|"artifact", skin, relic_id, wheel_kind, quantity}.
Responses
| Status | Body | Meaning |
| 200 | {"skins": [str], "relics": [{"id": int, "name": str}], "wheel_kinds": [str], "coin_relic_id": int, "terms_version": str, "terms_text": str} | |
Responses
| Status | Body | Meaning |
| 200 | {"listings": [Listing], "server_time": ISO8601} | Listing = {id, kind, status, seller_player_id, seller_name, created_at, expires_at, give: [Item], want: [Item]} |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"player_id": int, "player_name": str, "terms_accepted": bool, "terms_version": str, "coins": int, "email_verified": bool} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"terms_accepted": true, "terms_version": str} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
quick is free and lasts 60 s. long costs 1–4 Hades' Coins for 6 h each; coins aren't refunded.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
kind | "quick" | "long" | Yes | |
coins | int | For long | 1–4 |
give | [Item] | Yes | At least one |
want | [Item] | Yes | At least one |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "listing": Listing} | |
| 400 | {"error": str, "code": str} | Malformed listing |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str, "code": "email_unverified" | "terms_not_accepted"} | |
| 409 | {"error": str, "code": "give_not_owned" | "insufficient_coins"} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "listing": Listing} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str, "code": str} | As above |
| 404 | {"error": str, "code": "not_found"} | |
| 409 | {"error": str, "code": "own_listing" | "not_open" | "expired" | "seller_item_gone" | "your_item_gone"} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"success": true, "listing": Listing} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str, "code": "not_your_listing"} | |
| 404 | {"error": str, "code": "not_found"} | |
| 409 | {"error": str, "code": "not_open"} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
limit | int | No | |
before | int | No | Trade id to page before |
Responses
| Status | Body | Meaning |
| 200 | {"trades": [{"id", "listing_id", "kind", "role", "counterparty_name", "completed_at", "gave": [Item], "got": [Item]}], "has_more": bool, "next_before": int | null} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
Works without an account; with one, the report records the reporter.
Request body
| Field | Type | Required | Notes |
token | string | No | Account session |
reported_name | string | Yes | |
message | string | Yes | |
context | "lobby" | "market" | Yes | |
complaint | string | Yes | Up to 1000 characters |
Responses
| Status | Body | Meaning |
| 200 | {"status": "reported"} | |
| 400 | {"error": str, "code": str} | e.g. complaint_required, self_report |
Ranked
Matchmaking with visible tiers and seasons that turn at solstices and equinoxes. /ranked/queue/join returns a secret ticket; later ranked calls for that name need the ticket or the name's account session (token). A claimed name needs its account session to queue. Clients below protocol 3 get 426 update_required. Matches arrive over the ranked_match_found socket event.
Rejoining with the current ticket keeps your place.
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
ticket | string | One of these | From queue/join |
token | string | One of these | Account session |
Responses
| Status | Body | Meaning |
| 200 | {"status": "queued", "ticket": str} | |
| 400 | {"error": str} | Invalid name |
| 401 | {"error": str, "code": "login_required"} | Claimed name without its session |
| 409 | {"error": str, "code": "name_in_ranked"} | |
| 426 | {"error": str, "code": "update_required"} | |
Send every 30 s while the queue is showing; a queue left unrenewed for 2 minutes is dropped.
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
ticket | string | One of these | From queue/join |
token | string | One of these | Account session |
Responses
| Status | Body | Meaning |
| 200 | {"status": "renewed" | "not_queued"} | |
| 403 | {"error": str, "code": "not_authorized"} | Neither ticket nor session |
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
ticket | string | One of these | From queue/join |
token | string | One of these | Account session |
Responses
| Status | Body | Meaning |
| 200 | {"status": "left", "was_queued": bool} | |
| 403 | {"error": str, "code": "not_authorized"} | Neither ticket nor session |
Mints a fresh join token unless peek is set.
Request body
| Field | Type | Required | Notes |
name | string | Yes | |
ticket | string | One of these | From queue/join |
token | string | One of these | Account session |
peek | bool | No | Check without minting a token |
Responses
| Status | Body | Meaning |
| 200 | {"lobby_id": str | null, "token": str | null, "ranked_countdown_deadline": ISO8601 | null, "started": bool, "players": [str], "player_count": int, "max_players": int} | |
| 403 | {"error": str, "code": "not_authorized"} | Neither ticket nor session |
Responses
| Status | Body | Meaning |
| 200 | {"tier": str | null, "ranked_games_played": int} | null during placements |
Newest first; per_page defaults to 10, max 50.
Responses
| Status | Body | Meaning |
| 200 | {"games": [{"game_id": str, "placement": int, "rank": str | null, "opponents": [{"name": str, "place": int}], "at": ISO8601}], "total": int, "page": int, "per_page": int} | |
Responses
| Status | Body | Meaning |
| 200 | {"ranks": [{"tier": str, "season": str, "at": ISO8601}], "ai_ranks": [...]} | |
Responses
| Status | Body | Meaning |
| 200 | {"name": str, "ends_at": ISO8601} | |
Responses
| Status | Body | Meaning |
| 200 | {"human": [{"season": str, "tier": str, "current": bool}], "ai": [...]} | |
Responses
| Status | Body | Meaning |
| 200 | {"created_at": ISO8601 | null, "played_games": int, "wins": int, "kills": int} | |
Responses
| Status | Body | Meaning |
| 200 | {"well_wins": int, "rewards": [{"reward": str, "count": int, "first_awarded_at": ISO8601, "expected_share": float}]} | |
My AI & bot ranked
A personal AI trained on how you play, with its own ladder. Every route takes the account session as token (401 without it).
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"enabled": bool, "minute_counter": int, "knobs": {...}, "override_rules": [...], "credits": int, "trainable": bool, "logged_rows": int, "min_rows": int, "bot_rank": {"tier": str | null, "games_played": int}, "queue": {...}} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
enabled | bool | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"enabled": bool, "queued": bool, "reason": str} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
minute_counter | int | No | |
knobs | object | No | greed, revenge, grudge |
override_rules | array | No | |
Responses
| Status | Body | Meaning |
| 200 | {"saved": true, "enabled": bool, "minute_counter": int, "knobs": {...}, "override_rules": [...]} | |
| 400 | {"error": str} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"trained": bool, "games_rows": int, "min_rows"?: int, "deviations": [...]} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"matches": [{"match_id", "placement", "rank", "opponents", "at"}]} | |
Free. Matches arrive over ai_ranked_match_found.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"queued": true, "queue_size": int, "waited_seconds"?: float} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
Responses
| Status | Body | Meaning |
| 200 | {"left": true, "was_queued": bool} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
peek | bool | No | Check without minting a token |
Responses
| Status | Body | Meaning |
| 200 | {"lobby_id": str | null, "token": str | null, "ai_ranked_countdown_deadline": ISO8601 | null, "started": bool} | |
Merchants & timewarp
Sky events summon merchants who sell relics for Hades' Coins, once per player per event: John Dee sells Paper at a full moon and Pen at a new moon, Hildegard von Bingen sells the Stone of Vitality at a planetary conjunction. Sacrificing one of those relics rewinds the sky for everyone. SkyEvent = {kind: "full_moon"|"new_moon"|"conjunction", key, bodies, sign, at}.
Works without a token; with one, already_bought_this_period is filled in.
Request body
| Field | Type | Required | Notes |
token | string | No | Account session |
Responses
| Status | Body | Meaning |
| 200 | {"offers": [Offer], "sky_date": ISO8601 | null, "reverted": bool, "revert_expires_at": ..., "revert_locked_until": ..., "revert_to_date": ...} | sky_date is set during a timewarp |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
offer_id | int | No | |
event_key | string | No | |
Responses
| Status | Body | Meaning |
| 200 | {"ok": true, "item_name": str} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 409 | {"error": str, "code": "not_available" | "already_bought" | "insufficient_coins"} | |
Rewinds the sky to when that copy was bought, for an hour. Broadcasts timewarp to everyone.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
relic | "Stone of Vitality" | "Paper" | "Pen" | No | Default: Stone of Vitality |
copy_id | int | No | Default: newest copy |
Responses
| Status | Body | Meaning |
| 200 | {"ok": true, "expires_at": ISO8601, "revert_to_date": ISO8601, "events": [SkyEvent]} | |
| 400 | {"error": str, "code": "not_revertible" | "bad_copy"} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 404 / 409 | {"error": str, "code": "no_relic" | "already_reverted"} | |
Responses
| Status | Body | Meaning |
| 200 | {"at": ISO8601, "events": [SkyEvent]} | |
| 400 | {"error": str, "code": "bad_at"} | |
Shop
Products: Special Wheel (wheel_special), bot-game credits (ai_credits) and the Cherub skin (skin_cherub). The web pays through Stripe, the iOS app through StoreKit and the Steam build through the Steam Wallet. A closed shop answers 503 shop_disabled. The Special Wheel isn't sold where paid loot boxes are prohibited.
Responses
| Status | Body | Meaning |
| 200 | {"shop_enabled": bool, "terms_version": str, "products": [{"id", "name", "price_cents", "currency", "kind", ...}]} | |
Items are granted once payment is confirmed.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
product | string | Yes | |
quantity | int | No | Wheels and credit packs |
confirm_duplicate | bool | No | Buy a skin you already own |
Responses
| Status | Body | Meaning |
| 200 | {"checkout_url": str, "order_id": int} | |
| 400 | {"error": str} | Unknown product or quantity |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str, "code": "email_unverified" | "region_blocked" | "payments_blocked"} | |
| 409 | {"error": str, "code": "already_owned"} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
order_id | int | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"status": str, "product": str, "fulfilled": bool} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 404 | {"error": "No such order."} | |
Attach app_account_token to the StoreKit purchase.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
storefront | string | No | StoreKit storefront (alpha-3) |
Responses
| Status | Body | Meaning |
| 200 | {"app_account_token": str, "products": [{"product": str, "apple_product_id": str, "kind": str, "max_quantity": int}]} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str, "code": "email_unverified" | "payments_blocked"} | |
Idempotent. Finish the StoreKit transaction only after a final answer.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
signed_transaction | string | Yes | StoreKit signed transaction |
Responses
| Status | Body | Meaning |
| 200 | {"status": "fulfilled", "order_id": int, "product": str} | |
| 400 | {"error": str, "code": "invalid_transaction" | "unknown_product"} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str, "code": "account_mismatch"} | Bought for another account; leave it unfinished |
| 409 | {"error": str, "code": "revoked" | "already_claimed"} | |
Steam shows its approval dialog in the overlay. With web: true (no overlay) the reply's steam_url is the page to approve on.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
product | string | Yes | |
quantity | int | No | Wheels and credit packs |
confirm_duplicate | bool | No | Buy a skin you already own |
language | string | No | |
web | bool | No | |
Responses
| Status | Body | Meaning |
| 200 | {"order_id": int, "steam_url"?: str} | |
| 400 | {"error": str} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 403 | {"error": str, "code": "steam_not_linked" | "payments_blocked" | "region_blocked"} | |
| 409 | {"error": str, "code": "already_owned"} | |
| 502 | {"error": str, "code": "steam_unavailable"} | |
Send authorized from Steam's authorization response, or check: true to ask whether a web purchase was approved.
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
order_id | int | Yes | |
authorized | bool | One of these | |
check | bool | One of these | |
Responses
| Status | Body | Meaning |
| 200 | {"status": "fulfilled" | "cancelled" | "pending", "product": str} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 404 | {"error": "Order not found."} | |
| 502 | {"error": str, "code": "steam_unavailable"} | Approved; it will arrive shortly |
Version
Send your version as the X-Protocol-Version header (REST) or auth.protocol_version (Socket.IO). Too old: 426 / connect_error with code update_required.
Responses
| Status | Body | Meaning |
| 200 | {"protocol_version": int, "min_supported_protocol": int, "min_client": {"ios": str, "android": str, "steam": str}} | |
Socket.IO — client → server
Failures are answered with an error event {message} to the caller only.
Payload
{"lobby_id": str, "name": str, "email": str}
Answer
joined_lobby {lobby_id, token}
Required before any in-lobby action.
Payload
{"lobby_id": str, "token": str}
Answer
joined, then state_update
Payload
{"lobby_id": str}
Answer
left
Spends the relics players selected.
Payload
{"lobby_id": str}
Answer
state_update to the room
Payload
{"lobby_id": str, "target": str}
Answer
state_update
Before round 1 only.
Payload
{"lobby_id": str, "relic_id": int}
Answer
state_update
Payload
{"lobby_id": str}
Answer
state_update
action: attack | defend | well; resource: gain_hp | gain_coin | gain_attack; target required for attack.
Payload
{"lobby_id": str, "action": str | null, "resource": str | null, "target": str | null}
Answer
state_update
Only the player holding pending_deny.
Payload
{"lobby_id": str, "target": str}
Answer
state_update
Up to 200 characters.
Payload
{"lobby_id": str, "message": str}
Answer
chat_message to the room
No token needed to watch.
Payload
{"token": str | null}
Answer
market_chat_backlog
Up to 200 characters.
Payload
{"token": str, "message": str}
Answer
market_chat_message to the market
Call after /ranked/queue/join.
Payload
{"name": str, "ticket"?: str, "token"?: str}
Answer
joined_ranked_queue
Call after /my_ai/bot_ranked.
Payload
{"token": str}
Answer
joined_ai_ranked_queue
Answer
bossfight_roster
No payload.
Answer
city_presence
No payload.
Socket.IO — server → client
Sent after every change. Other players' moves for the current round are never included until revealed.
Shape
{"round": int, "players": [Player], "winner": str | null, "wellwinner": str | null, "pending_deny": str | null, "deny_target": str | null, "readyPlayers": [str], "history": [str], "round_end_time": ISO8601 | null, "boss_fight": bool, "start_time": ISO8601 | null, "gameover": bool, "chat": [{"sender", "message", "timestamp"}]}
Shape
{"name", "hp", "coins", "attackDamage", "alive", "admin", "spectator", "bot", "boss", "lost_soul", "title", "idle_rounds", "skin", "cosmetic", "selected_relic_ids", "bot_type", "pending_relic_nudge", "wheel_awarded", "pending_wheel_nudge", "artifact_awarded", "pending_artifact_nudge"}
Shape
{"sender": str, "message": str, "timestamp": ISO8601}
Shape
{"messages": [...]} / {"sender", "message", "timestamp"}
Shape
Listing / Listing / {"id": int}
Shape
{"count": int, "names": [str]}
Shape
{"lobby_id": str, "token": str}
Shape
Same as GET /get_bossfight_roster
Shape
{"bossfight": int, "ranked": int, "bot_ranked": int, "market": int}
Shape
{"revert_to_date", "events": [SkyEvent]} / {"revert_to_date", "ended_at", "events"}