Direct integration
Wallet contract for casinos integrating with us directly
Direct integration is forward-looking. The schemas here are concrete enough to scope work, but talk to us before starting — we'll confirm the final paths. The shape mirrors Gametech's (VisionLINK's) seamless wallet API: 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. All authentication is HTTP Basic Auth, all amounts are integers in the currency's smallest unit (see Money), all identifiers are UUIDs we generate.
Endpoints you must implement
| Endpoint | When we call it |
|---|---|
POST /getbalance | Before showing the card-purchase UI |
POST /debit | When the player buys cards or places a side bet |
POST /credit | When the player wins, or to close a zero-win round (terminal call) |
POST /reverse | When a debit must be rolled back (game crash, validation failure). Can also be the terminal call that closes a failed round |
We do not currently call /endsession. Sessions on our side expire
passively after their TTL (24 hours by default). If you need a positive
session-end signal, talk to us — it's on the roadmap.
POST /getbalance
Request (from us)
{
"playerid": "player_42",
"sessionid": "<our-session-id>",
"externalsessionid": "<your-session-id>",
"gamecode": "BINGO25"
}getbalance does not carry a currency field — it returns the
balance for whichever currency the player's wallet holds for this
session.
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 fake non-zero value: it will show up incorrectly on the player's screen.
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": "BINGO25",
"currency": "EUR",
"roundid": "<uuid>",
"transid": "<uuid>",
"debitamount": 100,
"reason": "REGULAR",
"roundstarted": true,
"roundended": false
}reason is always REGULAR today. The field is reserved for marking
promotional rounds (e.g., freebets) when those land — for now you can
ignore it; we will never send any other value.
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.
Success response
{
"cashbalance": 12400,
"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": "BINGO25",
"currency": "EUR",
"roundid": "<uuid>",
"transid": "<uuid>",
"creditamount": 250,
"reason": "REGULAR",
"roundstarted": false,
"roundended": true
}Response
{
"cashbalance": 12650,
"currency": "EUR"
}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) or a debit was accepted but the followup
operation could not complete.
Request (from us)
{
"playerid": "player_42",
"sessionid": "<our-session-id>",
"externalsessionid": "<your-session-id>",
"gamecode": "BINGO25",
"roundid": "<uuid>",
"transid": "<uuid>",
"originaltransid": "<the-debit-transid-being-reversed>",
"roundended": true
}Reverse intentionally omits currency, reason, and roundstarted — the
original debit already established those, and the reverse is identified by
originaltransid.
Response
{
"cashbalance": 12500,
"currency": "EUR"
}Rules
originaltransidMUST refer to a previously-accepteddebitfor the same player and round. If you don't recognise it, return an error.- 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— UUID, generated by us, unique per player per round (never shared between players, even in the same room).transid— UUID, generated by us, unique per wallet call. Idempotency key.- A 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.
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 on any non-2xx
response:
{ "errorcode": "NOT_SUFFICIENT_FUNDS", "errormessage": "balance below stake" }We read the errorcode field exactly. Do not return { code, message } —
our wallet client will not parse it and will treat the call as a transport
error, triggering retries you don't want.
Codes we currently treat specially:
| Code | What we do |
|---|---|
NOT_SUFFICIENT_FUNDS | Reject card purchase, surface a clear message to the player |
BET_NOT_ALLOWED | Reject card purchase, surface "this stake is not allowed for you right now" to the player |
SESSION_NOT_FOUND | Treat as session-expired, surface to the player |
Any other errorcode you return is logged but treated as a generic business
error from your side — the round fails and the player sees a generic error.
You may return codes like PLAYER_BLOCKED (self-excluded, hit a limit) or
CURRENCY_MISMATCH for completeness; today they are not specially handled,
but we expect to layer behaviour onto them as the integration matures.
Network timeouts and 5xx responses are retried per the table above (only
for credit and reverse).
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 | Yes — 3 attempts total, with 1s and 2s waits between (~3s span) |
/reverse (after a failed /debit) | Same as credit (3 attempts) |
/reverse during round-fail cleanup (game crash) | No retry, single attempt with 2s timeout |
/debit | No retry — single attempt. A failed debit triggers a retried /reverse (3 attempts) 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. We send one before each card purchase, AND one every 30 seconds for every connected player while their iframe is open (the WebSocket heartbeat). A 100-player room generates roughly 200getbalancecalls per minute from heartbeats alone, on top of the per-round pre-buy calls.debit: 1–3 calls per player per round (card purchase + optional side bet)credit: 1 call per player per round (settlement)reverse: only on failure paths; rare in steady state
A 100-player room with rounds back-to-back generates roughly 250–400 wallet
calls per minute at peak, dominated by getbalance. 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.