FAQ
Common situations during integration. Click to expand.
The socket handshake is refused / the session token expired?
A session token is minted by POST /v1/sessions at login, is multi-use for its TTL, and is validated before the upgrade — so a bad or expired token is refused at the handshake, never as a message. The close carries code 4001: stop retrying and ask the platform for a fresh token. Keep the TTL short and re-mint at login rather than issuing long-lived tokens; if it leaks it grants account access until it expires.
A user opened a second tab and the first socket died?
Working as designed — one socket per user, newest wins. The second connection closes the first with code 4002, and the SDK must not retry on it or the two tabs kick each other in a loop. Oldest-wins was rejected: a user whose browser crashed would be locked out of their own account until the session reaper runs. Games are unaffected — the new socket re-attaches to the same room by userId.
Why can't the client send its own rating?
Because the enqueue originates from the client, and a client that states its own rating can farm weak pairings. The engine pulls user detail + the rating triple from GET <platform>/users/{userId} at enqueue, caches it on the session and refreshes it locally after each game (it computes Glicko-2 itself). If the platform is unreachable within 2–3s the enqueue is rejected — there is deliberately no fallback to a client-supplied rating. Tournament join is unaffected: it is server-to-server, so the rating still rides in the request body.
A player doesn't show up at the start (no-show)?
Presence gate. ready_check (matchmaking default): both players are already connected, so each confirms with a { type: "accept" } socket message and the clock starts once both have; not confirming by gateTimeoutSec (~30s) aborts. grace_period is tournaments only — entrants joined over REST and may be offline when their round opens. Bot games have no gate at all. Either way the engine emits match_aborted (reason no_show) → matchmaking requeues whoever showed up, a tournament forfeits the absentee.
Connection drops mid-game?
The clock keeps running (server-authoritative). Reconnect is just dialling again with the same session token; the engine finds the room by userId and the SDK does it automatically. Your own game is restored; spectate subscriptions and a queue entry are not, so re-request those.
Is the socket staying open the same as being at the board?
No. A session socket stays open while the user browses elsewhere, so presence is derived from the subscription: subscribing to a room means at the board, unsubscribing means left it. Anything that needs real presence (the tournament gate, arena availability) uses the subscription, never socket liveness. Logout itself is inferred too — a session disconnected for more than 5 minutes is reaped, so there is no logout endpoint.
A callback arrives twice?
Expected — at-least-once delivery. Deduplicate by eventId; already-processed events still return 2xx (or 409) so retries stop. The net effect is exactly-once.
A callback never arrives (webhook down)?
The engine retries with backoff until 2xx (game_result & tournament_complete retry indefinitely). Backstop: run the pull job GET /v1/matches/results?since=<cursor> to apply missed results.
Callback ordering isn't guaranteed?
Don't rely on order across eventIds. Reconstruct via matchId + type (e.g. match_found before game_result for the same match).
A user tries to play two games at once?
Rejected — a hard invariant of one active game per user. On the socket the engine replies with an error carrying code already_in_game; REST endpoints reply 409 with the same code in the envelope. The platform may pre-check, but the engine is authoritative.
Can a user queue matchmaking while in a tournament?
By default no — entering a tournament is exclusive, so a { type: "queue" } message is rejected (error code already_in_game) while the user is entered. Set allowMatchmakingDuringTournament:true when creating the tournament to permit it.
When can the next game start for that user?
After the previous game's result has been applied by the platform (sequential rating consistency). The engine holds the new game until the busy-state is released post-apply.
What's different about unrated games?
queue with rated:false (FIFO pool, no rating lookup). Its game_result omits the ratingBefore/After/delta block. Bot games are always unrated.
What is autoBot and when does the bot fallback kick in?
A seeker still unpaired after ~30s is dropped into an UNRATED game against Stockfish — the match_found arrives with an isBot seat so the client can tell the user. It is per-enqueue opt-out: queue with autoBot:false to keep waiting for a human until the queue TTL (~90s) instead. A requeue after an opponent no-show resets it to on.
How do the rating floor & idle work?
Peak-linked floor: the platform tracks the peak per (variant, speed) and returns floor from GET /users/{userId}; the engine clamps the resulting rating. Idle: the same response carries lastGameAt; the engine performs the RD inflation. Ratings are separate per category (bullet/blitz/rapid).
Draws, threefold, 50-move?
Resign any time. Draw offers are rate-limited (must play ≥1 move before offering again). Threefold & 50-move are claimable; fivefold & 75-move are automatic; stalemate/insufficient/checkmate are always automatic.
How do puzzle ratings work?
GET /v1/puzzles/next?rating=&theme= serves a puzzle within ±300 of the player's rating (theme optional). The client solves move-by-move via POST /v1/puzzles/{id}/attempt (send ply + uci); mid-solve the engine returns the opponent reply + nextPly. Only the first attempt is rated — one Glicko-2 game vs the puzzle's rating, returning an overall delta plus a per-theme delta for each theme the platform tracks (send rating + themeRatings). Later attempts are recorded but unrated.
How do I get a game review / analysis?
GET /v1/matches/{id}/review. On-demand: the first call starts Stockfish analysis and returns 202 { status: "generating" } — poll until 200, which returns the permanent review (per-move cpLoss + classification, per-player accuracy & estRating, and an eval graph). Cached ~1 day in Redis, stored forever in the DB. accuracy/estRating and the ten move classes are heuristics (win%-model, chess.com-style) — an approximation, not a replica of any specific ladder. Not piggybacked on game_result — you fetch it when a user opens the analysis.
Do spectators see moves in real time?
Read-only, over the same session socket — { type: "subscribe", room }. Broadcasts are delayed by spectatorDelaySec (0 for regular games, >0 for tournaments so preparation can't be snooped) and a per-game spectator cap applies. Moves from spectators are rejected, and game frames are never dropped to make room for spectator fan-out. The authorisation decision lives in the engine: its default is open, so state a policy if you need to restrict who may watch what.
Does high latency affect the clock?
There is server-authoritative lag compensation (measured via ping/pong + RTT, capped at lagCompMaxMs per time control). Clients cannot cheat time.