Skip to content

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.

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`);

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.

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"),
);
}

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 bet with is_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_code and transaction_id identity 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.


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;
}
}

For Java, Go, C#, Ruby, Kotlin, Rust — generate a client from the spec:

Terminal window
# 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-client

The generator supports 30+ languages. Output is a typed client with request/response models matching the spec exactly.