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

These endpoints handle sportsbook balance queries and bet-related movements. They do not implement the player’s deposits, withdrawals, PIX, cards or other payment methods. Those stay in your platform’s cashier and payment providers. In particular, a settlement credit updates the casino wallet; it is not a bank payout.

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

This fixed example uses the public test secret documentation-only-secret. Its old timestamp is for offline verification only; generate a fresh timestamp for live calls. Download the test vector.

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: 5a409c37866145f0505662fc18747d92fb61c08c9bff78c1f404ad8779b9428d
{"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 required for debit, credit and rollback and must exactly match the request. balance must be an explicit integer (zero is valid; missing or null is not), and currency must match the request. Balance queries do not need to echo txId. Refuse with a 4xx status and a JSON error body with a nonempty error.code:

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

How we read your answer:

Your answer Means What we do
2xx, matching txId and currency, explicit integer balance, no error Applied. Done.
4xx except 408 and 429, valid error envelope 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, malformed or incomplete success/error body, mismatching transaction/currency 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 only when the full amount is available; otherwise refuse with insufficient_funds for operator review. 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"}

A bet settled normally receives an initial payout: credit, also when it lost. Full cash-out is closed by its cash-out credit instead. 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 = 5a409c37866145f0505662fc18747d92fb61c08c9bff78c1f404ad8779b9428d

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, amount, currency, kind, bet or reference, 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 Eligible calls remain pending and retry with the same txId and body. Refusals, corrections and predecessor obligations can stop retries; see operating limits below.
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. Check the balance and apply the correction atomically. If the full amount is not available, return 402 with error.code: "insufficient_funds" and move no money. Do not collect a partial amount, create a negative balance, or capture future deposits. Persist the refusal under the original txId; duplicates return that same refusal even after a deposit. OrbiSportbook retains the obligation as failed, stops automatic delivery for that bet and shows it in Operator workspace → Integration → Financial delivery and exceptions. The operator investigates with the original transaction and bet IDs. Do not invent a replacement transaction ID or mark the obligation paid without evidence from both ledgers.

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 an initial payout:/full cashout: credit (possibly 0), followed by any resettle: movements. Partial cash-outs may produce several cashout:<betId>:p<n> credits before the remaining stake settles. A debit alone does not prove an open bet: acceptance may have failed with a rollback still pending.
  • 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>).

Postings for the same bet are delivered in durable creation order. An unknown or refused predecessor blocks later postings for that bet; unrelated bets continue. Each retry preserves the original financial identity. A crashed worker may send the same transaction again after its lease expires: your wallet must still enforce atomic idempotency. There is no exactly-once network delivery guarantee.

The bet result may be visible before wallet delivery finishes. Never infer that a player was paid from won, cashed_out or an HTTP 200 on a bet operation alone. Use the financial delivery queue and your transaction ledger.

The current wallet transport has four endpoints. A transaction-status lookup is not implemented in this revision. Unknown outcomes are resolved through idempotent replay or stake rollback; keep the transaction records needed for manual investigation. Do not discard idempotency keys after a short retry window.

Exception: a corrected, unconfirmed credit is held for review. If a later resettlement supersedes a positive credit whose delivery is still unresolved, OrbiSportbook stops automatic sends and records obsolete_credit_requires_review. Neither the original credit nor the following correction may be assumed applied. Compare both ledgers using the original IDs before resolving the incident. Already confirmed cash-outs remain guaranteed. The wallet transport’s general reconciliation worker cannot bypass the settlement queue, its ordering or holds.

Download the OpenAPI contract and follow the certification scenarios.

The wallet URL, wallet mode and currency become fixed after the first financial use. Changing them requires a controlled migration with the integration team, covering balances, idempotency history, open bets and later corrections; ordinary settings return 409 with reason wallet_identity_locked. Coordinated signing-secret rotation remains available.

An unresolved transaction without proof of its original destination is held for operator review. Never send it to a replacement URL merely because that URL is now configured.