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 |
World servers
Players are spread over several world servers that share accounts, inventory and the market. Ask the directory which server to connect to, then use that server's url as the API host (null means the one you just called). A Server is {id, players, lobbies, state: "open"|"draining", url, cpu, full, busy, open_to_new}.
Responses
| Status | Body | Meaning |
| 200 | {"servers": [Server], "this_server": int} | |
Your home server if it has room and isn't busy, otherwise the roomiest open one. With a session, the answer becomes your home.
Request body
| Field | Type | Required | Notes |
token | string | No | Account session |
Responses
| Status | Body | Meaning |
| 200 | {"server": Server, "servers": [Server]} | |
Request body
| Field | Type | Required | Notes |
token | string | Yes | Account session (session_token from log-in) |
server_id | int | Yes | |
Responses
| Status | Body | Meaning |
| 200 | {"server": Server} | |
| 400 | {"error": str, "code": "bad_request"} | |
| 401 | {"error": str, "code": "invalid_session"} | Missing or expired account session |
| 404 | {"error": str, "code": "unknown_server"} | |
| 409 | {"error": str, "code": "server_full" | "server_closing"} | |
For joining a lobby by code on another server.
Responses
| Status | Body | Meaning |
| 200 | {"server": Server | null} | null: no live lobby has this code |
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"}