🎮 Beta Gamer

Webhooks

We POST signed HTTP callbacks to your server when game events occur. Webhooks only fire for live sessions — never for test or training.

Events

EventWhen it firesGames
session.createdSession is created via APIAll
session.startedBoth players connect and game beginsAll
session.expiredSession times out before players joinAll
game.moveA player makes a moveAll
game.endedGame finishes (any reason)All
chess.checkA king is placed in checkchess
chess.promotionA pawn is promotedchess
chess.castleA castling move is madechess
chess.afk_warningActive player idle 60 s — 30 s to actchess
chess.afk_timeoutPlayer forfeited for inactivity (90 s)chess
checkers.kingA piece is kingedcheckers
checkers.captureOne or more pieces captured (incl. multi-jump)checkers
checkers.afk_warningActive player idle 60 s — 30 s to actcheckers
checkers.afk_timeoutPlayer forfeited for inactivity (90 s)checkers
connect4.threatA player has 3-in-a-row with open endconnect4
tictactoe.forkA player creates a double-threat forktictactoe

Payload envelope

Every webhook shares the same outer envelope. The data field varies by event type.

{
  "event":      "game.ended",          // string — event type
  "deliveryId": "550e8400-...",        // string (UUID) — unique per delivery attempt
  "sessionId":  "a1b2c3d4-...",        // string (UUID)
  "game":       "chess",               // "chess" | "checkers" | "connect4" | "tictactoe" | "subway-runner"
  "mode":       "live",                // always "live"
  "timestamp":  "2026-03-15T09:00:00Z",// ISO 8601 UTC
  "data": { ... }                      // event-specific payload (see below)
}

HTTP headers

HeaderTypeDescription
X-BetaGamer-EventstringEvent type, e.g. game.ended
X-BetaGamer-Signaturestringsha256=<hmac-hex> — HMAC-SHA256 of raw body
X-BetaGamer-Deliverystring (UUID)Unique delivery ID, matches deliveryId in body
Content-TypestringAlways application/json

Event payloads

session.created / session.started / session.expired

{
  "players": [
    { "id": "user_123", "displayName": "Alex" },
    { "id": "user_456", "displayName": "Jordan" }
  ],
  "matchType": "matchmaking"   // "matchmaking" | "private" | "bot"
}

game.move

{
  "playerId":  "user_123",
  "moveIndex": 4,              // integer — 0-based move number
  "move":      "e2e4",         // string — game-specific notation (see below)
  "timestamp": "2026-03-15T09:00:05Z"
}
Gamemove formatExample
chessUCI notation"e2e4", "e7e8q" (promotion)
checkers"fromSquare-toSquare""11-15", "22x13" (capture)
connect4column index (0–6)"3"
tictactoe"row,col" (0–2)"1,2"
subway-runneraction string"jump", "slide", "left", "right"

game.ended

{
  "players": [
    { "id": "user_123", "displayName": "Alex" },
    { "id": "user_456", "displayName": "Jordan" }
  ],
  "result": {
    "winner":    "user_123",   // string (player id) | null (draw)
    "reason":    "checkmate",  // see reason values per game below
    "duration":  342,          // integer — seconds
    "moveCount": 38,           // integer
    "scores":    { "user_123": 1, "user_456": 0 },  // float — 0.5 each for draw
    "pgn":       "1. e4 e5 ..." // chess only — full PGN string | null for other games
  }
}
Gamereason values
chess"checkmate" | "resignation" | "timeout" | "stalemate" | "draw_agreement" | "insufficient_material" | "repetition"
checkers"victory" | "resignation" | "afk_timeout" | "draw" | "disconnect"
connect4"four_in_a_row" | "resignation" | "timeout" | "draw"
tictactoe"three_in_a_row" | "draw" | "timeout"
subway-runner"collision" | "timeout" | "distance_limit"

Game-specific events

Some games emit additional events beyond the standard set. See each game's webhook reference for full payload details.

Verifying the signature

Always verify X-BetaGamer-Signature before processing. Use your webhookSecret from the dashboard.

const crypto = require('crypto');

function verifyWebhook(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)   // raw Buffer — NOT parsed JSON
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(expected)
  );
}

// Express example
app.post('/webhooks/beta-gamer', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-betagamer-signature'];
  if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  const { event, data } = JSON.parse(req.body);
  switch (event) {
    case 'game.ended':   /* award points, update leaderboard */ break;
    case 'game.move':    /* broadcast to spectators */          break;
    case 'chess.check':  /* notify player */                    break;
  }
  res.sendStatus(200); // acknowledge within 5 seconds
});
⚠️ Use express.raw() (or equivalent) to read the raw body before parsing JSON. Parsing first changes the byte representation and breaks signature verification.

Retry policy

Timeout
5 seconds per attempt
Max retries
3 (after initial attempt)
Backoff
Exponential: 2s, 4s, 8s
Ack
Return HTTP 200 to acknowledge

Idempotency

Each delivery has a unique deliveryId. Store processed IDs to safely handle duplicates — your server may receive the same event more than once if it returned a non-200 but still processed it.

const processed = new Set(); // use Redis or DB in production

app.post('/webhooks/beta-gamer', express.raw({ type: 'application/json' }), (req, res) => {
  // ... verify signature ...
  const { deliveryId, event, data } = JSON.parse(req.body);
  if (processed.has(deliveryId)) return res.sendStatus(200);
  processed.add(deliveryId);
  // handle event...
  res.sendStatus(200);
});
Beta Gamer GaaS API — questions? support@beta-gamer.com