Platform setup

The standing contract between your platform and the engine: how server-to-server calls authenticate, the one response envelope every endpoint uses, and the two endpoints you must host.

Service auth (server ↔ server)

bash
# All REST calls are server-to-server. Base path: /v1
# Two-layer auth, both directions:
#   1. IP whitelist  (private network / VPC)
#   2. Header        X-Service-Auth: <secret>
# End-user socket clients do NOT use this — they use a session token from
# POST /v1/sessions, passed in the WebSocket subprotocol (next chapter).

The IP whitelist authenticates the network; the static secret authenticates the caller — cloud egress IPs can change under autoscaling, so both layers exist. The same pair protects both directions: your calls into /v1, and the engine's calls to your webhook and user-data endpoint.

The response envelope

Every JSON response from the engine uses the same envelope — { success, response, responseCode, data, error } — so your success and failure paths parse identically. All examples in these docs show the complete envelope.

jsonc
# EVERY JSON response from the engine is wrapped in one envelope, so success
# and failure parse the same way. responseCode mirrors the HTTP status.

# success — the endpoint's payload is in "data"
{
  "success": true,
  "response": "success",
  "responseCode": 200,
  "data": { /* the per-endpoint object */ }
}

# error — "data" is null and "error" carries the detail
{
  "success": false,
  "response": "error",
  "responseCode": 409,
  "data": null,
  "error": { "errorCode": "#409", "errorMessage": "user is already in an active game" }
}

# Actions with no payload return 200 with "data": null.

The two endpoints you host

The engine calls you in exactly two places. Everything else in the integration is you calling the engine.

POST your webhook — callbacks in

One endpoint receives all six event types. The engine retries with backoff until you acknowledge; Callbacks & reliability covers each event in depth.

ts
// The webhook — reply 2xx once processed, 409 for a duplicate eventId
// (the engine treats 409 exactly like 2xx: delivered, stop retrying).
app.post("/callbacks", (req, res) => {
  const ev = req.body;
  if (seen(ev.eventId)) return res.sendStatus(200);   // idempotent — already processed
  switch (ev.type) {
    case "match_found":         notifyPlayers(ev);    break; // notification — the engine already pushed the game
    case "match_aborted":       requeueOrForfeit(ev); break;
    case "game_result":         applyRatings(ev);     break; // persist + clamp to floor
    case "tournament_complete": saveStandings(ev);    break;
    case "fairplay_flag":       reviewSignal(ev);     break;
  }
  markSeen(ev.eventId);
  res.sendStatus(200);
});

GET <platform>/users/{userId} — ratings out

Called by the engine at enqueue (matchmaking), server-to-server. This is how a rating reaches the engine without the client ever sending one — a client that could state its own rating could farm weak pairings.

FieldTypeRequiredDefaultDescription
data.user_idstringyes—the id echoed back (opaque to the engine)
data.usernamestring—omitteddisplay name — the engine puts it on each match_found / game_state seat so the client can render the opponent by name; no canonical copy is kept
data.avatarstring—omittedavatar URL passthrough
data.titlestring—omittedchess title passthrough (FM · IM · GM …)
data.ratingsobjectyes—map keyed "<variant>:<speed>" → a rating block (fields below). Return EVERY category; omit ones never played

Each value under ratings is a Glicko-2 triple plus optional idle/floor hints:

FieldTypeRequiredDefaultDescription
rnumberyes—current rating, any positive number (e.g. 1512.3)
rdnumberyes—rating deviation (uncertainty)
volnumberyes—Glicko-2 volatility
lastGameAtnumber—omittedunix ms of the last game — drives idle RD inflation; omit if unknown
floornumber—omittedpeak-linked lower bound; the engine clamps the resulting rating up to it
http
GET <platform>/users/user_123
X-Service-Auth: <secret>              // engine → platform, server-to-server

// The engine sends NO query parameters — return EVERY rating category the
// player has and the engine selects the one for the pool being joined.
// You host this endpoint, so it uses YOUR response envelope: the payload sits
// under "data" (snake_case), and the engine unwraps it.

→ 200
{
  "success": true,
  "response_code": 200,
  "message": "User found.",
  "data": {
    "user_id": "user_123",
    "username": "magnus_fan",          // optional — display only
    "title": "FM",                     // optional — passthrough (FM · IM · GM …)
    "ratings": {
      "standard:bullet": { "r": 1489.2, "rd": 92.0,  "vol": 0.0601, "lastGameAt": 1719820000000, "floor": 1420 },
      "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
}

// The engine reads "data" and ignores success/response_code/message/errors.
// The identity key is "user_id" (snake_case). Keys are "<variant>:<speed>".
// Omit a category the player has never played — the engine seeds it
// provisionally rather than trusting an invented 1500.
// If this endpoint is unreachable within ~2–3s the enqueue is REJECTED
// (error code platform_unavailable) — there is deliberately no client fallback.

⚠ Rated matchmaking hard-depends on this endpoint. The engine caches the pulled rating per user on the session (one call covers queue/cancel loops and category switches) and recomputes Glicko-2 itself after each game, with a short TTL to pick up manual adjustments. Tournament join does not use it — the rating rides in the join request body instead, because that call is already server-to-server.

Checklist

  • Host one webhook endpoint (e.g. POST /callbacks) reachable by the engine; reply 2xx once processed.
  • Deduplicate every callback by eventId (idempotent) — already-processed events still get a 2xx (or 409).
  • Mint a session token at login (POST /v1/sessions) and hand it to that user's client — one socket per login session.
  • Expose GET /users/{userId} returning user detail + EVERY rating category, keyed "<variant>:<speed>".
  • Persist the new rating from game_result and clamp to the floor (track the peak per category) — the callback states its own variant / timeControl / speed.
  • Run a periodic reconciliation job: GET /v1/matches/results?since=<cursor>.
  • Provide an IP whitelist + shared secret; send the X-Service-Auth header on every REST call.

What can go wrong

SymptomCause & fix
401 / 403 from the engineMissing or wrong X-Service-Auth header, or your server's IP is not whitelisted. POST /v1/sessions additionally fails closed: it refuses to serve at all while the engine's secret is unconfigured.
Rated enqueue rejected with platform_unavailableYour /users/{userId} endpoint was unreachable or slower than ~2–3s. The engine never falls back to a client-supplied rating — fix availability, don't work around it.
Engine retries the same callback foreverYour webhook is not replying 2xx (or replies 409 only for real duplicates). Reply 2xx after processing — even for events you choose to ignore.
A player's blitz rating changed after a rapid gameYou filed the rating into the wrong category. Always write to ratings[variant + ":" + speed] using the fields ON the game_result payload — never infer the category.