Quickstart
The smallest complete integration: one rated blitz game from login to persisted rating. Seven steps — two endpoints you host, one REST call, and the SDK. Each step links to the guide chapter that covers it in depth.
Trace legend: user what the player does · platform your server code · SDK @inachess/sdk on the client · engine engine-internal · callback engine → your webhook.
- 1
Host two endpoints
Prerequisite · Platform setup →
- 🌐host webhook
POST /callbacks - 👤host user data
GET /users/{userId} - 🔐share secret + IPs with the engine
The engine calls you in exactly two places: a webhook that receives events, and a user-data endpoint it pulls ratings from when a player queues. Both are server-to-server, authenticated by IP whitelist + a shared
X-Service-Authsecret.ts// 1a. The webhook — the engine POSTs events here and retries until you reply 2xx. app.post("/callbacks", (req, res) => { const ev = req.body; if (seen(ev.eventId)) return res.sendStatus(200); // duplicate — already processed if (ev.type === "game_result") applyRatings(ev); // persist ratingAfter per player markSeen(ev.eventId); res.sendStatus(200); });ts// 1b. The user-data endpoint — the engine pulls it (server-to-server) when a // player queues. Return EVERY rating category the player has; omit ones never played. // The payload sits under "data" (snake_case) — the engine unwraps that envelope. app.get("/users/:userId", (req, res) => { res.json({ success: true, response_code: 200, message: "User found.", data: { user_id: req.params.userId, username: "magnus_fan", ratings: { "standard:blitz": { r: 1512.3, rd: 84.1, vol: 0.0598, lastGameAt: 1719828000000, floor: 1400 }, "standard:rapid": { r: 1533.9, rd: 70.4, vol: 0.0591 }, }, }, errors: null, }); }); - 🌐host webhook
- 2
Mint a session token at login
Platform → Engine ·
POST /v1/sessions· Sessions →- 👤User logs in
- 🔑platform calls
POST /v1/sessions - ⚙️engine mints session token
- 📨hand token to that user's client
One token per login, body carries
userIdonly. Hand the token to that user's client — it is the client's socket credential for the whole session.http# 2. At login, mint one session token and hand it to that user's client: 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" } } - 3
Connect the client
Client → Engine ·
@inachess/sdk· SDK reference →- 🔌SDK dials once
client.connect() - ✅token validated before the upgrade
pnpm add @inachess/sdkts// 3. In the client: one socket for the whole login session. import { EngineClient } from "@inachess/sdk"; const client = new EngineClient({ url: "wss://engine.example.com/ws", token }); client.on("matchFound", (m) => enterGame(m.matchId)); // route to the board client.on("gameState", (s) => render(s)); // full snapshot client.on("move", (m) => render(m.fen, m.turn)); // per-move update client.on("gameOver", (g) => show(g.result, g.reason)); client.on("error", (e) => handle(e.code)); // branch on code, not message client.connect(); - 🔌SDK dials once
- 4
Queue and get seated
Client → Engine (socket) · Finding a game →
- 👆User clicks Play · blitz 3|2
- 📤SDK sends
client.queue({ … }) - 🔎engine pulls your /users/{userId} → enters the pool
- 📡pushes match_found + game_state to both sockets
- ✅User clicks Ready
- 📤SDK sends
client.accept() - ⏱️clock starts once both accept
ts// 4. Queue for a rated blitz game — never send a rating; the engine pulls it // from YOUR /users/{userId} endpoint (step 1b): client.queue({ variant: "standard", timeControl: "blitz_3_2", rated: true }); // When paired, the engine pushes match_found (route to the board) followed by // game_state. The clock is NOT running yet — the ready_check gate holds it: client.on("gameState", (s) => { render(s); if (!s.started && s.gate === "ready_check" && !s.youAccepted) { showReadyButton(); // or client.accept() immediately on arrival } }); client.accept(); // clock starts once BOTH players have accepted⚠ The clock is held by the ready_check presence gate until both players send
accept. Not confirming within ~30s aborts the match as ano_show— nobody's rating changes. - 5
Play
Client ⇄ Engine (socket) · Playing a game →
- ♟️User drags e2 → e4
- 📤SDK sends
client.move("e2e4") - ✔️validates + ticks clock + broadcasts
- 📥SDK fires
on("move") → render
ts// 5. Play. Send UCI intent; render only what the engine broadcasts back: client.move("e2e4"); // promotion: 5-char UCI, e.g. "e7e8q" client.on("move", (m) => { render(m.fen, m.turn); // m.whiteMs / m.blackMs — server clocks, never tick your own }); client.on("gameOver", (g) => show(g.result, g.reason)); // ends this room onlyLive: each
client.move(…)below slides a piece — the client only sends the UCI string, the board is server state pushed back:User BBlack ♚LIVE♜♞♝♛♚♝♞♜♟♟♟♟♟♟♟♟♟♟♟♟♟♟♟♟♜♞♝♛♚♝♞♜User AWhite ♔to move// the per-move loopUser A · WhiteUser B · BlackAclient.move("e2e4");Bclient.move("e7e5");Aclient.move("d1h5");Bclient.move("b8c6");Aclient.move("f1c4");Bclient.move("g8f6");Aclient.move("h5f7");▶ looping · pure HTML/CSS/JS · A = white seat, B = black seat - 6
Receive the result, persist the rating
Engine → Platform (callback) · Callbacks →
- 🏁game ends → computes Glicko-2
- 📨engine POSTs
game_result - 💾persist rating (clamp floor), reply 2xx
json// 6. The result arrives at your webhook (step 1a) — it names its own rating // category, so you file it straight from the payload: { "eventId": "evt_003", "type": "game_result", "matchId": "match_abc", "variant": "standard", "timeControl": "blitz_3_2", "speed": "blitz", "result": "white_win", "termination": "checkmate", "players": [ { "userId": "user_123", "username": "magnus_fan", "color": "white", "score": 1, "ratingAfter": { "r": 1524.7, "rd": 78.2, "vol": 0.0597 }, "delta": 12.4 }, { "userId": "user_456", "username": "the_turk", "color": "black", "score": 0, "ratingAfter": { "r": 1589.1, "rd": 58.4, "vol": 0.0601 }, "delta": -10.9 } ] } // → ratings["standard:blitz"] = ratingAfter, clamped to your tracked floor. - 7
Run the reconciliation backstop
Platform → Engine ·
GET /v1/matches/results· Reliability →http# 7. Backstop (periodic job): pull anything your webhook missed. Idempotent — # dedupe by the same eventId, persist nextCursor between runs. GET /v1/matches/results?since=<cursor>&limit=200 X-Service-Auth: <secret>
You are integrated when…
- Webhook replies 2xx and dedupes by eventId (send the same event twice — the rating must apply once).
- GET /users/{userId} returns every rating category, keyed "<variant>:<speed>".
- Login mints a session token and the client connects with it — one socket, no token in any URL.
- queue → match_found → accept → moves → game_over works end to end.
- game_result persists the rating into the category the payload names.
- The reconciliation job runs on a schedule and applies missed results.
From here the guide goes deeper chapter by chapter — starting with Platform setup, the contract behind step 1.