Skip to content

Wallet Callback Integration Guide

  1. A player opens a game through your platform via The Aggregator.
  2. The game provider sends a callback to The Aggregator at POST /callbacks/{provider_code}/{action}.
  3. The Aggregator verifies the provider’s signature, records the transaction, and forwards a normalized payload to your configured callback_url.
  4. Your server processes the wallet event (debit or credit the player’s balance) and returns a JSON response.
Player -> Game Provider -> The Aggregator -> Your Server
|
<- response <--------+

The Aggregator acts as a normalizer: regardless of which game provider originates the event, you always receive the same payload shape and signing scheme.


The Aggregator sends a POST request to your callback_url with the following format.

Header Value
Content-Type application/json
X-SIGNATURE HMAC-SHA256 hex digest of the raw JSON body, keyed with your callback_secret
{
"action": "BET-WIN",
"transaction_type": "bet",
"is_free": false,
"free_round_grant_id": "",
"transaction_id": "provider-unique-tx-id",
"amount": 150,
"currency": "EUR",
"player_id": "player-123",
"provider_code": "truelabs",
"game": "gates-of-olympus",
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"round_id": "round-123",
"finished": false,
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
Field Type Description
action string Provider-specific action label (e.g. "BET-WIN", "BET", "WIN"). Use transaction_type for branching logic.
transaction_type string One of "bet", "win", or "refund". This is the field you use to decide how to update the player’s balance.
is_free boolean true when this movement belongs to a platform-issued free round. A free bet is grant-funded — do not debit the player’s real-money balance for it. See the Free Rounds guide. May be absent on some provider legs — treat a missing value as false.
free_round_grant_id string The free-round grant this movement belongs to; empty string for normal real-money play. May be absent on some provider legs — treat a missing value as empty.
transaction_id string Provider-unique opaque identifier. Combine it with provider_code, scoped to your integration/organization, for wallet idempotency.
amount integer Transaction amount in minor units (e.g. 150 = 1.50 EUR). A documented zero-win settlement may have amount: 0; validate according to the transaction and provider contract. Same unit as the response balance.
currency string ISO 4217 currency code (e.g. "EUR", "USD").
player_id string The player identifier you provided when launching the game session.
provider_code string Identifies the game provider (e.g. "truelabs", "bgaming").
game string Game slug or name from the provider.
game_id string UUID of the game in The Aggregator’s catalog.
round_id string Groups related transactions into a single game round.
finished boolean true on the final callback of a round.
session_id string UUID of the game session in The Aggregator.

Every forwarded callback is signed with your callback_secret using HMAC-SHA256. You must verify the signature before processing any callback.

  1. Read the raw request body as bytes. Do not parse or re-serialize it first.
  2. Compute the HMAC-SHA256 of those bytes using your callback_secret as the key.
  3. Compare the resulting hex digest with the value in the X-SIGNATURE header using a constant-time comparison function.
expected = HMAC-SHA256(raw_body_bytes, callback_secret)
valid = constant_time_compare(expected, request.headers["X-SIGNATURE"])

Important: The body is serialized with compact JSON (no spaces after separators). If you re-serialize the parsed JSON to verify the signature, the result will not match. Always use the raw bytes.


When your server accepts a transaction, return HTTP 200 with a JSON body containing the player’s updated balance:

{
"balance": 9850,
"currency": "EUR",
"player_id": "player-123"
}
Field Type Description
balance integer Required. Player’s balance after applying the transaction, in minor units (cents) — the same unit as the request amount.
currency string Required. ISO 4217 currency code. Must match the request currency.
player_id string Optional, recommended: echo back the player_id from the request. The platform validates only balance + currency.

A non-2xx status or non-JSON body does not by itself establish whether a delivery will be retried. Business declines follow the contract below; other failures and ambiguous outcomes follow the delivery path and deployed policy described under Retry behaviour.

Use the response status to distinguish an accepted transaction, a business refusal, and a technical failure. Always return JSON.

Outcome HTTP status JSON response
Transaction accepted 200 Updated balance and currency
Player cannot afford a bet 402, or 200 with body status: 402 Current balance and currency; status: 402 is required for the HTTP 200 form
Definite transaction-state refusal 400 status: 400, machine-readable error, current balance and currency
Transient server failure 5xx Error details without secrets; see Retry behaviour

When the player cannot afford a bet, decline it explicitly — this is a normal business outcome, not a server error:

  • HTTP 402, or
  • HTTP 200 with a body-level status marker: { "status": 402, "balance": <current>, "currency": "EUR" }.

Reserve this insufficient-funds response for an unaffordable bet. Do not use 402 to label a refused win or refund, or a round-state problem. Never answer a business refusal with a 5xx — use 5xx for transient server failures.

Refusing a transaction because of its state

Section titled “Refusing a transaction because of its state”

If your wallet definitively refuses a transaction under its round rules, return HTTP 400 with a matching body-level status: 400. For example:

{
"status": 400,
"error": "ROUND_CLOSED",
"balance": 9850,
"currency": "EUR"
}

Include the machine-readable reason, such as ROUND_CLOSED or ROUND_NOT_FOUND, and the player’s current balance and currency. This classification does not change which refunds your wallet must accept under its agreed round rules. Do not return HTTP 200 with status: 400: the HTTP success status can be treated as an accepted transaction. HTTP 409 is also not a substitute for this definite business refusal.

The wallet response describes your wallet’s decision. Provider-facing error mapping, reconciliation and retry behavior depend on the provider integration; returning a refusal does not by itself select a new automatic retry policy.


For a paid bet (is_free absent or false), debit the player’s balance by amount after validation. amount is already in minor units; no conversion is needed. For a grant-funded bet with is_free: true, do not debit the player’s real-money balance; follow the free-round rules. Commit the result atomically with the idempotency record as described below.

The player wins. Credit the player’s balance by amount. amount is already in minor units, so add it directly — no conversion.

player.balance += callback.amount // integer minor units

On a supported refund path, reconcile the refund with the original bet and reverse only its recorded wallet debit, once. Use the provider’s documented original-transaction identity and your ledger; round_id alone does not prove which movement should be reversed. A grant-funded free bet that did not debit the player’s wallet must not create a new real-money credit when reversed.

If the original outcome is missing or ambiguous, use the documented reconciliation procedure before changing the wallet. Do not infer a debit from the refund’s arrival or silently discard an unresolved financial event. Return the documented response for the resolved outcome and retain it for duplicate replay.


A paid round can follow this representative sequence; the provider contract determines the actual legs:

bet (finished=false)
-> win (finished=false) # optional, may repeat
-> win (finished=true) # final callback closes the round
  • In this representative paid-round flow, a bet callback with finished=false opens the round. Supported free-round paths may settle with a win alone, including amount: 0; see Provider differences.
  • Zero or more win callbacks follow within the same round_id.
  • The final callback in a round has finished=true.
  • If a round is cancelled, a refund callback reverses the original bet.

You can use round_id + finished to track round state on your side, but it is not required. Process callbacks using the scoped transaction identity described under Idempotency; a bare provider transaction ID is not globally unique.


The Aggregator enforces idempotency on its side using a body hash and signature. If a duplicate callback arrives from the provider, The Aggregator replays the cached response from your server instead of forwarding the request again.

You must also implement idempotency on your side. Use the combination of integration/organization, provider_code and transaction_id as the wallet idempotency identity. Two providers can send the same opaque transaction ID; their events must not share a stored result:

  1. Before processing a callback, look up the full scoped identity and check whether that transaction is already processed.
  2. If yes, return the same response you returned the first time. Do not debit or credit the player again.
  3. If no, durably commit the wallet change and its result keyed by that full scoped identity before returning success. Store them together so a retry cannot apply the change twice.

This protects against network-level retries and edge cases where The Aggregator’s deduplication does not cover (e.g., retry queue deliveries).


Retry eligibility, delays and attempt budgets depend on the delivery path and deployed policy. Where queued retries are enabled, eligible connection errors, timeouts or server failures may be retried. Do not assume every failed callback is queued or every path shares the same retry budget.

  • Definite business refusals (400 for transaction state, 402 for an unaffordable bet) are distinct from transient failures. Follow the provider integration’s reconciliation policy before replaying a refused transaction; do not assume every non-2xx response triggers a retry.
  • Confirm which failures are retryable for your integration; a client or business error may require correcting the request instead.
  • Confirm how exhausted or permanently failed deliveries are recorded and whether a controlled replay is available. Do not assume manual replay exists for every path.
  • After a timeout or recovery, reconcile transaction outcomes before requesting a replay: the wallet change may already have committed even when its response was not received.

Implication for your handler: make it idempotent. The same scoped transaction may arrive more than once and you must return its stored result on every duplicate invocation without double-debiting or double-crediting. See Idempotency.

Aim for under 2 seconds at p95 and well under 5 seconds at the worst case. Verify the signature and validate the request, then durably commit the wallet change and its idempotent result before returning a successful response with the resulting balance. Do not acknowledge success while settlement is pending. A duplicate transaction must replay its stored result. If processing cannot complete, return the documented failure response. Nonessential work such as analytics may run asynchronously after settlement.


Backpressure, pause/probe behavior and recovery controls depend on the eligible delivery path and deployed policy. Confirm the behavior configured for your integration; a pause does not establish that every pending transaction has been queued or will be delivered automatically.

After recovery, verify delivery status and reconcile your wallet ledger against the recorded transactions. Investigate missing or unresolved outcomes with your support contact and use only the replay procedure available for that path. Do not assume lossless delivery or that recovery needs no intervention.


Configure your callback_url and separate callback_secret through the operator cabinet setup wizard where your deployment exposes it, or confirm the available configuration procedure with your support contact.

  • callback_url: The HTTPS endpoint on your server that receives wallet callbacks. Must be publicly reachable. Internal/private IP addresses are blocked (SSRF protection).
  • callback_secret: A shared secret used to sign forwarded payloads. Encrypted at rest with AES-256-GCM. Treat it like a password – never log it, never commit it to version control.

Use the signature verification snippets for TypeScript, PHP and Python, and the callback integration requirements for the framework boundary. A signature verifier is one part of the endpoint; it does not implement wallet settlement.

On a free bet with is_free: true, do not debit the player’s real-money balance. Follow the free-round rules and the request and response contracts on this page.

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. A duplicate replays the stored original status and body without another wallet movement. Validate these guarantees in your own ledger implementation before handling customer traffic.

For diagnostic handling, use the fixed-code error guidance. Keep raw callback bodies, signature headers and secrets out of errors and logs.


Symptom: Your signature verification fails on every callback.

Common causes:

  1. Re-serialized body. You parsed the JSON and then re-serialized it before computing the HMAC. The Aggregator uses compact JSON (json.dumps(data, separators=(",", ":"))) with no whitespace. Always verify against the raw request bytes.
  2. Wrong secret. You are using the API key instead of the callback_secret. These are different values.
  3. Encoding mismatch. Your HMAC key must be the callback_secret string encoded as UTF-8 bytes. The body is raw bytes as received over the wire.
  4. Middleware altering the body. A proxy, WAF, or framework middleware modified the request body before your handler received it. Ensure you read the raw, unmodified body.

Debug steps:

Verify the captured raw body bytes and X-SIGNATURE with the in-process signature verifier, loading AGGREGATOR_CALLBACK_SECRET from protected environment configuration. Do not place the secret in command arguments, shell history or diagnostic logs. Record only whether verification passed; keep the captured body and header out of shared diagnostics.

Symptom: The Aggregator logs show Failed to reach operator or your callbacks are being retried.

Common causes:

  1. Slow database queries. Your handler takes longer than 5 seconds to respond. Optimize the durable settlement path and move only nonessential work after it. Never return success before the wallet change and idempotent result are committed.
  2. Firewall blocking inbound requests. The Aggregator’s IP addresses must be allowed through your firewall. Contact support for the current IP list.
  3. DNS resolution failure. Your callback_url hostname is not resolvable from The Aggregator’s network.

Symptom: The Aggregator treats your response as a failure even though you return HTTP 200.

Common causes:

  1. Non-JSON response body. Your server returns HTML (e.g., a framework error page) instead of JSON. Ensure your endpoint always returns Content-Type: application/json.
  2. Missing balance field. The response must include balance as an integer in minor units (cents). If you return the balance as a decimal string or omit it, the round may not complete correctly.

Symptom: You receive the same transaction_id more than once.

This is expected behavior during retries. Implement idempotency using the full integration/organization, provider and transaction identity described in the Idempotency section. Return the same response you returned the first time – do not process the transaction again.

Symptom: Callbacks stop arriving for a period, then resume with a burst of retries.

Sustained failures may trigger backpressure on an eligible delivery path. Confirm the actual cause and deployed recovery policy — see Backpressure. Do not infer automatic redelivery from the endpoint becoming healthy.

Action: Check safe server diagnostics for the root cause, fix the underlying issue, then verify delivery status and reconcile transaction outcomes. Use the supported recovery or replay procedure for unresolved deliveries.


Complete every item with authorized validation traffic and the applicable launch requirements before opening customer traffic.

  • Your callback_url is a publicly reachable HTTPS endpoint (not HTTP, not a private IP).
  • Your endpoint responds within 5 seconds under normal load.
  • Successful bet, win and refund callbacks return HTTP 200 with a valid JSON body only after the wallet change and idempotent result are durably committed.
  • You verify the X-SIGNATURE header on every request using HMAC-SHA256 with constant-time comparison.
  • You reject requests with a missing or invalid signature.
  • Your callback_secret is stored securely and not logged or exposed in error messages.
  • You store results by integration/organization, provider_code and transaction_id, and replay them only for duplicates of that full identity.
  • You do not debit or credit the player balance more than once for the same scoped transaction identity.
  • Paid bet callbacks debit the player balance; grant-funded bets with is_free: true do not debit real money.
  • win callbacks credit the player balance.
  • Supported refund callbacks reverse only an original recorded wallet debit, once; free bets without a debit do not create a real-money credit.
  • You read the request amount as an integer in minor units (cents) — the same unit as balance — and apply it without any major/minor conversion.
  • The balance in your response is an integer in minor units (cents).
  • You decline unaffordable bet callbacks with HTTP 402 (or 200 + body "status": 402) — never a 5xx. See Declining a bet.
  • Your endpoint always returns JSON, even on errors (no HTML error pages).
  • Definite transaction-state refusals use HTTP 400 and body status: 400, with the machine-readable error, current balance and currency. Only an unaffordable bet uses the insufficient-funds 402 response.
  • You use the documented refusal forms for business outcomes and reserve 5xx for genuine server failures.
  • You have alerting on server failures and understand the recovery controls enabled for your delivery path.
  • You have tested the full round lifecycle: bet -> win (with finished=true).
  • You have tested refund handling.
  • You have tested duplicate handling within the same provider/integration and distinct handling when another provider uses the same transaction_id.
  • You have tested signature verification with a known body + secret pair.
  • You have tested your endpoint under the 5-second timeout constraint.