Skip to content

Wallet API reference

Player money stays in your wallet. The platform calls your server for every movement: four POST endpoints under the walletUrl you give us (for example https://wallet.your-casino.example/sportsbook).

Endpoints
balance · debit · credit · rollback
Signature
HMAC-SHA256, hex
Clock window
±300 s
Timeout
5 s per attempt

Every call is a POST with a JSON body and three headers:

POST /sportsbook/debit HTTP/1.1
Content-Type: application/json
X-Timestamp: 1760000000
X-Signature: 8bc4fc9b84ee8f997c3f3f51c9e3dfc7e848ddff0ad05908327fca9b4ec70a0e
{"txId":"bet:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53","playerId":"player-42","amount":500,"currency":"USD","kind":"bet","betId":"8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53"}
Field Type
txId string Idempotency key, at most 200 characters. See transaction ids.
playerId string Your player id: the externalPlayerId you sent when creating the session.
amount integer Minor units (cents). Always zero or positive; the endpoint gives the direction.
currency string The tenant currency, e.g. USD, BRL.
kind string What the movement is, see the table below.
betId string The bet, when the movement belongs to one.
ref string Context: the debit being rolled back, or the result that caused a payout, e.g. result:ev_8f2c/1x2@1.
kind Endpoint
balance /balance Balance query (amount is 0).
bet /debit A stake.
payout /credit The first settlement of a bet: the winnings, or 0 for a lost bet.
refund /credit A void bet: the stake comes back.
cashout /credit The player cashed out.
resettle /credit or /debit A correction after a market was resettled.
rollback /rollback A stake debit that did not become a bet.
adjustment /credit or /debit A manual correction by the platform’s staff, audited.
txId Call
balance:<uuid> /balance (fresh every time)
bet:<betId> /debit, the stake
rollback:bet:<betId> /rollback of that stake (ref = bet:<betId>)
payout:<betId>:<version> /credit, first settlement (kind payout or refund)
resettle:<betId>:<version>:<n> /credit or /debit, the n-th settlement of the bet
cashout:<betId> /credit, cash-out

Treat the formats as informative: rely on txId being unique per movement, not on its shape.

Answer 2xx with the balance after the movement:

{ "txId": "bet:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53", "balance": 49500, "currency": "USD" }

txId is optional, but if you send it, it must be the request’s. Refuse with a 4xx status and an error body:

{ "error": { "code": "insufficient_funds", "message": "insufficient funds" } }

How we read your answer:

Your answer Means What we do
2xx, valid JSON, no error Applied. Done.
4xx except 408 and 429 Refused, nothing moved. A stake: the bet is refused (insufficient_funds tells the player why). Anything else: not retried, flagged for manual review.
408, 429, 5xx, timeout, connection error, a 2xx we cannot parse Unknown: you may or may not have applied it. Retried or rolled back, see Retries.
{"txId":"balance:5f0c7a52-3f6e-4f6e-9a54-0b8f8f6e2a11","playerId":"player-42","amount":0,"currency":"USD","kind":"balance"}

Answer {"balance": 50000, "currency": "USD"}. Called when the sportsbook shows the balance; no money moves. If it fails, the sportsbook shows no balance until the next successful call.

{"txId":"bet:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53","playerId":"player-42","amount":500,"currency":"USD","kind":"bet","betId":"8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53"}
  • kind: "bet" is a stake. If the balance is short, refuse with code insufficient_funds (402 is conventional; any 4xx except 408/429 works).
  • A stake debit is sent once and never retried. If its outcome is unknown, we roll it back and refuse the bet, so a bet is never accepted on money you did not confirm.
  • kind: "resettle" or "adjustment" is a correction the player owes. Apply it even if the balance goes negative; see corrections.
{"txId":"payout:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53:1","playerId":"player-42","amount":1050,"currency":"USD","kind":"payout","betId":"8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53","ref":"result:ev_8f2c/1x2@1"}

Credits are owed: we retry them until you confirm (see below). Accept amount: 0.

{"txId":"payout:d4e8a6b2-1c3f-4e5a-9b7d-6f0a2c4e8b19:1","playerId":"player-42","amount":0,"currency":"USD","kind":"payout","betId":"d4e8a6b2-1c3f-4e5a-9b7d-6f0a2c4e8b19","ref":"result:ev_8f2c/1x2@1"}

Every settled bet gets exactly one payout: credit, also when it lost. Use it to close the bet in your books (many regulators want an explicit result per wager). Answer 2xx with the unchanged balance.

{"txId":"rollback:bet:f27b9c1e-6d4a-4b8f-83e2-5a1c9d7e0b46","playerId":"player-42","amount":200,"currency":"USD","kind":"rollback","betId":"f27b9c1e-6d4a-4b8f-83e2-5a1c9d7e0b46","ref":"bet:f27b9c1e-6d4a-4b8f-83e2-5a1c9d7e0b46"}

Reverses the stake debit whose txId is ref, because the bet did not go through (your answer to the debit was lost, or the bet failed after it).

  1. If you applied that debit and have not reversed it: give the money back and mark the debit reversed.
  2. If you already reversed it: do nothing, answer 2xx.
  3. If you never saw that debit (it timed out before reaching you, or is still on its way): answer 2xx with the current balance, and remember the ref. If the debit arrives later, refuse it with a 4xx (rolled_back). Otherwise the player would pay for a bet that does not exist.

amount is the original stake; refund what you actually debited.

X-Signature = hex( HMAC-SHA256( walletSecret, X-Timestamp + "." + rawBody ) )
  • rawBody is the request body exactly as received, bytes for bytes. Verify before parsing; never re-serialise the JSON (key order and spacing would change).
  • X-Timestamp is the Unix time in seconds when the attempt was sent. Refuse requests more than 300 seconds away from your clock (replay protection), and keep your servers on NTP.
  • Compare in constant time. The hex is lowercase; accept either case.
  • A retry carries a fresh timestamp and signature over the same body.
walletSecret = dev-wallet-secret
X-Timestamp = 1760000000
rawBody = {"txId":"bet:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53","playerId":"player-42","amount":500,"currency":"USD","kind":"bet","betId":"8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53"}
signed text = 1760000000.{"txId":"bet:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53","playerId":"player-42","amount":500,"currency":"USD","kind":"bet","betId":"8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53"}
X-Signature = 8bc4fc9b84ee8f997c3f3f51c9e3dfc7e848ddff0ad05908327fca9b4ec70a0e

Check it from a shell:

Terminal window
printf '%s' '1760000000.{"txId":"bet:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53","playerId":"player-42","amount":500,"currency":"USD","kind":"bet","betId":"8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53"}' \
| openssl dgst -sha256 -hmac dev-wallet-secret

Each sample is tested against requests signed by the platform’s own wallet client code: valid requests pass; a changed body and a 301-second-old timestamp fail.

samples/verify-signature.mjs
import { createHmac, timingSafeEqual } from 'node:crypto';
const MAX_SKEW_SECONDS = 300;
/**
* @param {string} secret your wallet secret
* @param {string} timestamp the X-Timestamp header (unix seconds)
* @param {string} signature the X-Signature header (hex)
* @param {Buffer|string} rawBody the request body exactly as received
* @returns {null | 'bad_timestamp' | 'bad_signature'}
*/
export function verifySignature(secret, timestamp, signature, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) {
if (!/^\d+$/.test(timestamp ?? '') || Math.abs(nowSeconds - Number(timestamp)) > MAX_SKEW_SECONDS) return 'bad_timestamp';
const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
const given = Buffer.from(signature ?? '', 'hex');
return given.length === expected.length && timingSafeEqual(given, expected) ? null : 'bad_signature';
}

On failure answer 401 with {"error": {"code": "invalid_signature"}}.

txId is the idempotency key of every money call.

  • Store each txId you process with its outcome (status and body).
  • When a txId arrives again, answer the stored outcome and move no money. This is how retries become safe.
  • If a known txId arrives with a different player, endpoint or amount, refuse it (409, duplicate_tx).
  • Apply the movement and store the txId in one database transaction.
Timeout per attempt 5 seconds
Stake debits (kind: bet) One attempt. Unknown outcome: rolled back, bet refused.
Credits, rollbacks, correction debits Up to 5 attempts, waiting 200 ms, 400 ms, 800 ms and 1.6 s in between (doubling, at most 2 s)
Still unknown after that Kept pending and sent again, same txId and body, by a reconciliation job every minute, until you answer 2xx or 4xx
Settlements Also queued: a settlement that could not be paid is retried every 30 seconds

So a credit can reach you minutes or hours after the bet was settled (after an outage on your side), and more than once. Idempotency makes that harmless.

A market can be resettled (a result corrected after the first settlement). The platform then computes the difference for each affected bet and sends a resettle movement with txId resettle:<betId>:<version>:<n>: a credit if the player is owed more, a debit if they were overpaid.

The player may already have spent the winnings. Apply the correction debit anyway and let the balance go negative; recover it under your terms. If you refuse it with a 4xx, automatic processing stops and the movement goes to manual review on both sides.

Situation Status error.code Player sees
Stake larger than the balance 402 insufficient_funds “Insufficient funds”
Unknown player 404 player_not_found The bet is refused (payment_rejected)
Player blocked on your side 403 player_blocked The bet is refused (payment_rejected)
Bad signature or stale timestamp 401 invalid_signature The bet is refused
txId reused with other parameters 409 duplicate_tx The bet is refused
Debit already rolled back 409 rolled_back The bet is refused
Temporary failure 5xx or timeout any A stake: refused as temporarily unavailable. Others: retried

Only insufficient_funds has a special meaning. Any other code is recorded in the platform’s wallet call log and returned in the error details as operatorCode.

  • Keep a ledger row per txId: endpoint, kind, amount, bet, status, time.
  • For every bet you will see one bet: debit and then either one rollback (the bet was refused) or one payout:/cashout: credit (possibly 0), followed by any resettle: movements. A bet with only its debit is open.
  • The platform logs every money call it makes to you (status, attempts, your answer) and keeps unresolved ones pending until they are resolved.
  • Daily, compare your totals per UTC day with the back office Reports (GET /api/v1/admin/reports/ggr?from=YYYY-MM-DD&to=YYYY-MM-DD: stakes, payouts, refunds, cash-outs and GGR), and look up single bets under Bets (GET /api/v1/admin/bets?player=<player id>).