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.
POST /claim_name Claim a name with an email

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

FieldTypeRequiredNotes
namestringYes1–12 characters; no spaces or / \ @ " ' -
emailstringYes

Responses

StatusBodyMeaning
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
POST /check_name Is a name claimed?

Request body

FieldTypeRequiredNotes
namestringYes

Responses

StatusBodyMeaning
200{"claimed": bool}
400{"error": "Name is required."}
POST /log_in Log in with name + email

If the account has always verify email on, a one-time code is emailed instead and the client continues with /verify_code.

Request body

FieldTypeRequiredNotes
namestringYes
emailstringYes

Responses

StatusBodyMeaning
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
POST /verify_code Finish a log-in with the emailed code

Request body

FieldTypeRequiredNotes
namestringYes
codestringYesFrom the email

Responses

StatusBodyMeaning
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
POST /resolve_account_session Who does this session belong to?

Also extends the session.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session

Responses

StatusBodyMeaning
200{"name": str, "email": str | null, "always_verify_email": bool, "email_verified": bool}
401{"error": "Invalid or expired session."}
POST /log_out End an account session

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session

Responses

StatusBodyMeaning
200{"success": true}Always
POST /confirm_email_verification Open an emailed verification link

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

FieldTypeRequiredNotes
tokenstringYesFrom the emailed link

Responses

StatusBodyMeaning
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
POST /check_claim_verified Poll whether a claim was verified

For when the link is opened on another device. Safe to poll; never sends email.

Request body

FieldTypeRequiredNotes
namestringYes
emailstringYes

Responses

StatusBodyMeaning
200{"verified": true, "session_token": str}
200{"verified": false}Not (yet) verified
400{"error": str}Missing fields
POST /forgot_username Email the usernames registered to an email

Always answers the same way, whether or not the email is registered.

Request body

FieldTypeRequiredNotes
emailstringYes

Responses

StatusBodyMeaning
200{"success": true}Always, for a well-formed email
400{"error": str}Invalid email
POST /get_always_verify_email_flag Read the always-verify-email setting

Request body

FieldTypeRequiredNotes
namestringYes
emailstringYes

Responses

StatusBodyMeaning
200{"always_verify_email": bool}
400{"error": str}Missing fields
403 / 404{"error": str}The name and email don't match an account
POST /request_toggle_verify_email Ask to change the always-verify-email setting

Emails a confirmation link; the change applies when it is opened.

Request body

FieldTypeRequiredNotes
namestringYes
emailstringYes
always_verify_emailboolYesThe new value

Responses

StatusBodyMeaning
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
POST /confirm_toggle_verify_email Open the setting-change link

Request body

FieldTypeRequiredNotes
tokenstringYesFrom the emailed link

Responses

StatusBodyMeaning
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.
POST /auth/apple · /auth/steam Sign in

Request body

FieldTypeRequiredNotes
app_transaction / ticketstringYesStore credential (see above)

Responses

StatusBodyMeaning
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)
POST /auth/apple/create · /auth/steam/create Create an account for this store account

Can also take over a name that was only ever played as a guest.

Request body

FieldTypeRequiredNotes
app_transaction / ticketstringYesStore credential (see above)
namestringYes1–12 characters; no spaces or / \ @ " ' -

Responses

StatusBodyMeaning
200{"status": "ok", "name": str, "session_token": str}
400{"error": str, "code": "invalid_name"}
409{"error": str, "code": "already_linked" | "name_taken"}
Account settings
Every route here takes the account session as token and answers 401 invalid_session without one.
POST /account/delete Delete the account

Immediate and permanent. Personal data, items and sessions are deleted, open market listings are cancelled, and the name is freed.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
confirm_namestringYesMust equal the account's name

Responses

StatusBodyMeaning
200{"status": "deleted"}
400{"error": str, "code": "name_mismatch"}
409{"error": str, "code": "in_game"}Finish or leave the current game first
POST /account/age Confirm the player is 18+ (or has guardian consent)

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
200{"age_affirmed": true}
POST /account/connections The ways into this account

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
200{"steam": {} | null, "apple": {} | null, "web": {"email": str} | null}null = not connected
POST /account/entitlements What the account has paid for

Today only HD textures, which come with a store copy of the game or any purchase.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
200{"hd": bool}
POST /account/connect_web Add web log-in to a store account

Emails a link; opening it attaches the email and logs that browser in.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
emailstringYes

Responses

StatusBodyMeaning
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.
POST /account/merge/start Send the merge code

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
namestringYesWeb account's username
emailstringYesWeb account's email

Responses

StatusBodyMeaning
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
POST /account/merge/verify Check the code and compare the accounts

Each Summary lists the account's items (skins, wheels, relics, credits, artifact), stats, ranks and open listings.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
codestringYes

Responses

StatusBodyMeaning
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"}
POST /account/merge/commit Merge

All or nothing. The kept account gets both accounts' items and log-ins; a receipt is emailed.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
keep"store" | "web"YesWhich username to keep
artifact_from"kept" | "left"If both hold an artifactWhich artifact to keep

Responses

StatusBodyMeaning
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.
POST /create_lobby Create a private lobby (caller is admin)

Request body

FieldTypeRequiredNotes
namestringYes
emailstringIf the name is claimed
account_tokenstringNoAccount session that owns name

Responses

StatusBodyMeaning
200{"lobby_id": str, "token": str}
400{"error": str}Invalid name
403{"error": str}Name is claimed and not proven
POST /join_lobby/<lobby_id> Join a lobby by code

Joining after the game started makes the player a spectator.

Request body

FieldTypeRequiredNotes
namestringYes
emailstringIf the name is claimed
account_tokenstringNoAccount session that owns name

Responses

StatusBodyMeaning
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"}
GET /lobby_status/<lobby_id> Has the lobby started or ended?

Responses

StatusBodyMeaning
200{"round": int, "gameover": bool, "players": [str]}
404{"error": "Lobby not found"}
GET /get_history/<lobby_id> The lobby's public history log

Responses

StatusBodyMeaning
200{"history": [str]}
GET /get_player_messages/<lobby_id>/<name>?token=… A player's private messages

Only for the player the join token belongs to.

Responses

StatusBodyMeaning
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
GET /get_player_history/<lobby_id>/<name>?token=… A player's private round history

Only for the player the join token belongs to.

Responses

StatusBodyMeaning
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.
POST /get_bossfight_lobby Join the boss fight

Request body

FieldTypeRequiredNotes
namestringYes
account_tokenstringNoAccount session that owns name

Responses

StatusBodyMeaning
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
GET /get_next_bossfight_time When the next fight starts

Responses

StatusBodyMeaning
200{"start_time": ISO8601}
GET /get_bossfight_roster Who is in the fight (without joining)

Responses

StatusBodyMeaning
200{"lobby_id": str | null, "round": int, "start_time": ISO8601 | null, "players": [{"name": str, "skin": str | null, "alive": bool, "spectator": bool, "bot": bool}]}
POST /get_player_relics A player's relics

Request body

FieldTypeRequiredNotes
namestringYes

Responses

StatusBodyMeaning
200{"relics": [{"id", "name", "power_category", "flavour_text", "count", "created_at", "newest_copy_created_at", "copies"?}]}
404{"error": str}Player not found
POST /claim_pending_relic Claim a relic won in the fight

Granted at once to a verified account with this email; otherwise a verification link is emailed and the relic is held.

Request body

FieldTypeRequiredNotes
namestringYes
emailstringYes
lobby_idstringYes

Responses

StatusBodyMeaning
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.
POST /inventory Everything the account owns

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
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
POST /inventory/equip Equip a skin

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
skinstringYes

Responses

StatusBodyMeaning
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
POST /inventory/equip_cosmetic Equip or remove the cosmetic

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
cosmeticstringYes"" to remove

Responses

StatusBodyMeaning
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
POST /claim_pending_wheel Claim a wheel dropped after a match

Same flow as relics: granted at once to a verified account, otherwise held behind an emailed link.

Request body

FieldTypeRequiredNotes
namestringYes
emailstringYes
lobby_idstringYes

Responses

StatusBodyMeaning
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
POST /wheel/spin Spin a wheel

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
wheel_idintYes

Responses

StatusBodyMeaning
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
GET /wheel/tables The odds of every wheel

Responses

StatusBodyMeaning
200{"normal": [WheelOdds], "special": [WheelOdds]}WheelOdds = {skin, weight, probability}
GET /tradeup/rules What duplicate skins trade up into

Responses

StatusBodyMeaning
200{"rules": {<skin>: {"cost": int, "output_kind": "wheel" | "skin", "output": str}}}
POST /inventory/trade_up Trade duplicate skins up

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
skinstringYes

Responses

StatusBodyMeaning
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.
POST /artifacts/ledger Every artifact ever discovered

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
afterintNoOrdinal to page after (default 0)
limitintNoDefault 100, max 200

Responses

StatusBodyMeaning
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
POST /artifacts/transcribed_to Who an artifact was transcribed to

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
artifact_idintYes

Responses

StatusBodyMeaning
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."}
POST /claim_pending_artifact Claim an artifact found in a match

Same flow as relics and wheels.

Request body

FieldTypeRequiredNotes
namestringYes
emailstringYes
lobby_idstringYes

Responses

StatusBodyMeaning
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}.
GET /market/catalog Tradeable items and the market terms

Responses

StatusBodyMeaning
200{"skins": [str], "relics": [{"id": int, "name": str}], "wheel_kinds": [str], "coin_relic_id": int, "terms_version": str, "terms_text": str}
GET /market/listings The open listings

Responses

StatusBodyMeaning
200{"listings": [Listing], "server_time": ISO8601}Listing = {id, kind, status, seller_player_id, seller_name, created_at, expires_at, give: [Item], want: [Item]}
POST /market/enter The caller's market status

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
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
POST /market/accept_terms Accept the market terms

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
200{"terms_accepted": true, "terms_version": str}
401{"error": str, "code": "invalid_session"}Missing or expired account session
POST /market/listings Post a listing

quick is free and lasts 60 s. long costs 1–4 Hades' Coins for 6 h each; coins aren't refunded.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
kind"quick" | "long"Yes
coinsintFor long1–4
give[Item]YesAt least one
want[Item]YesAt least one

Responses

StatusBodyMeaning
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"}
POST /market/listings/<id>/accept Accept a listing (atomic swap)

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
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"}
POST /market/listings/<id>/cancel Cancel your own listing

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
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"}
POST /market/trades Your completed trades

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
limitintNo
beforeintNoTrade id to page before

Responses

StatusBodyMeaning
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
POST /chat/report Report a chat message

Works without an account; with one, the report records the reporter.

Request body

FieldTypeRequiredNotes
tokenstringNoAccount session
reported_namestringYes
messagestringYes
context"lobby" | "market"Yes
complaintstringYesUp to 1000 characters

Responses

StatusBodyMeaning
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.
POST /ranked/queue/join Join the ranked queue

Rejoining with the current ticket keeps your place.

Request body

FieldTypeRequiredNotes
namestringYes
ticketstringOne of theseFrom queue/join
tokenstringOne of theseAccount session

Responses

StatusBodyMeaning
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"}
POST /ranked/queue/renew Keep your place while the app is open

Send every 30 s while the queue is showing; a queue left unrenewed for 2 minutes is dropped.

Request body

FieldTypeRequiredNotes
namestringYes
ticketstringOne of theseFrom queue/join
tokenstringOne of theseAccount session

Responses

StatusBodyMeaning
200{"status": "renewed" | "not_queued"}
403{"error": str, "code": "not_authorized"}Neither ticket nor session
POST /ranked/queue/leave Leave the queue

Request body

FieldTypeRequiredNotes
namestringYes
ticketstringOne of theseFrom queue/join
tokenstringOne of theseAccount session

Responses

StatusBodyMeaning
200{"status": "left", "was_queued": bool}
403{"error": str, "code": "not_authorized"}Neither ticket nor session
POST /ranked/active Is there an unfinished ranked match to return to?

Mints a fresh join token unless peek is set.

Request body

FieldTypeRequiredNotes
namestringYes
ticketstringOne of theseFrom queue/join
tokenstringOne of theseAccount session
peekboolNoCheck without minting a token

Responses

StatusBodyMeaning
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
GET /ranked/profile/<name> A player's shown tier

Responses

StatusBodyMeaning
200{"tier": str | null, "ranked_games_played": int}null during placements
GET /ranked/games/<name>?page=&per_page= A player's ranked games this season

Newest first; per_page defaults to 10, max 50.

Responses

StatusBodyMeaning
200{"games": [{"game_id": str, "placement": int, "rank": str | null, "opponents": [{"name": str, "place": int}], "at": ISO8601}], "total": int, "page": int, "per_page": int}
GET /ranked/ranks/<name> Every rank a player (and their AI) has reached

Responses

StatusBodyMeaning
200{"ranks": [{"tier": str, "season": str, "at": ISO8601}], "ai_ranks": [...]}
GET /ranked/season The current season

Responses

StatusBodyMeaning
200{"name": str, "ends_at": ISO8601}
GET /ranked/season_history/<name> A player's tier in every season

Responses

StatusBodyMeaning
200{"human": [{"season": str, "tier": str, "current": bool}], "ai": [...]}
GET /player/profile/<name> General stats

Responses

StatusBodyMeaning
200{"created_at": ISO8601 | null, "played_games": int, "wins": int, "kills": int}
GET /well/profile/<name> Well stats

Responses

StatusBodyMeaning
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).
POST /my_ai/status The AI's settings, credits, rank and queue state

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
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": {...}}
POST /my_ai/toggle Turn the AI on or off

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
enabledboolYes

Responses

StatusBodyMeaning
200{"enabled": bool, "queued": bool, "reason": str}
POST /my_ai/settings Change settings (only fields sent)

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
minute_counterintNo
knobsobjectNogreed, revenge, grudge
override_rulesarrayNo

Responses

StatusBodyMeaning
200{"saved": true, "enabled": bool, "minute_counter": int, "knobs": {...}, "override_rules": [...]}
400{"error": str}
POST /my_ai/personality How the AI differs from the average player

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
200{"trained": bool, "games_rows": int, "min_rows"?: int, "deviations": [...]}
POST /my_ai/matches The AI's games this season

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
200{"matches": [{"match_id", "placement", "rank", "opponents", "at"}]}
POST /my_ai/bot_ranked Queue yourself against bots

Free. Matches arrive over ai_ranked_match_found.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
200{"queued": true, "queue_size": int, "waited_seconds"?: float}
POST /my_ai/bot_ranked/leave Leave the bot-ranked queue

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)

Responses

StatusBodyMeaning
200{"left": true, "was_queued": bool}
POST /my_ai/bot_ranked/active Is there a bot-ranked match to return to?

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
peekboolNoCheck without minting a token

Responses

StatusBodyMeaning
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}.
POST /merchant/offer The merchants in town now

Works without a token; with one, already_bought_this_period is filled in.

Request body

FieldTypeRequiredNotes
tokenstringNoAccount session

Responses

StatusBodyMeaning
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
POST /merchant/purchase Buy from a merchant

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
offer_idintNo
event_keystringNo

Responses

StatusBodyMeaning
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"}
POST /merchant/revert_time Sacrifice a relic to timewarp

Rewinds the sky to when that copy was bought, for an hour. Broadcasts timewarp to everyone.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
relic"Stone of Vitality" | "Paper" | "Pen"NoDefault: Stone of Vitality
copy_idintNoDefault: newest copy

Responses

StatusBodyMeaning
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"}
GET /merchant/sky_events?at=<ISO8601> Merchant events live at an instant

Responses

StatusBodyMeaning
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.
GET /shop/products What's for sale on the web

Responses

StatusBodyMeaning
200{"shop_enabled": bool, "terms_version": str, "products": [{"id", "name", "price_cents", "currency", "kind", ...}]}
POST /shop/checkout Start a Stripe checkout

Items are granted once payment is confirmed.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
productstringYes
quantityintNoWheels and credit packs
confirm_duplicateboolNoBuy a skin you already own

Responses

StatusBodyMeaning
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"}
POST /shop/order Poll one of your orders

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
order_idintYes

Responses

StatusBodyMeaning
200{"status": str, "product": str, "fulfilled": bool}
401{"error": str, "code": "invalid_session"}Missing or expired account session
404{"error": "No such order."}
POST /shop/apple/prepare iOS: products and the purchase token

Attach app_account_token to the StoreKit purchase.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
storefrontstringNoStoreKit storefront (alpha-3)

Responses

StatusBodyMeaning
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"}
POST /shop/apple/verify iOS: hand over a purchase

Idempotent. Finish the StoreKit transaction only after a final answer.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
signed_transactionstringYesStoreKit signed transaction

Responses

StatusBodyMeaning
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"}
POST /shop/steam/init Steam: start a purchase

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

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
productstringYes
quantityintNoWheels and credit packs
confirm_duplicateboolNoBuy a skin you already own
languagestringNo
webboolNo

Responses

StatusBodyMeaning
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"}
POST /shop/steam/finalize Steam: complete a purchase

Send authorized from Steam's authorization response, or check: true to ask whether a web purchase was approved.

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
order_idintYes
authorizedboolOne of these
checkboolOne of these

Responses

StatusBodyMeaning
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}.
GET /servers Every live world server

Responses

StatusBodyMeaning
200{"servers": [Server], "this_server": int}
POST /servers/place Which server to connect to now

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

FieldTypeRequiredNotes
tokenstringNoAccount session

Responses

StatusBodyMeaning
200{"server": Server, "servers": [Server]}
POST /servers/choose Make a server your home

Request body

FieldTypeRequiredNotes
tokenstringYesAccount session (session_token from log-in)
server_idintYes

Responses

StatusBodyMeaning
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"}
GET /servers/lobby/<code> Which server a lobby code is on

For joining a lobby by code on another server.

Responses

StatusBodyMeaning
200{"server": Server | null}null: no live lobby has this code
Version
GET /version Protocol versions this server speaks

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

StatusBodyMeaning
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.
WS join_lobby Join a lobby over the socket

Payload

{"lobby_id": str, "name": str, "email": str}

Answer

joined_lobby {lobby_id, token}

WS join_room Bind this connection to your player

Required before any in-lobby action.

Payload

{"lobby_id": str, "token": str}

Answer

joined, then state_update

WS leave_room Leave the lobby room

Payload

{"lobby_id": str}

Answer

left

WS start_game Start the match (admin)

Spends the relics players selected.

Payload

{"lobby_id": str}

Answer

state_update to the room

WS kick_player Kick a player (admin, before round 2)

Payload

{"lobby_id": str, "target": str}

Answer

state_update

WS toggle_relic_selection Select a relic to spend at the start

Before round 1 only.

Payload

{"lobby_id": str, "relic_id": int}

Answer

state_update

WS add_dummy Add a bot (admin)

Payload

{"lobby_id": str}

Answer

state_update

WS submit_choice Submit this round's move

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

WS submit_deny_target Name who is denied this round

Only the player holding pending_deny.

Payload

{"lobby_id": str, "target": str}

Answer

state_update

WS send_message Lobby chat

Up to 200 characters.

Payload

{"lobby_id": str, "message": str}

Answer

chat_message to the room

WS join_market Watch the market

No token needed to watch.

Payload

{"token": str | null}

Answer

market_chat_backlog

WS leave_market Stop watching the market

No payload.

WS send_market_message Market chat

Up to 200 characters.

Payload

{"token": str, "message": str}

Answer

market_chat_message to the market

WS join_ranked_queue Receive your ranked match

Call after /ranked/queue/join.

Payload

{"name": str, "ticket"?: str, "token"?: str}

Answer

joined_ranked_queue

WS join_ai_ranked_queue Receive your bot-ranked match

Call after /my_ai/bot_ranked.

Payload

{"token": str}

Answer

joined_ai_ranked_queue

WS watch_bossfight / stop_watching_bossfight Watch the boss-fight roster

Answer

bossfight_roster

No payload.

WS watch_city_presence / stop_watching_city_presence Watch how busy the city is

Answer

city_presence

No payload.

Socket.IO — server → client
WS state_update The full lobby snapshot

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"}]}
WS Player (in state_update) A player as everyone in the lobby sees them

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"}
WS chat_message A new lobby chat message

Shape

{"sender": str, "message": str, "timestamp": ISO8601}
WS market_chat_backlog / market_chat_message Market chat history / a new message

Shape

{"messages": [...]} / {"sender", "message", "timestamp"}
WS listing_created / listing_updated / listing_expired Market board changes

Shape

Listing / Listing / {"id": int}
WS market_frogs Who is in the market

Shape

{"count": int, "names": [str]}
WS ranked_match_found / ai_ranked_match_found You were seated; join with join_room

Shape

{"lobby_id": str, "token": str}
WS bossfight_roster Boss-fight roster changed

Shape

Same as GET /get_bossfight_roster
WS city_presence Players in each activity

Shape

{"bossfight": int, "ranked": int, "bot_ranked": int, "market": int}
WS online_count Players online

Shape

{"count": int}
WS timewarp / timewarp_end A timewarp started / ended

Shape

{"revert_to_date", "events": [SkyEvent]} / {"revert_to_date", "ended_at", "events"}
WS error A request from this connection failed

Shape

{"message": str}