Skip to content

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_id generated by the platform. The operator must use tx_id for 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 ​

EndpointWhenWhat it doesRetry
POST {wallet_base_url}/balanceEntering the game screen, refreshing the balanceReturns the current balanceNo
POST {wallet_base_url}/debitBets (bet, ante, double, split), taking a seat (buy_in), adding chips (top_up)Deducts from the balanceNo — if the outcome is uncertain, it is treated as a failure and rolled back
POST {wallet_base_url}/creditRound settlement (settle, amount: 0 even on a loss), leaving a seat (cash_out)Adds to the balanceUntil it succeeds
POST {wallet_base_url}/rollbackBet cancellation (cancel_bet), voided round (round_void), uncertain debit (timeout), cleanup of unfinished debits (system)Refunds the original debit amountUntil 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_urlActual callPath used for signing
https://api.example-casino.com/walletPOST https://api.example-casino.com/wallet/debit/wallet/debit
https://wallet.example-casino.comPOST https://wallet.example-casino.com/credit/credit
  • All four endpoints use the POST method.
  • 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.

HeaderValue
Content-Typeapplication/json; charset=utf-8
X-OperatorOperator code (e.g. acme)
X-TimestampTime of sending, Unix epoch milliseconds (e.g. 1792547400123)
X-NonceA new 24-character random string for each request (upper- and lowercase letters, digits, -, _)
X-SignatureLowercase hex HMAC-SHA256 signature (key = api_secret)
  • The platform does not send X-Api-Key with 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, and X-Signature are generated anew each time.

The operator should check the following:

  1. X-Operator is its own operator code
  2. X-Timestamp is within ±5 minutes of the current time
  3. The signature is valid (constant-time comparison)
  4. The same X-Nonce has 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.

FieldTypebalancedebitcreditrollbackDescription
tx_idstring●●●Platform transaction ID (tx_ + 32 hex digits). Idempotency key
external_idstring●●●●Operator-side player ID (the value sent at game launch)
player_idstring | null●●●●Platform player ID (pl_…), for reference. null in the balance connection test
currencystring●●●●Currency code (e.g. PHP). A player's currency is fixed at the first launch and never changes
operator_session_idstring | null●●●●The value sent at game launch. null if none was sent; may be null on platform recovery paths
amountinteger●●●Integer in the currency's minor unit. 1 or more for debit and rollback, 0 or more for credit
reasonstring●●●Transaction reason. See the table on each endpoint's page
game_codestring●●●Game code (e.g. live_baccarat)
table_idstring●●●Table ID (tbl_…)
round_idstring●●●Grouping ID. A round (rnd_…) for bet-flow games, a table session (tss_…) for buy-in-flow games
created_atstring●●●Time the platform created the transaction, UTC with milliseconds (2026-10-19T02:10:00.123Z). Does not change on retries
round_closedboolean●Whether this credit ends the player's round (or table session)
ref_tx_idstring●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.

json
{ "status": "OK", "balance": 1250000, "operator_tx_id": "w-99812" }
FieldTypeDescription
statusstringResult code. One of the values in the table below (required)
balanceintegerPlayer 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_idstringOperator-side transaction ID. Stored together in the platform ledger and used for transaction lookup and reconciliation

status values ​

statusMeaningdebitcredit / rollbackbalance
OKProcessed (for a repeated request with the same tx_id, the original result)ConfirmedConfirmedBalance shown
INSUFFICIENT_FUNDSInsufficient balanceDefinitive rejectionRetriedTreated as failure
PLAYER_NOT_FOUNDPlayer does not existDefinitive rejectionRetriedTreated as failure
PLAYER_BLOCKEDPlayer is blocked or self-excludedDefinitive rejectionRetriedTreated as failure
LIMIT_EXCEEDEDOperator's responsible gambling limit exceededDefinitive rejectionRetriedTreated as failure
CURRENCY_MISMATCHRequest currency differs from the player's currencyDefinitive rejectionRetriedTreated as failure
INVALID_SIGNATURESignature verification failedDefinitive rejectionRetriedTreated as failure
TX_ALREADY_ROLLED_BACKA debit arrived late for a tx_id that has already been rolled backDefinitive rejectionRetriedTreated as failure
INVALID_REQUESTMissing fields or malformed valuesDefinitive rejectionRetriedTreated as failure
UNKNOWN_ERRORAny other error (unknown whether it was processed)Uncertain outcome → treated as failure + immediate rollbackRetriedTreated 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 status is treated exactly like UNKNOWN_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 than OK.
  • 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_id arrives 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_id for a different amount, a different player, or a different reason. If the operator receives a request with the same tx_id but different contents, reject it with INVALID_REQUEST and notify the platform.
  • tx_id uniqueness spans all endpoint types. The tx_ids of debits, credits, and rollbacks never overlap.
  • The tx_id of 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.

CallResponse waitOn failure or uncertainty
balance3 secondsNot retried. Shows the balance known within the last 10 seconds
debit3 secondsNot retried. Treated as a failure, and a rollback for the same amount (reason: "timeout") is sent immediately
credit5 secondsRetried until it succeeds
rollback5 secondsRetried 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_10

Buy-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.

MomentPrepaid creditWallet callback
Just before a debitThe debit amount is deducted firstIf prepaid credit is insufficient, no debit is sent and the bet or seat is rejected (player screen error OPERATOR_CREDIT_EXHAUSTED)
Debit definitively rejectedThe deducted amount is restored—
Debit uncertainThe deducted amount is restored when the rollback completes with OKRollback sent
Rollback OKThe amount deducted for the original debit is restored—
Credit OKThe 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 ​

  1. Keep a transaction record table with tx_id as the primary key. Store the processing result (the response JSON) with it, and return it unchanged for repeated requests with the same tx_id.
  2. Keep rollback markers (tombstones). If a rollback arrives and the original debit does not exist, record ref_tx_id as "rolled back", and if that debit arrives later, reject it with TX_ALREADY_ROLLED_BACK.
  3. 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.
  4. Credits and rollbacks must end with OK for 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.
  5. For errors where you are not sure whether the deduction happened (a DB timeout, etc.), respond with UNKNOWN_ERROR or HTTP 5xx. Use a rejection status only when you are certain the balance was not touched.
  6. Always include the post-processing balance in the response.
  7. Storing the request body's game_code, table_id, round_id, and reason makes reconciliation with the Rounds and Transactions API easier. You can match both sides' records using tx_id and operator_tx_id from GET /transactions.
  8. Respond with HTTP 404 for unknown endpoint paths and with INVALID_REQUEST for 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 res

Connection 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.

Docs v1.0.0 · Operator API v1