Seamless Wallet Callbacks
The operator's wallet is the source of truth for player balances. Whenever money moves, the platform calls four wallet endpoints implemented by the operator over server-to-server HTTP. This chapter describes the side the operator must implement, so please follow every idempotency, retry, and ordering rule without exception.
Overview
- The platform does not hold player money. It calls debit (withdrawal) when a player bets or takes a seat, credit (deposit) on settlement or when a player leaves a seat, rollback (debit cancellation) on cancellation, and balance when it needs to show the balance on screen.
- Calls are made in the background from the platform servers, independently of the player's screen. Credits and rollbacks still arrive after the player has closed the window or logged out of the operator site.
- Every transaction carries a
tx_idgenerated by the platform. The operator must usetx_idfor duplicate handling (idempotency). - The platform records the transaction in its own ledger before making the call. As a result, a transaction is never lost even if the response is cut off; it is finalized (retried or cancelled) according to the rules below.
The Four Callbacks
| Endpoint | When | What it does | Retry |
|---|---|---|---|
POST {wallet_base_url}/balance | Entering the game screen, refreshing the balance | Returns the current balance | No |
POST {wallet_base_url}/debit | Bets (bet, ante, double, split), taking a seat (buy_in), adding chips (top_up) | Deducts from the balance | No — if the outcome is uncertain, it is treated as a failure and rolled back |
POST {wallet_base_url}/credit | Round settlement (settle, amount: 0 even on a loss), leaving a seat (cash_out) | Adds to the balance | Until it succeeds |
POST {wallet_base_url}/rollback | Bet cancellation (cancel_bet), voided round (round_void), uncertain debit (timeout), cleanup of unfinished debits (system) | Refunds the original debit amount | Until it succeeds |
Callback URL
When applying for integration, the operator registers a single wallet base URL (wallet_base_url). The platform makes calls by appending / and the endpoint name to it.
Registered wallet_base_url | Actual call | Path used for signing |
|---|---|---|
https://api.example-casino.com/wallet | POST https://api.example-casino.com/wallet/debit | /wallet/debit |
https://wallet.example-casino.com | POST https://wallet.example-casino.com/credit | /credit |
- All four endpoints use the
POSTmethod. - A trailing
/on the registered URL is removed when it is stored. Do not append a query string. - Calls go out directly without a proxy. You may need to allow the platform's outbound IPs in the operator firewall.
To be agreed
The platform's outbound IPs for wallet callbacks and the HTTPS requirements for the wallet URL are agreed during the integration application. Use HTTPS for any wallet URL that is reachable over the public internet.
Request Headers and Signature
Wallet callbacks use the same HMAC-SHA256 signing rules as the Operator API. For how the string to sign is built and for sample code, see Authentication and Signing.
| Header | Value |
|---|---|
Content-Type | application/json; charset=utf-8 |
X-Operator | Operator code (e.g. acme) |
X-Timestamp | Time of sending, Unix epoch milliseconds (e.g. 1792547400123) |
X-Nonce | A new 24-character random string for each request (upper- and lowercase letters, digits, -, _) |
X-Signature | Lowercase hex HMAC-SHA256 signature (key = api_secret) |
- The platform does not send
X-Api-Keywith wallet callbacks. - The path that is signed is the path portion of the called URL (the "Path used for signing" in the table above), and the body is the raw bytes exactly as received. If you parse the JSON and re-serialize it before comparing signatures, the comparison will fail.
- When the same transaction is retried, the body is byte-for-byte identical to the original, but
X-Timestamp,X-Nonce, andX-Signatureare generated anew each time.
The operator should check the following:
X-Operatoris its own operator codeX-Timestampis within ±5 minutes of the current time- The signature is valid (constant-time comparison)
- The same
X-Noncehas not been seen again within 5 minutes — if you run multiple servers, record nonces in a shared store (Redis, etc.)
If verification fails, respond with HTTP 200 + {"status":"INVALID_SIGNATURE"}.
Persistent signature failures lock up money
A debit ends as a rejection when it receives INVALID_SIGNATURE, but credits and rollbacks keep being retried until they receive OK. If verification keeps failing because of a secret rotation or server clock drift, winnings and refunds will not reach players in the meantime. Synchronize your server clocks with NTP, and coordinate the timing with the platform when you rotate the secret.
Request Body
The body is single-line JSON with no whitespace. Field order is not fixed, so do not depend on it. Debit, credit, and rollback share the same skeleton, and each type adds one extra field.
| Field | Type | balance | debit | credit | rollback | Description |
|---|---|---|---|---|---|---|
tx_id | string | ● | ● | ● | Platform transaction ID (tx_ + 32 hex digits). Idempotency key | |
external_id | string | ● | ● | ● | ● | Operator-side player ID (the value sent at game launch) |
player_id | string | null | ● | ● | ● | ● | Platform player ID (pl_…), for reference. null in the balance connection test |
currency | string | ● | ● | ● | ● | Currency code (e.g. PHP). A player's currency is fixed at the first launch and never changes |
operator_session_id | string | null | ● | ● | ● | ● | The value sent at game launch. null if none was sent; may be null on platform recovery paths |
amount | integer | ● | ● | ● | Integer in the currency's minor unit. 1 or more for debit and rollback, 0 or more for credit | |
reason | string | ● | ● | ● | Transaction reason. See the table on each endpoint's page | |
game_code | string | ● | ● | ● | Game code (e.g. live_baccarat) | |
table_id | string | ● | ● | ● | Table ID (tbl_…) | |
round_id | string | ● | ● | ● | Grouping ID. A round (rnd_…) for bet-flow games, a table session (tss_…) for buy-in-flow games | |
created_at | string | ● | ● | ● | Time the platform created the transaction, UTC with milliseconds (2026-10-19T02:10:00.123Z). Does not change on retries | |
round_closed | boolean | ● | Whether this credit ends the player's round (or table session) | |||
ref_tx_id | string | ● | The tx_id of the debit to cancel |
The prefix of round_id is determined by reason. buy_in, top_up, and cash_out use tss_…; bet, ante, double, split, settle, cancel_bet, and round_void use rnd_…. timeout and system rollbacks use the same prefix as the original debit.
Response
Always return the business outcome with HTTP 200, distinguished by the status in the body. The platform reads only the three fields below and ignores everything else. Only the first 64 KB of the response body is read.
{ "status": "OK", "balance": 1250000, "operator_tx_id": "w-99812" }| Field | Type | Description |
|---|---|---|
status | string | Result code. One of the values in the table below (required) |
balance | integer | Player balance after processing (integer in the minor unit). Always include it with OK — the platform immediately shows this value on the player's screen |
operator_tx_id | string | Operator-side transaction ID. Stored together in the platform ledger and used for transaction lookup and reconciliation |
status values
| status | Meaning | debit | credit / rollback | balance |
|---|---|---|---|---|
OK | Processed (for a repeated request with the same tx_id, the original result) | Confirmed | Confirmed | Balance shown |
INSUFFICIENT_FUNDS | Insufficient balance | Definitive rejection | Retried | Treated as failure |
PLAYER_NOT_FOUND | Player does not exist | Definitive rejection | Retried | Treated as failure |
PLAYER_BLOCKED | Player is blocked or self-excluded | Definitive rejection | Retried | Treated as failure |
LIMIT_EXCEEDED | Operator's responsible gambling limit exceeded | Definitive rejection | Retried | Treated as failure |
CURRENCY_MISMATCH | Request currency differs from the player's currency | Definitive rejection | Retried | Treated as failure |
INVALID_SIGNATURE | Signature verification failed | Definitive rejection | Retried | Treated as failure |
TX_ALREADY_ROLLED_BACK | A debit arrived late for a tx_id that has already been rolled back | Definitive rejection | Retried | Treated as failure |
INVALID_REQUEST | Missing fields or malformed values | Definitive rejection | Retried | Treated as failure |
UNKNOWN_ERROR | Any other error (unknown whether it was processed) | Uncertain outcome → treated as failure + immediate rollback | Retried | Treated as failure |
- Definitive rejection: The platform considers the debit not applied and rejects the bet or seat. No rollback is sent. Therefore, when you return a rejection status, you must not have touched the balance.
- Uncertain outcome: A non-HTTP-200 response, a timeout, a connection failure, a non-JSON body, or a response with an empty
statusis treated exactly likeUNKNOWN_ERROR. - Retried: Credits and rollbacks are resent whenever the response is not
OK, regardless of the status. For credits and rollbacks, there is no terminating response other thanOK. - Treated as failure (balance): The platform uses the last known balance (a value from the last 10 seconds); if none is available, it cannot display the balance.
Do not use statuses that are not in the list
If a debit returns a value not in the table above (e.g. "ERROR"), the platform treats it as a definitive rejection and does not send a rollback. For errors where you are not sure whether the deduction happened, always respond with UNKNOWN_ERROR or HTTP 5xx. That way the platform cleans it up with a rollback.
Idempotency
Because the platform derives tx_id deterministically from the transaction contents, the same transaction always arrives with the same tx_id, no matter how many times it is resent.
- When the same
tx_idarrives again, do not apply it again; return the original response as is. This applies not only to successes but also to rejections (INSUFFICIENT_FUNDS, etc.): return the original result. - The platform never reuses a
tx_idfor a different amount, a different player, or a different reason. If the operator receives a request with the sametx_idbut different contents, reject it withINVALID_REQUESTand notify the platform. tx_iduniqueness spans all endpoint types. Thetx_ids of debits, credits, and rollbacks never overlap.- The
tx_idof the rollback for a given debit is fixed and unique. The same debit is never cancelled twice under different reasons.
Timeouts and Retries
These are the platform's default settings.
| Call | Response wait | On failure or uncertainty |
|---|---|---|
| balance | 3 seconds | Not retried. Shows the balance known within the last 10 seconds |
| debit | 3 seconds | Not retried. Treated as a failure, and a rollback for the same amount (reason: "timeout") is sent immediately |
| credit | 5 seconds | Retried until it succeeds |
| rollback | 5 seconds | Retried until it succeeds |
- Connection establishment must complete within 2 seconds and the TLS handshake within 3 seconds. Connections are reused with keep-alive, and HTTP/2 is attempted.
- Credit and rollback retry intervals: 1 s → 2 s → 5 s → 15 s → 1 min → 5 min, then every 5 minutes indefinitely. Transactions that remain unfinished for more than 24 hours are flagged for manual review by platform operations.
- If a platform server stops in the middle of a call, the remaining credits and rollbacks are resumed after about 1 minute, and debits whose result was not received are cleaned up after 30 seconds with a
reason: "system"rollback. - In live games, debits cluster right before betting closes. Because a debit fails once it exceeds 3 seconds, the wallet must respond fast enough even under peak load.
To be agreed
The wallet response time target (SLA) and throughput are agreed at contract time.
Ordering and Concurrency
- The platform queues new calls for the same player and sends them in order. Two debits for the same player are never sent concurrently. Calls for different players run in parallel.
- However, retries do not guarantee ordering. A credit being retried may arrive later than a debit created after it.
- If a debit exceeds 3 seconds, the platform sends a rollback without waiting for the response. On the operator side, a debit that is still being processed may overlap with its rollback, or the rollback may be processed before the debit. → rollback
Processing debits, credits, and rollbacks inside a per-player lock (e.g. SELECT … FOR UPDATE on the player's balance row) lets you handle these cases safely.
Call Sequence Examples
Bet-flow games (Baccarat, Blackjack, Speed Hold'em)
Each round may receive several debits, and the settlement credit arrives once per player who still has bets in the round. On a loss it is amount: 0.
Round rnd_A — normal
debit tx_1 5,000 reason=bet
debit tx_2 10,000 reason=bet
credit tx_3 30,000 reason=settle round_closed=true (total payout including returned stake)
Round rnd_B — one bet cancelled
debit tx_4 5,000 reason=bet
debit tx_5 5,000 reason=bet
rollback tx_6 5,000 reason=cancel_bet ref_tx_id=tx_5
credit tx_7 0 reason=settle round_closed=true (loss)
Round rnd_C — round voided
debit tx_8 5,000 reason=bet
rollback tx_9 5,000 reason=round_void ref_tx_id=tx_8 (no credit is sent)
Round rnd_D — delayed debit response
debit tx_10 5,000 reason=bet ── no response within 3 seconds → platform rejects the bet
rollback tx_11 5,000 reason=timeout ref_tx_id=tx_10Buy-in-flow games (Texas Hold'em, Pot-Limit Omaha, Live Hold'em, Matgo, Go-Stop, Seotda, Tong-its, Red Dog)
The wallet is not called for each hand. Chips move only within the table: a debit when the player sits down and a credit when the player stands up. round_id contains the table session ID (tss_…).
Table session tss_B — from sitting down to leaving
debit tx_1 100,000 reason=buy_in
debit tx_2 50,000 reason=top_up
credit tx_3 182,000 reason=cash_out round_closed=true (all remaining chips, 0 if everything was lost)- If a hand is voided, the chips return to the seat, so there is no wallet call.
- Rake (commission) is not a wallet transaction. It is aggregated separately in the GGR report.
Prepaid Credit and Wallet Callbacks
For operators whose operating mode is prepaid (prepaid mode), prepaid credit moves together with the wallet callbacks, before and after them. This does not apply to independent mode.
| Moment | Prepaid credit | Wallet callback |
|---|---|---|
| Just before a debit | The debit amount is deducted first | If prepaid credit is insufficient, no debit is sent and the bet or seat is rejected (player screen error OPERATOR_CREDIT_EXHAUSTED) |
| Debit definitively rejected | The deducted amount is restored | — |
| Debit uncertain | The deducted amount is restored when the rollback completes with OK | Rollback sent |
Rollback OK | The amount deducted for the original debit is restored | — |
Credit OK | The credit amount is added (no change if 0) | — |
A bet rejected for insufficient prepaid credit leaves no call at all in the wallet. Check the prepaid credit balance with GET /credit.
Implementation Recommendations for Operators
- Keep a transaction record table with
tx_idas the primary key. Store the processing result (the response JSON) with it, and return it unchanged for repeated requests with the sametx_id. - Keep rollback markers (tombstones). If a rollback arrives and the original debit does not exist, record
ref_tx_idas "rolled back", and if that debit arrives later, reject it withTX_ALREADY_ROLLED_BACK. - Apply the balance change and the transaction record within a single DB transaction, after acquiring a per-player lock. The rollback marker check must also be done after acquiring the lock, so that no double processing occurs when a debit and a rollback overlap.
- Credits and rollbacks must end with
OKfor a player in any state. Transactions that return money must be applied even if the player is blocked, logged out, has an expired session, or is self-excluded. - For errors where you are not sure whether the deduction happened (a DB timeout, etc.), respond with
UNKNOWN_ERRORor HTTP 5xx. Use a rejection status only when you are certain the balance was not touched. - Always include the post-processing
balancein the response. - Storing the request body's
game_code,table_id,round_id, andreasonmakes reconciliation with the Rounds and Transactions API easier. You can match both sides' records usingtx_idandoperator_tx_idfromGET /transactions. - Respond with HTTP 404 for unknown endpoint paths and with
INVALID_REQUESTfor malformed JSON.
Implementation example (pseudocode):
on debit(req):
verify signature → on failure {status: INVALID_SIGNATURE}
validate fields (tx_id, external_id, amount > 0 integer)
→ on failure {status: INVALID_REQUEST}
BEGIN
if wallet_tx[req.tx_id] exists → return stored response
lock player row (SELECT … FOR UPDATE) → if missing PLAYER_NOT_FOUND (record, then return)
if rolled_back[req.tx_id] exists → TX_ALREADY_ROLLED_BACK (record, then return)
if currency differs → CURRENCY_MISMATCH
if blocked / self-excluded → PLAYER_BLOCKED
if responsible gambling limit exceeded → LIMIT_EXCEEDED
if balance < amount → INSUFFICIENT_FUNDS
balance -= amount
res = {status: OK, balance, operator_tx_id}
insert wallet_tx(req.tx_id, 'debit', amount, res)
COMMIT
return resConnection Check
After the integration application, the platform calls balance once on the registered wallet_base_url to verify connectivity and signing. Because the call uses a non-existent player (external_id is __hq_ping_<number>, player_id is null), returning PLAYER_NOT_FOUND after passing signature verification is the expected result. If you provide the external_id of a test account, the platform also confirms an OK response with that account. For pre-launch checks, see the Integration Checklist.