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
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}