Webhooks
Instead of polling, let Chess Scanner notify your app whenever one of a user's
games changes. You receive a signed HTTP POST for these events:
game.createdgame.updatedgame.deleted
Set up a webhook
- Open your app under Dashboard → OAuth Apps → your app.
- In the Webhooks section, set:
- Payload URL: where deliveries are sent, for example
https://your-app.com/webhooks/chess-scanner. - Events: which of the three events you want to receive.
- Active: turn delivery on or off.
- Payload URL: where deliveries are sent, for example
- Save. A signing secret is generated and shown once, so store it. You'll need it to verify deliveries.
A webhook delivers events for every user who authorized your app with the
games.read scope.
Payload
Deliveries are intentionally thin. They tell you what changed, not the full game, so fetch the current state through the API.
{
"id": "evt_…",
"type": "game.updated",
"timestamp": "2026-06-14T10:00:00.000Z",
"data": { "gameId": "…", "databaseId": "…" }
}
Every request carries these headers:
| Header | Description |
|---|---|
X-Signature | sha256= + HMAC-SHA256 of the raw body, keyed with your secret |
X-Webhook-Id | Unique delivery id (use it to deduplicate) |
X-Webhook-Event | The event type |
X-Webhook-Timestamp | Unix seconds when the event happened |
Verify the signature
Always verify the signature before you trust a delivery. Compute the HMAC over the raw request body and compare it in constant time:
import { createHmac, timingSafeEqual } from "node:crypto";
export function isValid(rawBody, signatureHeader, secret) {
const expected =
"sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(signatureHeader ?? "");
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
Respond with a 2xx status quickly and do any heavy work afterwards. Anything
other than a 2xx, a timeout, or a connection error counts as a failure and is
retried.
Delivery & reliability
- At least once. Failed deliveries are retried with exponential backoff up to
a maximum number of attempts, then marked failed. Make your handler idempotent
by deduplicating on
X-Webhook-Id. - Order isn't guaranteed. Treat each event as a signal that something changed and fetch the latest, rather than relying on the order they arrive in.
- History and resend. The app's detail page shows recent deliveries with their status and lets you resend any of them with one click.
Webhooks are best effort. To be sure you didn't miss anything (for example after
downtime), reconcile against the /api/v1/changes feed.