Integration guide
This guide follows one player from the lobby to a settled round. It explains what ZooZ does when something goes wrong, and it lists the rules your wallet must follow for the money to reconcile. The exact field lists are in the API reference, and the signature scheme is in Signing.
#Roles
| Party | Responsibility |
|---|---|
| Operator backend (you) | Authenticates the player, opens ZooZ sessions, and runs the seamless wallet: balance, debit, credit, rollback |
| ZooZ RGS | Runs the games: RNG, outcomes, round state and recovery. Calls your wallet for every money movement |
| Game client | The HTML5 game in the player's browser. It talks only to ZooZ and only displays results |
ZooZ is the game provider. You are the system of record for the player's money.
#1. Launching a game
sequenceDiagram
autonumber
participant B as Player browser
participant O as Operator backend
participant Z as ZooZ RGS
participant G as Game client (iframe)
B->>O: Open "Safari King"
O->>O: Create your own game session (sessionId)
O->>Z: POST /api/v1/operator/sessions (signed with operator key)
Z->>Z: Verify signature, create session and one-time launch code
Z-->>O: 200 { gameUrl, launchExpiresAt } (signed)
O->>O: Verify response signature
O-->>B: Page with an iframe (src = gameUrl)
B->>G: Load gameUrl (?launch=...)
G->>Z: POST /api/v1/sessions/exchange { launch }
Z-->>G: { token, expiresAt } (code is now used)
G->>Z: GET /api/v1/game/init (Bearer token)
Z->>O: POST walletUrl/balance (signed with wallet key)
O-->>Z: 200 { status: "ok", balance } (signed)
Z-->>G: Game config and balance- Create your own game session first. Its id is the
sessionIdthat you send to ZooZ. ZooZ sends it back in every wallet call, and you use it to check the player and currency (see rule 1). OnesessionIdis for one player, one currency and one game launch. - Call
POST /api/v1/operator/sessionswith the player, currency and game. Verify the response signature before you use thegameUrl. - Open the
gameUrlright away, in an iframe or with a redirect. It carries a one-time launch code that is valid for 60 seconds. It is not a session token: the game client exchanges the code for its own token, and after that the code is useless. So the URL can safely show up in browser history, proxy logs or aRefererheader. If the player opens the game again later, request a newgameUrl. - The game client keeps its session token in memory and sends it as
Authorization: Bearer. The token has a sliding lifetime (8 hours by default). You never see or handle it.
Why a launch code instead of a token in the URL? URLs leak: into browser history, server logs, analytics and
Referer headers. A code that works once and expires after 60 seconds is worthless to anyone who finds it later.
#2. A round: debit, outcome, credit
Slots, mines and dice use the same money flow. A round is one spin, one mines game (from the first pick to cash-out or a mine) or one dice roll. Free spins triggered by a spin belong to the same round.
sequenceDiagram
autonumber
participant G as Game client
participant Z as ZooZ RGS
participant O as Operator wallet
G->>Z: play (bet 1.00, requestId)
Z->>Z: Record round (Opening)
Z->>O: POST /debit { transactionId: "{round}-bet", amount: 1.00 }
O->>O: Check session, take 1.00 atomically, store tx
O-->>Z: 200 { status: "ok", balance: 99.00 } (signed)
Z->>Z: Compute outcome, store result and state in one transaction
Z->>O: POST /credit { transactionId: "{round}-win", amount: 2.50 }
O->>O: Add 2.50 atomically, store tx
O-->>Z: 200 { status: "ok", balance: 101.50 } (signed)
Z-->>G: Result + balance 101.50- The outcome is produced only after your debit returned
ok. If there is no successful debit, there is no outcome. - A credit is always sent, even for a loss (
amount: 0). A zero credit closes the round in your ledger. - A round has exactly one debit and then either one credit or one rollback. Mines rounds with several picks and slot rounds with free spins still have exactly one debit and one credit.
roundIdis the same on the debit and the credit. The transaction ids are{roundId}-betand{roundId}-win.
#Crash (multiplayer)
A crash round is shared by every player at the table, and one player can hold several bets in the same round (one per bet panel). So in crash, the unit of money is the bet, not the round:
- Each bet has its own debit (
crash-{betId}-bet) and its own credit (crash-{betId}-win), or a rollback. roundIdiscrash-{gameId}-{roundNumber}and is shared by many players and by several bets of one player. Do not assume that oneroundIdmeans one debit. Relationship checks must look for this player's debit in the round.- The debit happens while bets are open. The credit follows when the player cashes out (manually or
automatically) or when the round crashes (
amount: 0). - A rollback of a debit that succeeded is normal in crash. It happens when the player cancels a bet during the betting phase, when the bet reached the table after betting closed, and when a round is voided after a server restart.
sequenceDiagram
autonumber
participant G as Game client
participant Z as ZooZ RGS
participant O as Operator wallet
G->>Z: place bet (panel 1, 5.00)
Z->>O: POST /debit { transactionId: "crash-{bet}-bet", roundId: "crash-sky-parrot-1042" }
O-->>Z: 200 ok (signed)
alt Player cancels before take-off
Z->>O: POST /rollback { transactionId: "crash-{bet}-bet", amount: 5.00 }
O-->>Z: 200 ok, refunded (signed)
else Player cashes out at 2.35x
Z->>O: POST /credit { transactionId: "crash-{bet}-win", amount: 11.75 }
O-->>Z: 200 ok (signed)
else Round crashes first
Z->>O: POST /credit { transactionId: "crash-{bet}-win", amount: 0 }
O-->>Z: 200 ok (signed)
end#3. When the outcome of a call is unknown
A wallet call has an unknown outcome if ZooZ cannot be sure whether your wallet applied it. These count as unknown:
- a network or TLS error, or the connection closing before the full response arrived,
- no response within the timeout (5 seconds by default),
- any HTTP status other than 200, including 4xx, 5xx and 401,
- a response that is missing its signature, is signed with an unknown key id, has a wrong signature or a timestamp outside ±300 s,
- a body that is not valid JSON.
Why is a bad signature "unknown", not "failed"? An unsigned or forged ok might not come from you, so ZooZ
cannot trust it. But ZooZ cannot assume that nothing happened either. Your wallet may have applied the
transaction and then sent a broken reply. Treating it as unknown resolves both cases safely.
ZooZ resolves an unknown outcome as follows:
| Call | ZooZ's reaction |
|---|---|
| debit | Sends a rollback for the same transactionId, and tells the player that no bet was placed (WALLET_UNAVAILABLE). The debit itself is never retried |
| credit | The outcome stands. ZooZ retries the same credit (same transactionId, same amount, new nonce) until you answer ok |
| rollback | ZooZ retries the same rollback until you answer ok (or TRANSACTION_NOT_FOUND) |
| balance | Nothing moves. The game shows that the wallet is unavailable and the player can try again |
sequenceDiagram
autonumber
participant Z as ZooZ RGS
participant O as Operator wallet
Z->>O: POST /debit { transactionId: "r7-bet", amount: 2.00 }
Note over O: Applied, but the response is lost (or times out)
O--xZ: timeout
Z->>O: POST /rollback { transactionId: "r7-bet", amount: 2.00 }
O->>O: Debit r7-bet exists and is not refunded, so refund 2.00
O-->>Z: 200 { status: "ok" } (signed)
Note over Z: Round cancelled, no outcome was producedsequenceDiagram
autonumber
participant Z as ZooZ RGS
participant O as Operator wallet
Z->>O: POST /debit { transactionId: "r8-bet" }
Note over Z,O: Request lost on the way. The debit never arrives
Z->>O: POST /rollback { transactionId: "r8-bet" }
O->>O: No debit r8-bet, so store a tombstone
O-->>Z: 200 { status: "ok" } (signed)
Z-)O: (late copy) POST /debit { transactionId: "r8-bet" }
O-->>Z: 200 { status: "error", code: "TRANSACTION_ROLLED_BACK" }#Retry timing
| Game type | When ZooZ retries a pending credit or rollback |
|---|---|
| Slots, mines, dice, plinko, crossing | Right away for a rollback after an unknown debit. After that, at the start of the player's next request in the same game and by a background job (first attempt about 30 seconds after the round started, then every 15 seconds, backing off to every 10 minutes while your wallet keeps failing), whether or not the player comes back. Until it succeeds the player cannot start a new round in that game (SETTLEMENT_PENDING or WALLET_UNAVAILABLE) |
| Crash | By a background job, about every 10 seconds, whether or not the player is still connected |
There is no retry limit and no give-up. Expect a credit or rollback to arrive minutes, hours or days after
the original debit, possibly after your game session has expired. Retries always reuse the same
transactionId and amount, and they always use a new nonce and timestamp.
#Business errors: what ZooZ does with each code
A business error is an HTTP 200, signed response with status: "error".
Your code |
on debit | on credit | on rollback |
|---|---|---|---|
INSUFFICIENT_FUNDS |
Bet cancelled. The player sees "insufficient funds" | Retried later | Retried later |
PLAYER_NOT_FOUND, PLAYER_BLOCKED |
Bet refused and round cancelled | Retried later | Retried later |
SESSION_INVALID |
Bet refused and round cancelled | Retried later | Retried later |
WRONG_CURRENCY, LIMIT_EXCEEDED |
Bet refused and round cancelled | Retried later | Retried later |
TRANSACTION_ROLLED_BACK |
Bet refused and round cancelled (the bet does not exist) | Retried later | Retried later |
TRANSACTION_NOT_FOUND |
Bet refused | Retried later | Counts as success: the debit never arrived |
| Any other code | Bet refused and round cancelled | Retried later | Retried later |
After a refused debit, ZooZ does not send a rollback. The debit is final, so nothing needs refunding. On
debit, only INSUFFICIENT_FUNDS is shown to the player as such. Every other refusal is shown as "the wallet
rejected the bet".
For credit and rollback, every error code means "try again later". A win that ZooZ has decided is owed to the player, and a refund is owed as well. So a credit or rollback that passes signature and session checks must never be refused for business reasons (limits, blocked player, expired session). Accept it and apply your limits to the next debit instead.
#4. Idempotency
ZooZ may send the same wallet call more than once: a retry after a timeout, a recovery after a restart, or a duplicate delivery. Your wallet must make repeats harmless:
- The idempotency key is (call type,
transactionId), for example(debit, "…-bet"). A debit and its rollback share the sametransactionIdbut are different call types. - Enforce it with a unique index in your database, not just an in-memory check.
- A repeat never moves money a second time. It returns the first answer: the same status, the same code
and the same
operatorTransactionId, with the current balance. - Idempotency is based on
transactionId, not on the nonce. Every retry has a new nonce, because the signature layer rejects reused nonces as replays. - Store refused debits as well (for example
INSUFFICIENT_FUNDS). That way a late duplicate cannot succeed after ZooZ has already cancelled the round.
Why? Without idempotency, a timeout on a winning credit followed by a retry would pay the win twice. With idempotency, "unknown outcome → retry" is always safe.
#5. Rollback semantics
POST /rollback means: reverse the debit with this transactionId, whatever state it is in. The body carries
the debit's transactionId (for example …-bet, not a new id), its roundId, gameId, session, player,
currency and the debit amount.
| State of the debit at your side | Correct behaviour | Answer |
|---|---|---|
| Debited and not refunded | Refund the amount you stored for the debit | ok with the new balance |
| Already refunded (repeated rollback) | Nothing | The first rollback answer (ok) |
| Debit was refused (for example insufficient funds) | Nothing, because no money moved | ok |
| Never arrived | Store a tombstone for this transactionId |
ok (or TRANSACTION_NOT_FOUND, which ZooZ also accepts as success) |
| Debit arrives after the tombstone | Refuse it without moving money | error / TRANSACTION_ROLLED_BACK |
Rollbacks are sent:
- after a debit with an unknown outcome (all games),
- if a game fails internally after a successful debit, before an outcome exists (all games, rare),
- in crash, when the player cancels a bet during betting, when the bet arrives after betting closed, or when the round is voided after a server restart.
ZooZ never sends a credit for a debit that it rolled back, and never sends a rollback after a credit for the same bet.
#6. Timeouts and performance
| Value | |
|---|---|
| ZooZ timeout per wallet call | 5 s by default (configured per operator). After that, the outcome is unknown |
| Target response time of your wallet | < 500 ms (p99). Players wait for the debit before every spin |
| Signature clock window | ±300 s. Keep your clocks NTP-synchronised |
| Launch code lifetime | 60 s, single use |
| Game session lifetime (ZooZ side) | 8 hours by default, extended on every request |
Concurrency. ZooZ processes one request at a time per player and game, but it does not serialise across games or bets. The same player can have a slot credit, two crash debits (two bet panels) and a background crash settlement in flight at the same moment. Your balance update must be atomic (see rule 3).
#Rules your wallet must follow
These rules are mandatory. Each one exists because its absence caused lost or duplicated money in real provider integrations that ZooZ reviewed.
- Session binding. Look up the session only by
sessionId(never "the player's latest session"). Check thatplayerIdandcurrencymatch that session.- A debit requires a live session.
- A credit or rollback must also be accepted when the session has expired, as long as the round belongs to that session. Wins and refunds are owed even after the player leaves.
- Why: taking the player's most recent session instead of the one the game was opened with lets bets land in the wrong currency or account, and keeps sessions usable forever.
- Idempotency.
(type, transactionId)is unique (database unique index). A repeat never moves money again and returns the first answer (sameoperatorTransactionId) with the current balance. Retries have new nonces, so idempotency must rely ontransactionId, never on the nonce.- Why: retries are how ZooZ makes unknown outcomes safe. Without idempotency, every retry could pay twice.
- Atomicity. The balance update, the ledger entry and the transaction record are written in one database
transaction. Update the balance with a single conditional statement, for example
UPDATE … SET balance = balance + @amount WHERE … AND balance + @amount >= 0. Reading the balance first and writing it back afterwards is forbidden.- Why: read-then-write loses updates when two calls for one player run at the same time, and a crash between separate writes leaves the ledger and the balance out of sync.
- Relationship check. On a credit or rollback, if you have a debit for the same round, it must belong to
the same player. (In crash, many players share a
roundId, so look for this player's debit in that round.)- Why: it stops a signed call for one player's round from paying another player.
- Rollback. If the debit exists and is not refunded, refund it. If it was already refunded, return the first
answer. If the debit never arrived, store a tombstone and return
ok. A debit that arrives later with thattransactionIdis refused withTRANSACTION_ROLLED_BACK.- Why: requests can overtake each other. Without a tombstone, a delayed debit after its rollback would take money for a bet that ZooZ already cancelled.
- Response time. Aim for under 500 ms. ZooZ gives up after 5 s and treats the call as unknown.
The following rules follow directly from how ZooZ handles responses:
- Sign every HTTP 200 answer, business errors included, with a key from your wallet key family. Use the request's nonce. ZooZ ignores the content of anything unsigned or non-200.
- Accept zero-amount credits. They close lost rounds and lost crash bets.
- Never refuse a valid credit or rollback for business reasons. ZooZ retries it until you accept it (see the error table).
- Verify before you parse. Authenticate every wallet request first, in the order given in Signing: key id, time, signature, then nonce. Only then look up the session or the player.
#What ZooZ guarantees
- Globally unique transaction ids:
{roundId}-betand{roundId}-winfor slots, mines and dice, andcrash-{betId}-betandcrash-{betId}-winfor crash. - Exactly one debit per round (per bet in crash), followed by exactly one credit (a zero credit for a loss) or one rollback.
- The outcome is produced only after your debit answered
ok. A rolled-back debit never gets a credit. - Retries are identical. A retried credit or rollback has the same
transactionId,roundId,gameId,sessionId,playerId,currencyandamountas the first attempt. The amount has the same numeric value, and it is always written with the currency's decimal places (2.50), so the text is identical too. Still compare amounts as decimals, never as strings or floats. - Nothing is left open. Credits and rollbacks are retried until they succeed, also after restarts.
- The whole round is capped by the max-win limit. It covers the base spin and every free spin it triggers.
- Your limits are applied before your wallet is called. A bet below your minimum, above your maximum, or whose
largest possible payout exceeds your max win per round is refused with
INVALID_BETand never becomes a debit. The game client only offers the stakes that fit. A bonus buy debits bet × price (for example 100× the bet): its bet follows the same rules, and the debit is only limited by your optional max stake per currency (the largest single debit you accept). Without a max stake, a buy at a bet of 20 debits 2000. Crash is the exception: stakes are not reduced; instead each bet is cashed out automatically when its win reaches your max win, so the payout can never exceed it. A crash round also has a table-wide payout cap per currency; when many large bets are in one round, they are all cashed out automatically at the multiplier where the round total would reach it. - ZooZ never logs secrets or signatures, and signature checks cannot be switched off.
#Error handling on your side
| Situation | What to do |
|---|---|
POST /api/v1/operator/sessions returns 401 |
Check your key id, secret, clock and canonical string (run the samples). The body only contains a generic code, on purpose |
It returns 4xx with a code |
Do not retry blindly. Fix the input (see API reference) |
| It returns 5xx or times out | Safe to retry with a new nonce and timestamp. A failed launch never moves money. Abandon your sessionId or reuse it for the retry |
| The response signature does not verify | Do not use the gameUrl. Treat it as a failed launch and alert |
| A wallet call fails your signature check | Answer 401 with {"status":"error","code":"INVALID_SIGNATURE"} (or STALE_TIMESTAMP / REPLAY). Never include expected values. ZooZ treats this as unknown |
| A wallet call has a malformed body | Answer 400. ZooZ treats this as unknown, and it should never happen |
| Your database is down | Answer 5xx or let the call time out. Never answer ok without committing. ZooZ will retry or roll back |
#Go-live test plan
Run these cases in staging. Each one must give the expected ledger:
| # | Case | Expected result |
|---|---|---|
| 1 | Losing spin | One debit and one credit with amount: 0 |
| 2 | Winning spin with free spins | One debit and one credit with the total win of the round |
| 3 | Same debit delivered twice | Money moves once. The second answer equals the first |
| 4 | Your wallet sleeps 6 s on a debit | ZooZ sends a rollback. The debit is refunded (or tombstoned). Net 0 |
| 5 | Rollback before its debit | Tombstone and ok. The late debit gets TRANSACTION_ROLLED_BACK |
| 6 | Your wallet returns 500 on a credit | ZooZ retries the credit in the background (slots/mines/dice/plinko/crossing: from about 30 s; crash: about every 10 s) and when the player comes back. Paid once |
| 7 | Credit after your session expired | Accepted and paid |
| 8 | Debit after your session expired | SESSION_INVALID. The bet is refused |
| 9 | Crash: two bets in one round, one cashed out, one lost | Two debits and two credits with the same roundId |
| 10 | Crash: bet cancelled during betting | Debit followed by a rollback that refunds it |
| 11 | Request with a wrong signature, an old timestamp or a reused nonce | 401 with the right code. No database lookup of session or player |
| 12 | Balance lower than the bet | INSUFFICIENT_FUNDS, and nothing moves |