Code samples
The five operations every operator integration needs, in the three languages that cover most incumbent B2B casino backend stacks: TypeScript, PHP, and Python. The request examples need configured credentials, enabled sample games and an approved test budget. Signature functions can be tested locally. Wallet settlement must be integrated with your application’s durable store and authorization model.
Sample catalogue
Section titled “Sample catalogue”| # | Operation | What it proves |
|---|---|---|
| 1 | List games | Auth works; the catalog is enabled for your key |
| 2 | Create a session | The launch path works, with idempotency |
| 3 | Verify a callback signature | Forged callbacks cannot move money |
| 4 | Handle a callback | The durable settlement boundary, debit, credit, decline |
| 5 | Structured error handling | Failures branch on stable codes, not messages |
All samples assume two environment variables: AGGREGATOR_API_KEY (your agg_ key) and AGGREGATOR_CALLBACK_SECRET (the HMAC secret you set under API -> Callbacks in the cabinet). Never hard-code either in source. The Python HTTP samples use httpx (pip install httpx). TypeScript samples use server-side Node.js 24+ with global fetch; PHP examples require PHP 8.1+ and cURL. The callback section describes the durable settlement boundary your application must supply.
Building on Node? The typed client in Node.js SDK wraps operations 1, 2, and 5 for you. Every other language follows the same HTTP contract documented on this page and Callbacks; Other languages shows how to generate a typed client from the published spec.
List games
Section titled “List games”The minimal “is auth working” call: one page of games for your enabled providers. These examples use an ordinary operator key. Platforms must first create a brand and add its selector only to the supported calls listed in Platform accounts.
const res = await fetch( "https://api.aggregator.gg/v1/games?per_page=10", { headers: { Authorization: `Bearer ${process.env.AGGREGATOR_API_KEY}` }, },);
if (!res.ok) { throw new Error(`list games failed: HTTP ${res.status}`);}
const { games, total } = await res.json();console.log(`Got ${games.length} of ${total} games`);<?php
$ch = curl_init('https://api.aggregator.gg/v1/games?per_page=10');curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('AGGREGATOR_API_KEY'), ],]);$body = curl_exec($ch);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("list games failed: HTTP $status");}
$payload = json_decode($body, true);printf("Got %d of %d games\n", count($payload['games']), $payload['total']);import osimport httpx
resp = httpx.get( "https://api.aggregator.gg/v1/games", params={"per_page": 10}, headers={"Authorization": f"Bearer {os.environ['AGGREGATOR_API_KEY']}"}, timeout=10.0,)if not resp.is_success: raise RuntimeError(f"Aggregator request failed: HTTP {resp.status_code}")payload = resp.json()print(f"Got {len(payload['games'])} of {payload['total']} games")Step check: the call prints a valid game list; an empty list requires a catalog/access check. A 401 here means the Bearer header or the key itself; nothing further will work until this does.
Create a session
Section titled “Create a session”Real-money game launch. The Idempotency-Key header is required. Replace the illustrative operation ID with a unique persisted ID per launch, not per spin; reuse it with the identical body only for retries. A session can contain many spins; Idempotency explains why and how to pick keys.
const playerId = "player_42";const operationId = "launch-operation-001";const idempotencyKey = `launch-${playerId}-${operationId}`; // stable across retries
const res = await fetch("https://api.aggregator.gg/v1/sessions", { method: "POST", headers: { Authorization: `Bearer ${process.env.AGGREGATOR_API_KEY}`, "Idempotency-Key": idempotencyKey, "Content-Type": "application/json", }, body: JSON.stringify({ // Aggregator.gg 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", }),});
if (!res.ok) { throw new Error(`create session failed: ${res.status}`);}
const { session_id, game_url } = await res.json();// Redirect the player to game_url promptly; keep the URL private and follow provider expiry/reuse rules.<?php
$playerId = 'player_42';$operationId = 'launch-operation-001';$idempotencyKey = "launch-{$playerId}-{$operationId}"; // stable across retries
$ch = curl_init('https://api.aggregator.gg/v1/sessions');curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('AGGREGATOR_API_KEY'), 'Idempotency-Key: ' . $idempotencyKey, 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ // Aggregator.gg 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', ]),]);$body = curl_exec($ch);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("create session failed: HTTP $status");}
$session = json_decode($body, true);// Redirect the player to $session['game_url'] promptly; keep the URL private and follow provider expiry/reuse rules.import osimport httpx
player_id = "player_42"operation_id = "launch-operation-001"idempotency_key = f"launch-{player_id}-{operation_id}" # stable across retries
resp = httpx.post( "https://api.aggregator.gg/v1/sessions", headers={ "Authorization": f"Bearer {os.environ['AGGREGATOR_API_KEY']}", "Idempotency-Key": idempotency_key, }, json={ # Aggregator.gg 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", }, timeout=10.0,)
if not resp.is_success: raise RuntimeError(f"Aggregator request failed: HTTP {resp.status_code}")
session = resp.json()# Redirect the player to session["game_url"] promptly; keep the URL private and follow provider expiry/reuse rules.Step check: run the snippet twice with the same key and body; the second response is identical and carries X-Idempotent-Replayed: true.
Verify a callback signature
Section titled “Verify a callback signature”The single highest-leverage snippet on this site. Copy verbatim; the full model and replay-protection reasoning are in Request signatures.
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, no timestamp.
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)Constant-time compare matters. All three samples use timingSafeEqual, hash_equals, or hmac.compare_digest; a plain == is a timing-attack vector.
Step check: use a synthetic signed fixture: the original verifies, while a changed body, malformed/non-ASCII header, trailing bytes or missing secret fails closed.
Handle a callback
Section titled “Handle a callback”Combine the signature verifier with your validated wallet and idempotency implementation. These are integration requirements, not a standalone wallet implementation. Follow the callback contract for request fields, responses and business declines.
- Verify the signature over raw bytes before parsing or changing money. Validate transaction fields and use fixed error codes; do not reflect request values into errors or logs.
- A paid bet debits the wallet; a free
betwithis_free: truedoes not debit the player’s real-money balance. Wins follow the free-round and cash-play rules. - For a refund, reverse only the recorded wallet debit of the supported original transaction once. A grant-funded bet with no real-money debit must not create a new real-money credit. Reconcile missing or ambiguous originals;
round_idalone does not identify a debit. - Atomically commit the wallet change and the stored original status and body under the trusted integration/organization context,
provider_codeandtransaction_idbefore returning success. The same transaction ID text from another integration, brand or provider is a different transaction. A duplicate replays the stored result 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.
Step check: exercise your implementation against the readiness check and the callback checks, including paid and free bets, wins, declines, scoped duplicate replay and refund reconciliation. Readiness is advisory and does not replace your own wallet, concurrency and recovery tests.
Structured error handling
Section titled “Structured error handling”Business errors use the envelope on Errors, while authentication failures use HTTP 401 with a flat body. Treat every response field as untrusted. These wrappers retain only HTTP status and a fixed allowlist of codes; they discard raw body, message and request headers. Unknown codes become UNRECOGNIZED. Do not log the original exception or attach it as a cause. Use synthetic failures to test diagnostics; do not send real credentials to an agent.
class AggregatorError extends Error { constructor(public httpStatus: number, public code: string) { super(`Aggregator request failed: HTTP ${httpStatus}`); }}
async function callAggregator(path: string) { let res: Response; try { res = await fetch(`https://api.aggregator.gg/v1${path}`, { headers: { Authorization: `Bearer ${process.env.AGGREGATOR_API_KEY}` }, }); } catch { throw new AggregatorError(0, "TRANSPORT_ERROR"); } if (res.ok) return res.json();
let code = res.status === 401 ? "AUTHENTICATION_FAILED" : "UNRECOGNIZED"; try { const payload = await res.json(); const candidate = payload?.error?.code; if (res.status !== 401 && ["E2001", "E2003", "E3004", "E4001", "E9001"].includes(candidate)) { code = candidate; } } catch { // Keep the fixed fallback; never relay parsing errors or the raw body. } throw new AggregatorError(res.status, code);}
try { await callAggregator("/games?per_page=1");} catch (e) { if (e instanceof AggregatorError) { console.error({ status: e.httpStatus, code: e.code }); // Branch on e.code === "E3004" to stop a self-excluded player's launch. } else { console.error("Aggregator request failed"); }}<?phpclass AggregatorException extends RuntimeException { public function __construct( public readonly int $httpStatus, public readonly string $apiCode, ) { parent::__construct("Aggregator request failed: HTTP $httpStatus"); }}
function callAggregator(string $path): array { $ch = curl_init('https://api.aggregator.gg/v1' . $path); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('AGGREGATOR_API_KEY')], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($body === false) { throw new AggregatorException(0, 'TRANSPORT_ERROR'); } $payload = json_decode($body, true); if ($status >= 200 && $status < 300) { if (!is_array($payload)) { throw new AggregatorException($status, 'INVALID_RESPONSE'); } return $payload; } $code = $status === 401 ? 'AUTHENTICATION_FAILED' : 'UNRECOGNIZED'; $err = is_array($payload) ? ($payload['error'] ?? null) : null; $candidate = is_array($err) ? ($err['code'] ?? null) : null; if ($status !== 401 && in_array($candidate, ['E2001', 'E2003', 'E3004', 'E4001', 'E9001'], true)) { $code = $candidate; } throw new AggregatorException($status, $code);}
try { callAggregator('/games?per_page=1');} catch (AggregatorException $e) { error_log("Aggregator request failed: HTTP {$e->httpStatus}, code {$e->apiCode}");} catch (Throwable $e) { error_log('Aggregator request failed');}import osimport httpx
class AggregatorError(Exception): def __init__(self, http_status: int, code: str): self.http_status = http_status self.code = code super().__init__(f"Aggregator request failed: HTTP {http_status}")
def call_aggregator(path: str) -> dict: try: resp = httpx.get( f"https://api.aggregator.gg/v1{path}", headers={"Authorization": f"Bearer {os.environ['AGGREGATOR_API_KEY']}"}, timeout=10.0, ) except httpx.RequestError: raise AggregatorError(0, "TRANSPORT_ERROR") from None if resp.is_success: return resp.json()
code = "AUTHENTICATION_FAILED" if resp.status_code == 401 else "UNRECOGNIZED" try: payload = resp.json() except ValueError: payload = None err = payload.get("error") if isinstance(payload, dict) else None candidate = err.get("code") if isinstance(err, dict) else None if resp.status_code != 401 and isinstance(candidate, str) and candidate in ( "E2001", "E2003", "E3004", "E4001", "E9001" ): code = candidate raise AggregatorError(resp.status_code, code)
try: call_aggregator("/games?per_page=1")except AggregatorError as e: print(f"Aggregator request failed: HTTP {e.http_status}, code {e.code}")except Exception: print("Aggregator request failed")Step check: mock 404/E2001, 401, malformed JSON, and a body/code/header containing a synthetic secret. Only status and recognized fixed codes should reach diagnostics; no secret, raw body, header or exception chain should appear. For TypeScript, the SDK already provides the typed error; apply the same diagnostic boundary.
Other languages
Section titled “Other languages”For Java, Go, C#, Ruby, Kotlin, or Rust, generate a typed client from the machine-readable OpenAPI spec this hub publishes at /openapi.yaml:
npx @openapitools/openapi-generator-cli generate \ -i https://hub.aggregator.gg/openapi.yaml \ -g java \ -o ./aggregator-clientSwap -g java for go, csharp, ruby, kotlin, rust, or any of the 30+ generators the tool supports. The output is a typed client with request and response models matching the spec exactly. The same file drives the API Reference section of this hub, so a generated client and the reference pages always describe the same contract.