Code samples
Request and signature samples, callback integration requirements, and safe error handling in the three languages we ship hand-curated samples for. TypeScript, PHP, and Python — chosen because they cover ~85% of incumbent B2B casino backend stacks. For Java, Go, C#, Ruby, Kotlin, Rust — generate clients from our OpenAPI spec with openapi-generator-cli.
1. List games (first request)
Section titled “1. List games (first request)”List a single page of games for your enabled providers. Load the matching callAggregator / call_aggregator helper from Structured error handling first; both request examples use it to keep transport and response details out of diagnostics.
const payload = await callAggregator("/games?per_page=10");if (!payload || typeof payload !== "object" || Array.isArray(payload)) { throw new Error("Invalid games response");}const { games, total } = payload as { games?: unknown; total?: unknown };if (!Array.isArray(games) || typeof total !== "number" || !Number.isSafeInteger(total) || total < 0) { throw new Error("Invalid games response");}console.log(`Got ${games.length} of ${total} games`);<?php
$payload = callAggregator('/games?per_page=10');if (!is_array($payload) || !isset($payload['games'], $payload['total']) || !is_array($payload['games']) || !is_int($payload['total']) || $payload['total'] < 0) { throw new RuntimeException("Invalid games response");}printf("Got %d of %d games\n", count($payload['games']), $payload['total']);payload = call_aggregator("/games", params={"per_page": 10})if (not isinstance(payload, dict) or not isinstance(payload.get("games"), list) or type(payload.get("total")) is not int or payload["total"] < 0): raise ValueError("Invalid games response")print(f"Got {len(payload['games'])} of {payload['total']} games")2. Create a session
Section titled “2. Create a session”Real-money game launch using the same safe HTTP helper. The caller supplies playerId and spinId (PHP: $playerId, $spinId; Python: player_id, spin_id). Use your authorized player and a unique logical launch ID, persisted and reused only for retries of that launch. Requires an Idempotency-Key header — see Idempotency for the why.
const idempotencyKey = `launch-${playerId}-${spinId}`; // stable for retries
const session = await callAggregator("/sessions", { method: "POST", headers: { "Idempotency-Key": idempotencyKey, "Content-Type": "application/json", }, body: JSON.stringify({ // The Aggregator catalog UUID (the `id` from GET /v1/games) — NOT the // provider's own game code (provider_game_id). game_id: "d4f7a2b1-3c8e-4f5a-9b6d-1e2f3a4b5c6d", player_id: playerId, balance: 10_000, // minor units — 100.00 EUR currency: "EUR", country: "DE", lang: "de", return_url: "https://your-casino.example/lobby", }),});
// Validate the session response against the API contract before using its game_url.// Redirect the player to the validated game_url immediately — it is single-use.<?php
$idempotencyKey = "launch-{$playerId}-{$spinId}";
$session = callAggregator('/sessions', [ 'headers' => [ 'Idempotency-Key: ' . $idempotencyKey, 'Content-Type: application/json', ], 'curl' => [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode([ // The Aggregator catalog UUID (the `id` from GET /v1/games) — NOT the // provider's own game code (provider_game_id). 'game_id' => 'd4f7a2b1-3c8e-4f5a-9b6d-1e2f3a4b5c6d', 'player_id' => $playerId, 'balance' => 10000, // minor units — 100.00 EUR 'currency' => 'EUR', 'country' => 'DE', 'lang' => 'de', 'return_url' => 'https://your-casino.example/lobby', ]), ],]);
// Validate the session response against the API contract before using game_url.// Redirect the player to $session['game_url'] immediately — it is single-use.idempotency_key = f"launch-{player_id}-{spin_id}"
session = call_aggregator( "/sessions", method="POST", headers={ "Idempotency-Key": idempotency_key, }, json={ # The Aggregator catalog UUID (the `id` from GET /v1/games) — NOT the # provider's own game code (provider_game_id). "game_id": "d4f7a2b1-3c8e-4f5a-9b6d-1e2f3a4b5c6d", "player_id": player_id, "balance": 10_000, # minor units — 100.00 EUR "currency": "EUR", "country": "DE", "lang": "de", "return_url": "https://your-casino.example/lobby", },)
# Validate the session response against the API contract before using game_url.# Redirect player to session["game_url"] immediately — it is single-use.3. Verify a callback signature
Section titled “3. Verify a callback signature”The single highest-leverage snippet on this site. Copy verbatim. Full guide and replay-protection guidance: Signature verification.
The platform sends an X-SIGNATURE header containing a hex-encoded HMAC-SHA256 digest of the raw body, keyed with your callback_secret. No sha256= prefix.
import crypto from "node:crypto";
export function verifyAggregatorSignature( rawBody: Buffer, signatureHeader: string | undefined,): boolean { // The wire format is exactly 64 lowercase hexadecimal characters. if (typeof signatureHeader !== "string" || signatureHeader.length !== 64 || !/^[0-9a-f]+$/.test(signatureHeader)) return false;
const secret = process.env.AGGREGATOR_CALLBACK_SECRET; if (!secret) return false;
const expected = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex");
return crypto.timingSafeEqual( Buffer.from(expected, "hex"), Buffer.from(signatureHeader, "hex"), );}<?php
function verifyAggregatorSignature(string $rawBody, ?string $signatureHeader): bool { if ($signatureHeader === null || preg_match('/\A[0-9a-f]{64}\z/', $signatureHeader) !== 1) { return false; } $secret = getenv('AGGREGATOR_CALLBACK_SECRET'); if ($secret === false || $secret === '') return false; $expected = hash_hmac('sha256', $rawBody, $secret); return hash_equals($expected, $signatureHeader);}import hmacimport hashlibimport osimport re
def verify_aggregator_signature(raw_body: bytes, signature_header: str | None) -> bool: if not isinstance(signature_header, str) or re.fullmatch(r"[0-9a-f]{64}", signature_header) is None: return False secret = os.environ.get("AGGREGATOR_CALLBACK_SECRET") if not secret: return False expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature_header)4. Integrate a callback handler
Section titled “4. Integrate a callback handler”A callback endpoint combines the signature verifier with your validated wallet and idempotency implementation. The requirements below describe that integration; they are not a standalone wallet implementation. Follow the wallet callback contract for request fields, responses and business declines.
- Verify the signature over raw bytes before parsing or changing money. Validate the transaction fields and use fixed error codes; do not reflect request values into errors or logs.
- On a free
betwithis_free: true, do not debit the player’s real-money balance. Apply the free-round rules; paid bets, wins and refunds follow the corresponding wallet contract. - Atomically commit the wallet change and the stored original status and body under the integration/organization,
provider_codeandtransaction_ididentity before returning success. The same provider ID from another integration or provider is a different transaction. A duplicate replays the stored original status and body without another wallet movement. - A timeout does not prove settlement failed. Reconcile ambiguous outcomes before recovery; retry eligibility follows the deployed policy.
Use Express raw-body middleware for the callback route. Pass the original Buffer and X-SIGNATURE header to verifyAggregatorSignature, then call your validated settlement boundary.
Read the original bytes from php://input and the HTTP_X_SIGNATURE header. Use verifyAggregatorSignature before decoding JSON, then call your validated settlement boundary.
Read the original request body bytes before JSON decoding. Pass those bytes and X-SIGNATURE to verify_aggregator_signature, then call your validated settlement boundary.
5. Structured error handling
Section titled “5. Structured error handling”Business errors use an error object with a machine code; authentication and rate-limiting failures can use a flat error string. The examples handle both shapes and retain only the HTTP status and a fixed code from the sample’s allowlist. Extend the list with known catalogue codes your application handles. Unexpected values become UNRECOGNIZED; transport failures use status 0 with NETWORK_ERROR, and invalid JSON uses INVALID_RESPONSE. Messages, details, response bodies, and request IDs are untrusted and are never copied into these exceptions or diagnostic logs. Log only the status and allowlisted code, and use a correlation identifier generated by your own application when investigating failures.
// Extend this list only with fixed codes your application handles from the catalogue.const KNOWN_CODES = [ "E0001", "E0002", "E1002", "E1053", "E1054", "E2001", "E3004", "E4001", "E4002", "E9001", "invalid_token", "rate_limit_exceeded", "rate_limit_unavailable", "rate_limited", "NETWORK_ERROR", "INVALID_RESPONSE",] as const;
function safeCode(value: unknown): string { return KNOWN_CODES.find((code) => code === value) ?? "UNRECOGNIZED";}
function responseCode(payload: unknown): string { if (!payload || typeof payload !== "object" || Array.isArray(payload)) { return "UNRECOGNIZED"; } const error = (payload as Record<string, unknown>).error; const value = typeof error === "string" ? error : error && typeof error === "object" && !Array.isArray(error) ? (error as Record<string, unknown>).code : undefined; return safeCode(value);}
class AggregatorError extends Error { readonly httpStatus: number; readonly code: string;
constructor(httpStatus: number, code: string) { const status = Number.isInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599 ? httpStatus : 0; const diagnosticCode = safeCode(code); super(`Aggregator request failed (HTTP ${status}; code ${diagnosticCode})`); this.name = "AggregatorError"; this.httpStatus = status; this.code = diagnosticCode; }}
async function callAggregator(path: string, init?: RequestInit) { let res: Response; try { res = await fetch(`https://api.aggregator.gg/v1${path}`, { ...init, headers: { Authorization: `Bearer ${process.env.AGGREGATOR_API_KEY}`, ...init?.headers, }, }); } catch { throw new AggregatorError(0, "NETWORK_ERROR"); }
let payload: unknown; try { payload = await res.json(); } catch { throw new AggregatorError(res.status, "INVALID_RESPONSE"); } if (res.ok) return payload; throw new AggregatorError(res.status, responseCode(payload));}
// Use:try { await callAggregator("/games?per_page=1");} catch (e) { if (e instanceof AggregatorError) { // Log only these fields, never the response, request headers, or local variables. console.error("Aggregator request failed", { status: e.httpStatus, code: e.code }); if (e.code === "E3004") { // Player self-excluded: show the appropriate business message. } else { throw e; } } else { throw e; }}<?php
function safeCode(mixed $value): string { // Extend only with fixed catalogue codes your application handles. $knownCodes = [ 'E0001', 'E0002', 'E1002', 'E1053', 'E1054', 'E2001', 'E3004', 'E4001', 'E4002', 'E9001', 'invalid_token', 'rate_limit_exceeded', 'rate_limit_unavailable', 'rate_limited', 'NETWORK_ERROR', 'INVALID_RESPONSE', ]; foreach ($knownCodes as $code) { if ($code === $value) return $code; } return 'UNRECOGNIZED';}
function responseCode(mixed $payload): string { $error = is_array($payload) ? ($payload['error'] ?? null) : null; return safeCode(is_array($error) ? ($error['code'] ?? null) : $error);}
class AggregatorException extends RuntimeException { public readonly int $httpStatus; public readonly string $apiCode; // Exception::$code is reserved for an integer.
public function __construct(int $httpStatus, string $apiCode) { $this->httpStatus = $httpStatus >= 100 && $httpStatus <= 599 ? $httpStatus : 0; $this->apiCode = safeCode($apiCode); parent::__construct("Aggregator request failed (HTTP {$this->httpStatus}; code {$this->apiCode})"); }}
function callAggregator(string $path, array $opts = []): array { $ch = curl_init('https://api.aggregator.gg/v1' . $path); // array_replace preserves the integer cURL option keys. curl_setopt_array($ch, array_replace([ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => array_merge( ['Authorization: Bearer ' . getenv('AGGREGATOR_API_KEY')], $opts['headers'] ?? [], ), ], $opts['curl'] ?? [])); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch);
if ($body === false) throw new AggregatorException(0, 'NETWORK_ERROR'); $payload = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new AggregatorException($status, 'INVALID_RESPONSE'); } if ($status >= 200 && $status < 300) { if (!is_array($payload)) throw new AggregatorException($status, 'INVALID_RESPONSE'); return $payload; } throw new AggregatorException($status, responseCode($payload));}
// Use:try { callAggregator('/games?per_page=1');} catch (AggregatorException $e) { // Log only these fields, never the response, request headers, or local variables. error_log(json_encode(['status' => $e->httpStatus, 'code' => $e->apiCode])); if ($e->apiCode === 'E3004') { // Player self-excluded: show the appropriate business message. } else { throw $e; }}import osimport httpx
# Extend only with fixed catalogue codes your application handles.KNOWN_CODES = ( "E0001", "E0002", "E1002", "E1053", "E1054", "E2001", "E3004", "E4001", "E4002", "E9001", "invalid_token", "rate_limit_exceeded", "rate_limit_unavailable", "rate_limited", "NETWORK_ERROR", "INVALID_RESPONSE",)
def safe_code(value: object) -> str: if isinstance(value, str): return next((code for code in KNOWN_CODES if code == value), "UNRECOGNIZED") return "UNRECOGNIZED"
def response_code(payload: object) -> str: error = payload.get("error") if isinstance(payload, dict) else None return safe_code(error.get("code") if isinstance(error, dict) else error)
class AggregatorError(Exception): def __init__(self, http_status: int, code: str): self.http_status = http_status if type(http_status) is int and 100 <= http_status <= 599 else 0 self.code = safe_code(code) super().__init__(f"Aggregator request failed (HTTP {self.http_status}; code {self.code})")
def call_aggregator(path: str, method: str = "GET", **kwargs): headers = { "Authorization": f"Bearer {os.environ['AGGREGATOR_API_KEY']}", **kwargs.pop("headers", {}), } try: resp = httpx.request( method, f"https://api.aggregator.gg/v1{path}", headers=headers, timeout=10.0, **kwargs, ) except httpx.RequestError: raise AggregatorError(0, "NETWORK_ERROR") from None
try: payload = resp.json() except ValueError: raise AggregatorError(resp.status_code, "INVALID_RESPONSE") from None if resp.is_success: return payload raise AggregatorError(resp.status_code, response_code(payload))
# Use:try: call_aggregator("/games", params={"per_page": 1})except AggregatorError as e: # Log only these fields, never the response, request headers, or local variables. print("Aggregator request failed", {"status": e.http_status, "code": e.code}) if e.code == "E3004": # Player self-excluded: show the appropriate business message. pass else: raiseOther languages
Section titled “Other languages”For Java, Go, C#, Ruby, Kotlin, Rust — generate a client from the spec:
# Choose python, java, go, csharp, ruby, kotlin, rust, or another supported target.npx @openapitools/openapi-generator-cli generate \ -i https://docs.aggregator.gg/downloads/aggregator.openapi.yaml \ -g python \ -o ./aggregator-clientThe generator supports 30+ languages. Output is a typed client with request/response models matching the spec exactly.