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
  1. Create your own game session first. Its id is the sessionId that 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). One sessionId is for one player, one currency and one game launch.
  2. Call POST /api/v1/operator/sessions with the player, currency and game. Verify the response signature before you use the gameUrl.
  3. Open the gameUrl right 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 a Referer header. If the player opens the game again later, request a new gameUrl.
  4. 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.
  • roundId is the same on the debit and the credit. The transaction ids are {roundId}-bet and {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.
  • roundId is crash-{gameId}-{roundNumber} and is shared by many players and by several bets of one player. Do not assume that one roundId means 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 produced
sequenceDiagram
    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 same transactionId but 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.

  1. Session binding. Look up the session only by sessionId (never "the player's latest session"). Check that playerId and currency match 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.
  2. Idempotency. (type, transactionId) is unique (database unique index). A repeat never moves money again and returns the first answer (same operatorTransactionId) with the current balance. Retries have new nonces, so idempotency must rely on transactionId, never on the nonce.
    • Why: retries are how ZooZ makes unknown outcomes safe. Without idempotency, every retry could pay twice.
  3. 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.
  4. 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.
  5. 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 that transactionId is refused with TRANSACTION_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.
  6. 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:

  1. 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.
  2. Accept zero-amount credits. They close lost rounds and lost crash bets.
  3. Never refuse a valid credit or rollback for business reasons. ZooZ retries it until you accept it (see the error table).
  4. 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}-bet and {roundId}-win for slots, mines and dice, and crash-{betId}-bet and crash-{betId}-win for 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, currency and amount as 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_BET and 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