REST reference

Every REST endpoint with request params, response fields, and status codes. Matchmaking, bot games, presence and spectating are NOT here — they are socket messages (see WebSocket wire).

Index

bash
# Base path: /v1 — all server-to-server, IP whitelist + X-Service-Auth header.
# Every response uses the one envelope: { success, response, responseCode, data, error }.
# On failure data is null and error = { errorCode: "#<status>", errorMessage }.

Game types

GET /v1/game-types

Catalog of variants + time controls — render your game-picker from it. No parameters.

http
GET /v1/game-types
X-Service-Auth: <secret>

# 200 — catalog the platform renders its "choose game" UI from
{
  "success": true, "response": "success", "responseCode": 200,
  "data": {
    "variants": ["standard"],
    "timeControls": [
      { "id": "bullet_1_0", "speed": "bullet", "base": 60,  "inc": 0, "label": "1 | 0" },
      { "id": "blitz_3_2",  "speed": "blitz",  "base": 180, "inc": 2, "label": "3 | 2" },
      { "id": "rapid_10_5", "speed": "rapid",  "base": 600, "inc": 5, "label": "10 | 5" }
    ]
  }
}
Response fieldTypeDescription
variantsstring[]available variants, e.g. "standard"
timeControls[].idstringthe id you pass to queue / play_bot / tournaments
timeControls[].speed"bullet" | "blitz" | "rapid"rating bucket the control belongs to
timeControls[].basenumberbase clock in seconds
timeControls[].incnumberincrement per move in seconds
timeControls[].labelstringdisplay label, e.g. "3 | 2"

Sessions

POST /v1/sessions

FieldTypeRequiredDefaultDescription
userIdstringyes—platform user id the token is minted for (opaque to the engine)
Response fieldTypeDescription
tokenstringopaque session token — the client's socket credential for the whole login session
expiresAtstring (Y-m-d H:i:s.v)token expiry; re-mint at the next login, not before
http
POST /v1/sessions
X-Service-Auth: <secret>

{ "userId": "user_123" }

# 201
{ "success": true, "response": "success", "responseCode": 201,
  "data": { "token": "s_9f3c...", "expiresAt": "2026-07-21 11:00:00.000" } }

# FAILS CLOSED: refuses to serve when the service secret is unconfigured.
# Minting again does not invalidate earlier tokens — the socket enforces one-per-user.

Tournaments

http
POST /v1/tournaments                 → 201 { data: { tournamentId } } · 422 unknown format
POST /v1/tournaments/{id}/join       → 200 { data: null } · 409 already in a tournament
POST /v1/tournaments/{id}/withdraw   → 200 { data: null }   (forfeits remaining games)
POST /v1/tournaments/{id}/start      → 200 { data: null }   (close registration + begin)
GET  /v1/tournaments                 → 200 tournaments still open for registration
GET  /v1/tournaments/{id}            → 200 config + standings + current round

POST /v1/tournaments

FieldTypeRequiredDefaultDescription
formatstringyes—arena · swiss · round_robin · knockout (unsupported → 422)
variantstring—"standard"one of the variants from GET /v1/game-types
timeControlstringyes—a time-control id from GET /v1/game-types
ratedboolean—truewhether tournament games update Glicko-2
roundsnumberif swissceil(log2(n))number of Swiss rounds
roundDeadlineSecnumber—0per-round deadline in seconds; 0 = none
allowMatchmakingDuringTournamentboolean—falsefalse = exclusive (blocks the player's matchmaking while entered)
arenaobjectif arena—{ durationMin } — minutes, capped 1..1440

POST /v1/tournaments/{id}/join

FieldTypeRequiredDefaultDescription
userIdstringyes—participant
ratingobjectif rated—{ r, rd, vol } for the tournament's (variant, speed)

GET /v1/tournaments

Response fieldTypeDescription
tournaments[].tournamentIdstringid
tournaments[].formatstringarena · swiss · round_robin · knockout
tournaments[].variant / timeControlstringgame type
tournaments[].ratedbooleanrated flag
tournaments[].playersnumbercurrent entrant count
tournaments[].durationMin / roundsnumberarena duration · swiss rounds (per format)

GET /v1/tournaments/{id}

Response fieldTypeDescription
standings[].userIdstringparticipant
standings[].ranknumbercurrent rank (1-based)
standings[].scorenumberpoints
standings[].gamesnumbergames played

Matches

GET /v1/matches/{id}

http
GET /v1/matches/{id}
# 200 — history / state of one game
{
  "success": true, "response": "success", "responseCode": 200,
  "data": {
    "matchId": "match_abc", "status": "finished",
    "result": "white_win", "termination": "checkmate",
    "movesUci": ["e2e4","e7e5","d1h5","b8c6","f1c4","g8f6","h5f7"]
  }
}
Response fieldTypeDescription
matchIdstringid
status"active" | "finished"live or done
resultstringwhite_win | black_win | draw | aborted (finished only)
terminationstringhow it ended — see Errors & codes for the full list
movesUcistring[]full move list (UCI)

Game review

GET /v1/matches/{id}/review

http
GET /v1/matches/{id}/review
# First call starts async Stockfish generation:
#   202 { data: { status: "generating", matchId } }  → poll until 200
#   200 → the finished review (cached ~1d in Redis, stored permanently)
#   503 → analysis engine unavailable
Response field (data.*)TypeDescription
engine / depthstring / numberanalyzer + depth (e.g. "Stockfish", 18)
white / blackobjectper-player summary: { accuracy, estRating, counts }
…accuracynumber0..100 — win%-model accuracy (heuristic, chess.com-style)
…estRatingnumberestimated performance rating for this game (heuristic)
…countsobject10 keys: brilliant · great · book · best · excellent · good · inaccuracy · mistake · miss · blunder
moves[].plynumber1-based ply index
moves[].by"white" | "black"side that moved
moves[].uci / beststringthe move played · the engine's best move
moves[].cpWhitenumbereval after the move, centipawns from White's view
moves[].cpLossnumbercentipawns given up vs the best move
moves[].classificationstringone of the 10 classes above
evalGraph[]object[]{ ply, cp } — plot-ready eval curve

ℹ accuracy / estRating / classification are heuristics (win%-model, chess.com-style) — an approximation, not a replica of any specific ladder.

Reconciliation

GET /v1/matches/results?since=<cursor>

http
GET /v1/matches/results?since=<cursor>&limit=200
# Reconciliation — finished game_result events ended after "since", oldest-first.
# since: unix ms (also "Y-m-d H:i:s.v" or RFC3339); empty = from the beginning.
# limit: default 200, max 1000.
{
  "success": true, "response": "success", "responseCode": 200,
  "data": { "results": [ { /* identical game_result payloads */ } ],
            "count": 12, "nextCursor": "1720429652000" } }

Puzzles

GET /v1/puzzles/next

FieldTypeRequiredDefaultDescription
ratingnumber—1500player's puzzle rating — drawn within ±300
themestring—anyfilter by theme; omit for any
userIdstring——carried for tracking; selection is by rating + theme
Response fieldTypeDescription
puzzleIdstringid for the attempt calls
fenstringposition BEFORE the setup move
setupMovestringopponent's setup move (UCI) — animate it, then the player solves
ratingnumberthe puzzle's rating
themesstring[]the puzzle's themes

POST /v1/puzzles/{id}/attempt

FieldTypeRequiredDefaultDescription
userIdstringyes—player attempting the solve
plynumberyes—solution index being answered (1, 3, 5, …)
ucistringyes—the move played (UCI)
msTakennumber——time spent (ms) — recorded, not rated
ratingobjectif rated—{ r, rd, vol } overall puzzle rating; omit to record unrated
themeRatingsobject——{ [theme]: { r, rd, vol } } per tracked theme
Response fieldTypeDescription
correctbooleanthis move matched the solution
donebooleanthe solve is over (finished or failed)
solvedbooleanthe whole puzzle was solved correctly
reply / nextPlystring / numbermid-solve only: opponent's reply to animate + the next ply to answer
ratingobjectfirst rated attempt only: { before, after, delta } for the overall puzzle rating
themeRatingsobjectfirst rated attempt only: { [theme]: { before, after, delta } }

Hosted by you: GET /users/{userId}

The one endpoint the engine pulls — user detail + every rating category, at enqueue. Request carries no query parameters. The payload sits under a data envelope with snake_case keys (user_id); the engine unwraps it. Full behaviour in Platform setup.

FieldTypeRequiredDefaultDescription
data.user_idstringyes—echoed back (opaque to the engine)
data.username / avatar / titlestring—omitteddisplay passthrough; username is placed on each match_found / game_state seat so clients render the opponent by name
data.ratingsobjectyes—EVERY category, keyed "<variant>:<speed>" → { r, rd, vol, lastGameAt?, floor? }

Status codes

400 malformed payload · 401/403 service auth failure · 404 not found · 409 conflict (e.g. already in a game/tournament) · 422 validation (unknown game type / format) · 503 engine or analysis not ready. Full handling advice: Errors & codes.