Direct integration
Wallet contract for casinos integrating with us directly
This page describes the current direct-integration contract. We'll confirm your endpoint URLs and operator settings during onboarding. The shape follows the standard seamless wallet API pattern: we call your wallet in real time per transaction, rather than transferring a balance to us up-front.
What direct means
Instead of an aggregator handling the wallet, you implement wallet endpoints on your side and we call them. You retain full ownership of the player wallet, identity verification (Know Your Customer, KYC), and responsible gambling limits. You skip the aggregator fee.
You implement four endpoints. We call them when game events happen. We
generate sessionid, roundid, and transid; you provide playerid and
externalsessionid. Wallet amounts use the default integer-cents convention
unless we agree on a custom convention during onboarding. See
Money.
Authentication is HTTP Basic Auth by default. On request, we can sign
wallet calls instead: we send an x-operator-signature header holding the
HMAC-SHA256 of the raw request body (hex encoded), using a secret we share with
you. Check the signature against the exact bytes you received.
Endpoints you must implement
| Endpoint | When we call it |
|---|---|
POST /getbalance | Before a card purchase, when the player opens the slot panel, and about every 30 seconds while the player is connected |
POST /debit | When the player buys cards, places a side bet, or spins a slot |
POST /credit | When a round or spin settles, including with creditamount: 0 for no win (terminal call) |
POST /reverse | When a debit must be rolled back (the round failed, or the debit itself failed). Can also be the terminal call that closes a failed round |
We do not call /endsession. Sessions on our side expire passively after
their TTL (24 hours). If you need a positive session-end signal,
discuss that requirement during onboarding.
POST /getbalance
Request (from us)
{
"playerid": "player_42",
"sessionid": "<our-session-id>",
"externalsessionid": "<your-session-id>",
"gamecode": "live-bingo"
}By default, getbalance does not carry a currency field. Custom
integrations may receive it when their wallet requires a per-currency
balance lookup.
Response
{
"cashbalance": 12500,
"bonusbalance": 0,
"currency": "EUR"
}We read cashbalance and currency for our own checks. bonusbalance is
forwarded to the player's UI as a separate displayed value — return
your real bonus-funds figure if you track it, otherwise return 0. Don't
return a placeholder or arbitrary non-zero value when you do not track
bonus funds, because the value is shown directly to the player.
We read the same cashbalance / bonusbalance / currency trio from the
/debit, /credit, and /reverse responses below and refresh the player's
UI from them the same way. So return bonusbalance consistently on those
responses too (or 0) — if you omit it there while tracking bonus funds,
the player's bonus display drops to 0 after every bet and win until the
next /getbalance.
POST /debit
Place a bet. Deduct from the player's wallet.
Request (from us)
{
"playerid": "player_42",
"sessionid": "<our-session-id>",
"externalsessionid": "<your-session-id>",
"gamecode": "live-bingo",
"currency": "EUR",
"roundid": "<uuid>",
"transid": "<uuid>",
"debitamount": 100,
"reason": "REGULAR",
"roundstarted": true,
"roundended": false
}reason is REGULAR for paid rounds and FREEBET for free-card rounds. See
Free cards for the additional fields on a free-card debit.
A slot spin's debit may also carry "autoplay": true when the player's
autospin placed the bet. See Slots.
debitamount: 100 means 1.00 in the player's currency. Wallet amount
conversion happens in our integration adapter; current direct integrations use
this integer-cents convention.
Side-bet debits arrive with roundstarted: false. A round starts on
the first card-buy debit (roundstarted: true); subsequent debits within
the same round (e.g. lucky line side bets) carry roundstarted: false. A slot
spin is its own round, so its single debit carries roundstarted: true.
Success response
{
"cashbalance": 12400,
"bonusbalance": 0,
"currency": "EUR"
}Failure response
{ "errorcode": "NOT_SUFFICIENT_FUNDS", "errormessage": "balance below stake" }Idempotency
A repeated call with the same transid MUST return the same result — same
balance, same status. Do not deduct twice. Treat transid as the deduplication
key.
POST /credit
Award a win. Add to the player's wallet.
Request (from us)
{
"playerid": "player_42",
"sessionid": "<our-session-id>",
"externalsessionid": "<your-session-id>",
"gamecode": "live-bingo",
"currency": "EUR",
"roundid": "<uuid>",
"transid": "<uuid>",
"creditamount": 250,
"reason": "REGULAR",
"roundstarted": false,
"roundended": true
}Response
{
"cashbalance": 12650,
"bonusbalance": 0,
"currency": "EUR"
}The balance fields use the same integer-cents convention:
cashbalance: 12650 means 126.50.
Important rules
- Every round MUST be closed exactly once, by a single terminal call with
roundended: true. The terminal call is either acredit(with the win amount, orcreditamount: 0for a no-win round) or areverse(when the round failed mid-play). Never both. - Reject any non-idempotent call against a round you've already closed.
- Idempotent on
transid.
POST /reverse
Roll back a previously-issued debit. We call this when:
- a game round fails mid-play (state machine error, crash);
- a round is cancelled before it starts, because some games skip a round when too few players joined (every debit in that round is reversed);
- a debit got a server error or timed out, so we can't tell whether you took the money;
- a debit was accepted but the follow-up operation could not complete.
Request (from us)
{
"playerid": "player_42",
"sessionid": "<our-session-id>",
"externalsessionid": "<your-session-id>",
"gamecode": "live-bingo",
"roundid": "<uuid>",
"transid": "<uuid>",
"originaltransid": "<the-debit-transid-being-reversed>",
"roundended": true
}By default, reverse omits currency, reason, and roundstarted — the
original debit already established those, and the reverse is identified by
originaltransid. Custom integrations may receive currency when their
wallet requires a per-currency reverse lookup.
Response
{
"cashbalance": 12500,
"bonusbalance": 0,
"currency": "EUR"
}Rules
originaltransidMUST refer to a previously-accepteddebitfor the same player and round. If you don't recognise it, return an error.- Note that we may send a reverse for a debit you never received: when a
debit times out we reverse it without knowing whether it arrived, and the
reverse can reach you before a late debit does. If you reject a reverse (or
it fails), we keep sending the same reverse, same
transid, about every 5 minutes for about 3 hours. So if the late debit lands while the round is still open, the next retry undoes it. If the debit never arrives, or the round is already closed, you will see these retries until they stop and we check the round with you. - Reverses are idempotent on
transid— a duplicate retry returns the same result. - A reverse against an open round (one we have not yet closed with
roundended: true): apply the reverse. If we setroundended: trueon this reverse call, also close the round. - A reverse against an already-closed round: reject with
ROUND_ALREADY_CLOSED. Once closed, no further wallet activity should affect the round. (The exception is a duplicate retry of the sametransid— that's idempotent and you should return the original result.)
Round and transaction ID contract
roundid— generated by us, unique per player per round (never shared between players, even in the same room). Store it as an opaque string, not a UUID. Most round IDs are UUIDs, but some games add a prefix: a slot spin isslots:<uuid>. Today no round ID is longer than 44 characters.transid— UUID, generated by us, unique per wallet call. Idempotency key. When we retry a call, we reuse itstransid.- A bingo round may receive up to two
debitcalls before it's closed: one for the card purchase and (optionally) one for a side bet (e.g. lucky line). Each is a freshtransid;roundidstays constant. - A slot spin receives exactly one
debit. See Slots.
Free cards
Free card grants and the FREEBET wallet calls they cause are described on
their own page: Free cards.
Worked timing examples
Happy path — player wins:
POST /getbalance { player: P1 } → balance 5000
POST /debit { round: R1, trans: T1, debit: 100, ended: false } → balance 4900
POST /credit { round: R1, trans: T2, credit: 250, ended: true } → balance 5150
round R1 closedIdempotent retry — we didn't get your credit response, we retry:
POST /credit { round: R1, trans: T2, credit: 250, ended: true } → balance +250
POST /credit { round: R1, trans: T2, credit: 250, ended: true } → SAME response,
no second addZero-win round — required to close it:
POST /debit { round: R1, trans: T1, debit: 100, ended: false } → balance -100
POST /credit { round: R1, trans: T2, credit: 0, ended: true } → balance unchanged
round R1 closedGame crashed mid-round — we reverse and close:
POST /debit { round: R1, trans: T1, debit: 100, ended: false } → balance -100
POST /reverse { round: R1, trans: T2, originaltransid: T1, ended: true } → balance +100
round R1 closedLate reverse against a closed round — reject:
POST /credit { round: R1, trans: T2, credit: 250, ended: true } → round R1 closed
POST /reverse { round: R1, trans: T3, originaltransid: T1 } → ROUND_ALREADY_CLOSEDSide-bet reverse keeps the round open:
POST /debit { round: R1, trans: T1, debit: 100, ended: false } → cards bought
POST /debit { round: R1, trans: T2, debit: 25, ended: false } → lucky line bet
POST /reverse { round: R1, trans: T3, originaltransid: T2,
ended: false } → lucky line refunded,
round R1 still open
POST /credit { round: R1, trans: T4, credit: 0, ended: true } → round R1 closedError model we expect from you
Return a JSON body with errorcode and errormessage fields when you refuse
a call:
{ "errorcode": "NOT_SUFFICIENT_FUNDS", "errormessage": "balance below stake" }We read the errorcode field exactly, and we classify your response like
this:
| Your response | What we do |
|---|---|
2xx without errorcode | Success |
2xx with an errorcode | Refusal (the call failed, no retry, except a reverse, which we keep resending in the background) |
| 4xx | Refusal, no retry, except a reverse (as above). We use your errorcode if present |
| 5xx, or no response within the timeout | Server failure. credit and reverse are retried (see below) |
Never return a 2xx with { code, message } for a refusal. We don't read
code, so we would treat the call as a success.
Codes we currently treat specially:
| Code | What we do |
|---|---|
NOT_SUFFICIENT_FUNDS | Reject the bet, show the player "Insufficient balance." |
BET_LIMIT_REACHED | Reject the bet, show the player "You have reached your betting limit." |
GAMING_LIMIT_REACHED | Same as BET_LIMIT_REACHED |
SESSION_NOT_FOUND | Show the player "Your session has expired. Please reconnect." |
Any other errorcode you return is logged but treated as a generic refusal
from your side: the bet fails and the player sees a generic error. You may
return codes like PLAYER_BLOCKED (self-excluded) or CURRENCY_MISMATCH for
your own records; today they are not specially handled.
Only credit and reverse retry. debit and getbalance are
single-shot. 5xx responses (server-side HTTP errors, status 500–599)
and network timeouts trigger:
| Endpoint | Retries on 5xx / timeout |
|---|---|
/credit for a bingo round | 3 attempts, with 1s and 2s waits between (~3s span), then a background job resends the same call, same transid, about every 5 minutes for about 3 hours (a refusal is retried too). If a round has two credits and the first fails, the second (the one with roundended: true) is held back and resent after it, in order |
/credit for a slot spin | 3 attempts, then a background job retries the same call about every 2 minutes until your wallet accepts it (a refusal is retried too). See Slots |
/reverse (after a failed /debit) | Same as credit (3 attempts), then a background job resends the same call about every 5 minutes for about 3 hours (a refusal is retried too) |
/reverse during round-fail cleanup (game crash, round cancelled before it starts) | Single attempt with 2s timeout, then the same background job as above |
/debit | No retry — single attempt. A failed debit triggers a retried /reverse to return funds |
/getbalance | No retry — single attempt |
The default per-call HTTP timeout is 10 seconds. Be idempotent on
transid — for the calls that do retry, you will receive duplicates.
Wallet call rate
Direct integrators should size for the following peak rates per operator. These are guidance, not enforced limits — talk to us if you anticipate sustained traffic well above this:
getbalance: the dominant call for bingo. We send one before each card purchase, AND one about every 30 seconds for every connected player while their iframe is open (the connection heartbeat). A 100-player room generates roughly 200getbalancecalls per minute from heartbeats alone, on top of the per-round pre-buy calls.debit: 1–2 calls per player per bingo round (card purchase + optional side bet)credit: 1 call per player per bingo round (settlement), 2 for a free-card round with a side betreverse: only on failure paths; rare in steady state- Slots: 1
debitand 1creditper spin. With autospin, that's a pair every few seconds per spinning player. See Slots.
A 100-player bingo room with rounds back-to-back generates roughly 250–400
wallet calls per minute at peak, dominated by getbalance, before any slot
spins. Confirm your specific volume expectations during onboarding.
Voids and disputes after a round closes
We have no API to undo a closed round. reverse only works while the
round is still open. Once a round is closed by a terminal credit or
reverse, it is final from our perspective.
If a regulator orders a void after the fact, or a player wins a chargeback
against a settled round, the casino is responsible for making the
adjustment manually in its own books. We can supply the round record (from
/gamelauncher/replay) for
evidence; we cannot reverse the wallet entries.
Sandbox
When you're ready to start, we'll provision a sandbox callback URL pair (yours → ours, ours → yours), exchange Basic Auth credentials, and run a fixed test suite that exercises every endpoint above including failure paths. Talk to us when you're ready.