Skip to content

Open app

Idempotency

Network failures between your backend and Aggregator.gg are inevitable. Without idempotency, a retry after a timeout can charge the same player twice or open the same session twice. The Idempotency-Key header removes that risk.

The machine contract requires an Idempotency-Key header on POST /v1/sessions and POST /v1/free-rounds. Use one unique, persisted key for each logical request:

POST /v1/sessions HTTP/1.1
Authorization: Bearer $AGGREGATOR_API_KEY
Idempotency-Key: 8c8e8c8e-8c8e-4c8e-8c8e-8c8e8c8e8c8e
Content-Type: application/json
{"game_id": "...", "player_id": "...", "balance": 10000, "currency": "EUR", "country": "DE"}
  • First request: processed normally, response cached.
  • Retried request with the same key and the same body: the cached response is returned; no duplicate work happens.
  • Same key while the first request is still running: 409 Conflict with {"error": "idempotency_in_progress"}. Wait and retry; this is the guard doing its job, not a failure.

Reusing a key with a different body is the case to design against, and the two endpoints answer it differently. POST /v1/free-rounds stores a hash of the request body and rejects the mismatch with 409 Conflict and {"error": "idempotency_conflict"}. POST /v1/sessions does not compare bodies at all: it returns the first request’s cached response, and your new body is never processed. Nothing warns you.

Write your client so it never depends on which of the two you get. One key means one logical request, forever; treat a reused key as a bug in your own code rather than something the API will catch for you.

This header protects your requests to the API. Inbound wallet callbacks use a separate identity: the trusted integration/organization context, provider_code and transaction_id. Atomically store the wallet result with its original status and body under that identity and replay it for duplicates, as described in Callbacks.

Where the header is required:

  • POST /v1/sessions
  • POST /v1/free-rounds

Both reject a request without it with 400 and {"error": "missing_idempotency_key"}. POST /v1/demo-sessions moves no money and does not currently require the header. GET operations do not require this header. Do not infer absence of all side effects: a platform catalog call with an unknown brand ref can trigger lazy provisioning where allowed. Register brands deliberately first.

Key format:

  • Recommended: UUID v4
  • Maximum length: 256 characters. A longer value is rejected with 400 and {"error": "invalid_idempotency_key"}
  • Character set: the platform accepts any non-empty value and does not validate the alphabet. Keep to letters, digits, dashes and underscores anyway, so your keys stay safe to log, index, and paste into a support ticket
  • Never reuse a key across logically different operations. One key means one logical request.

The session path replays the original response with its original status code and adds a response header. A replayed successful POST /v1/sessions therefore answers 201 again:

HTTP/1.1 201 Created
Content-Type: application/json
X-Idempotent-Replayed: true
{"session_id":"...","game_url":"..."}

One surface differs: POST /v1/free-rounds replays a duplicate grant as 200 with a "_replayed": true marker in the body instead of the header. See Free rounds.

Step check: send the same POST /v1/sessions twice with the same key and body. The second response is 201, carries the same stored response payload as the first, and carries X-Idempotent-Replayed: true. Then send the same key with a different body and confirm you get that same replayed first response back rather than a new session: on this endpoint that is what key reuse looks like, and your key discipline is the only thing standing between it and a wrong session.

Stable per operation:

const idempotencyKey = `launch-${playerId}-${launchOperationId}`;

Stable across retries of the same launch, unique across different launches; a session can contain many spins.

Generated once, persisted, reused on retry:

const idempotencyKey = crypto.randomUUID();
// Store this in your own state so retries reuse it

Generate the key when you start the operation, persist it in your database, and reuse it on every retry until the operation succeeds. A key that lives only in memory dies with the process that was retrying.

The anti-pattern is any key derived from the current time:

// DO NOT do this
const idempotencyKey = `bet-${Date.now()}`;

Two retries 100 ms apart get different keys, and the protection is gone.

Local database idempotency does not remove a required API header. Always send Idempotency-Key on POST /v1/sessions and POST /v1/free-rounds, even when your own storage also prevents duplicates. Follow each other operation’s documented requirements; reads do not need this header.

The session path caches a completed response for 24 hours when it stores that response. After expiry, the same key can be processed as a new request. Reconcile an ambiguous outcome before creating another real operation.

Free-round issuance uses its durable grant record; the session cache’s expiry is not the grant contract. Keep the original key and body and reconcile the recorded grant before requesting another one.

The in-progress session guard is a short lease, separate from the completed-response cache. A guard failure or an expired lease does not prove the provider did nothing. Preserve the operation key and body, use bounded retries, and reconcile before creating another launch.

Replay does not bypass current authentication, provider approval or brand lifecycle checks. Access changes can refuse a replay; retain the original operation identity and reconcile its recorded outcome rather than treating that refusal as proof no operation occurred.