Skip to content

REST Endpoints

All routes are relative to https://play-api.jc.id.lv and require Authorization: Bearer <token> unless marked public. See Authentication & Identity for tokens and Common error codes.

Covered in depth under Authentication & Identity; summarised here.

Method Path Purpose
POST /bot/anon Mint an anonymous token (public; ?name= optional).
POST /bot/register Claim a durable identity. Token shown once.
POST /bot/token Rotate the token (registered only).
GET /bot/account Current identity.
POST /bot/ladder/join · /bot/ladder/leave Opt in/out of the rating ladder (registered only).

POST /bot/challenge

{ "team": "house", "name": "greedy", "timeControl": { "Unlimited": {} } }

Responds 201 with the challenge, including targetOnline (advisory — an offline target can still discover it by polling). Errors: 400 challenging yourself; 429 too many pending. An unclaimed challenge expires after ~5 minutes.

GET /bot/challenges

Every pending challenge involving you. in entries are addressed to you (accept/decline by id); out are yours to watch. Recovers challenges you missed while offline.

{ "in": [{ "id": "challenge-7", "challenger": { "Bot": { "team": "acme", "name": "rival" } }, "timeControl": { "Unlimited": {} } }], "out": [] }

POST /bot/challenge/{id}/accept201 { "gameId": "game-uuid" } (only the challenged bot). POST /bot/challenge/{id}/decline200.

Also useful for testing your own bot against yourself — see Play Against Your Bot.

POST /bot/seeks

A standing public offer in the same lobby guests use — anyone, human or bot, may accept it.

{ "timeControl": { "Fischer": { "initialSeconds": 180, "incrementSeconds": 2 } } }

Responds 201 { "seekId": "seek-12", "secret": "capability-secret" }. Hold the seek by polling GET /lobby/seeks/{id}?secret=<secret> — bot seeks expire after ~2 minutes without a poll; that same poll reports the match. Cancel with DELETE /lobby/seeks/{id}?secret=<secret>. Cap: 3 open seeks (429 beyond).

POST /bot/seeks/{id}/accept — accept an open seek from the public GET /lobby/seeks list. Colour is random; read it off GET /bot/games. Errors: 404 no such seek, 409 claimed first, 400 your own seek.

POST /bot/game/{id}/seed

Contribute this seat’s entropy for the provably-fair dice. Submit once, as soon as the game starts and before the opening roll.

{ "seed": "f3a1c0de9b8a7c6d" }

Responds 202 (fire-and-forget). A duplicate, too-late, or malformed seed is ignored (a malformed one may surface as a Rejected game-stream event). A seed is 16–256 characters.

POST /bot/game/{id}/move

The turn’s micro-moves in UCI, one per rolled die.

{ "moves": ["e2e4", "g8f6"] }

The verdict is synchronous:

  • 200 { "applied": true, "version": 17, "reason": null } — applied; version is the resulting TurnPlayed’s v.
  • 409 { "applied": false, "version": null, "reason": "illegal turn" } — refused, same reason the stream’s Rejected carries ("not your turn", "illegal turn", "game is over").
  • 202 — fallback: no verdict within a few seconds (never blocks on a wedged game); treat as fire-and-forget and watch the stream.

A TurnPlayed/Rejected still broadcasts on the game stream regardless, so fire-and-forget bots can ignore the body.

POST /bot/game/{id}/resign202.

GET /bot/games

Every live game you are seated in — the polling counterpart of GameStart and the post-restart recovery path.

{ "games": [{ "gameId": "game-uuid", "seat": "White", "activeSeat": "White", "dicePending": true, "timeControl": { "Unlimited": {} }, "clocks": null, "version": 17 }] }

GET /gamespublic. Every live game on the node, both seats’ public faces — the spectating surface. Sorted by version descending, capped at 50; total carries the real count. No legal-move tree (fetch the per-game endpoint).

{
"games": [{
"gameId": "game-uuid",
"players": { "white": { "kind": "Bot", "name": "house greedy" }, "black": { "kind": "Human", "name": null } },
"timeControl": { "Unlimited": {} },
"activeSeat": "Black", "dicePending": true, "clocks": null, "version": 17,
"dfen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1 nk"
}],
"total": 1
}

GET /games/{id}/movespublic. The full legal-move tree for the pending roll, never capped.

{
"version": 4,
"dfen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 NBK",
"dicePending": true,
"legalMoves": { "b1c3": { "g1f3": { "e2e4": {} } }, "e2e4": { "b1c3": { "g1f3": {} } } }
}

version and dfen tie the tree to the roll it answers. legalMoves is {} when dicePending is false or the roll is a forced pass. Errors: 404 unknown game.

GET /games/{id}public. The polling read of a single game: the same Snapshot.state object the game stream sends on connect (documented under Event Streams) — dfen, activeSeat, dicePending, clocks, commit, players, and, while dicePending is true, the inline legalMoves. This is also where a withheld dice reveal becomes available: for a mirror-pair game, seed and clientSeeds stay null on the live GameEnded event until both games conclude, then appear here on a re-poll. Errors: 404 unknown game.

Public, no Authorization. Both exist only when the server runs with persistence; an in-memory dev server answers 404.

GET /leaderboard

Registered bots whose rating has converged (RD ≤ 110), best first. Provisional bots are counted internally but absent by policy — see Rating & Ladder for what that means and why it can take longer than expected. wins/draws/losses count rated, decided games only.

{ "leaders": [{ "rank": 1, "team": "acme", "name": "alice", "rating": 1720.5, "rd": 85.2, "onLadder": true, "games": 42, "wins": 30, "draws": 2, "losses": 10 }] }

A bot that left the ladder keeps its frozen rating and stays listed with onLadder: false.

GET /bots/{team}/{name}

One registered bot’s public card: rating summary plus up to 20 recent games, newest first. Unlike the board, a provisional bot is visible here (flagged). opponent is a public face (never a raw id); result is from the profiled bot’s point of view.

{
"team": "acme", "name": "alice",
"rating": 1650.0, "rd": 95.0, "provisional": false, "onLadder": true,
"games": 30, "wins": 20, "draws": 3, "losses": 7,
"recent": [{ "gameId": "game-uuid", "seat": "White", "opponent": { "kind": "Bot", "name": "acme bob" }, "result": "win", "rated": true, "termination": "resign", "finishedAt": "2026-07-16T12:00:00Z" }]
}

Errors: 404 no registered bot with that team/name.