Skip to content

Idempotency

Network failures between your backend and The Aggregator 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 solves this.

Send a unique Idempotency-Key for each logical operation on the endpoints that require it. Keep the same key and body for retries:

POST /v1/sessions HTTP/1.1
Authorization: Bearer YOUR_ISSUED_API_KEY
Idempotency-Key: 8c8e8c8e-8c8e-4c8e-8c8e-8c8e8c8e8c8e
Content-Type: application/json
{"game_id": "...", "player_id": "...", "balance": 10000, "currency": "EUR", "country": "DE"}

The endpoints have different replay contracts:

  • POST /v1/sessions: once a response is cached, the same operator account and key replay that response. The session path does not compare a later request body before replay; changing the body can still return the original session. Never use this as a body-conflict check. An in-flight request can return 409 idempotency_in_progress with Retry-After.
  • POST /v1/free-rounds: the grant record stores the key and request hash. The same key and body replay the recorded grant result; a different body returns 409 idempotency_conflict. The same body while issuance is still in flight can return 409 idempotency_in_progress.

After an ambiguous outcome, reconcile the original operation before starting another one. A new key starts a new logical operation and can create another session or grant.

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

For other operations, follow their own API contract rather than assuming every POST requires this header.

POST /v1/demo-sessions does not move money and does not currently require Idempotency-Key.

Idempotency on GET is intrinsic (queries don’t mutate); the header has no effect there.

  • Recommended: UUID v4, generated once and persisted with the operation
  • For compatibility with the published OpenAPI profile, use no more than 128 ASCII letters, digits, dashes or underscores. This is a portable client profile, not a claim that every deployed endpoint enforces the same validation limit.
  • Never reuse keys across logically different operations. One key = one logical request.

Use identifiers from your own persisted operation state; define playerId and launchAttemptId before constructing the key:

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

Stable across retries of the same launch, unique across different logical launches. This outbound request key is separate from the scoped identity used to deduplicate inbound wallet callbacks.

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

Generated once when you start the operation, persisted in your DB, reused on every retry until the operation succeeds.

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

Two retries 100ms apart get different keys. You lose the protection.

The session path includes X-Idempotent-Replayed: true when serving a cached response and preserves its original status and body. For example, a replay of a successful session creation retains 201:

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

The free-round issuance path returns 200 with _replayed: true in the recorded grant body on a completed replay. Do not require the session replay header on that endpoint.

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. Do not use expiry as permission to repeat an operation whose outcome is still ambiguous.

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

Do not omit Idempotency-Key where the API operation requires it; a caller-side UPSERT does not remove that requirement. For operations that do not require the header, follow their documented contract. Inbound wallet callbacks use a separate scoped transaction identity, not this outbound request header.