API reference
All bodies are JSON (Content-Type: application/json, UTF-8). Field names are camelCase. Production traffic in
both directions uses HTTPS only (TLS 1.2+). For the signature headers and algorithm, see Signing.
| Direction | Endpoint | Auth |
|---|---|---|
| Operator → ZooZ | POST /api/v1/operator/sessions |
Signed with the operator key. Response signed |
| Operator → ZooZ | GET /api/v1/games |
Public |
| Game client → ZooZ | POST /api/v1/sessions/exchange |
Launch code (for reference only, you never call it) |
| ZooZ → Operator | POST {walletUrl}/balance |
Signed with the wallet key. Your response signed |
| ZooZ → Operator | POST {walletUrl}/debit |
Same |
| ZooZ → Operator | POST {walletUrl}/credit |
Same |
| ZooZ → Operator | POST {walletUrl}/rollback |
Same |
#Common types
#Amounts
| Property | Rule |
|---|---|
| JSON type | number in the currency's major unit: 1.5 = EUR 1.50 |
| Precision | Always a whole number of minor units (EUR: cents). Every amount ZooZ sends has at most the currency's decimals of significant precision |
| Fraction digits on the wire | Always the currency's decimal places (EUR: 1.00, 0.20, 2.50), also on retries. Parse amounts as decimals (decimal, BigDecimal, Decimal, a decimal library in JavaScript) and compare them numerically, never as floats |
| Range | debit: > 0. credit: ≥ 0 (0 closes a lost round or bet). rollback: the debit amount (> 0) |
Your balance |
A JSON number in the major unit. Sending it with the currency's decimals (for example 998.00) is recommended. ZooZ truncates anything beyond the currency's decimals |
#Identifiers
| Field | Format | Max length to store |
|---|---|---|
transactionId |
Slots, mines, dice, plinko, crossing: {roundId}-bet, {roundId}-win. Crash: crash-{betId}-bet, crash-{betId}-win (betId is 32 lowercase hex characters). Globally unique |
128 |
roundId |
Slots, mines, dice, plinko, crossing: 32 lowercase hex characters (a UUID without dashes). Crash: crash-{gameId}-{roundNumber}, shared by all bets in that crash round |
128 |
gameId |
Catalog id, for example safari-king, sky-parrot, jungle-mines, monkey-dice, jungle-plinko, jungle-jewels, croc-crossing, tropic-treats |
64 |
sessionId |
Your session id, sent back unchanged. 1–128 characters | 128 |
playerId |
Your player id (a JSON string), sent back unchanged. 1–128 characters | 128 |
currency |
ISO 4217, upper case | 3 |
Treat all ids as opaque strings. The formats above are informative. Do not parse them.
#Operator → ZooZ
#POST /api/v1/operator/sessions
Opens a real-money game session for one of your players and returns a one-time game URL.
Request headers: Content-Type: application/json plus the five request signature headers
(X-ZooZ-Operator, X-ZooZ-Key-Id, X-ZooZ-Timestamp, X-ZooZ-Nonce, X-ZooZ-Signature), signed with an
operator key. The path to sign is /api/v1/operator/sessions, and the query is empty.
Request body:
{
"sessionId": "op-sess-7c1e",
"playerId": "12345",
"nickname": "ahmet",
"currency": "EUR",
"gameId": "safari-king",
"language": "en",
"returnUrl": "https://casino.example/lobby"
}| Field | Type | Required | Rules |
|---|---|---|---|
sessionId |
string | yes | Your own game-session id. 1–128 characters. ZooZ sends it back in every wallet call for this game session |
playerId |
string | yes | Your player id, 1–128 characters. Send a JSON string, even if the id is numeric |
nickname |
string | no | Up to 64 characters. Stored with the session, reserved for display: not shown yet. Crash feeds show a masked playerId |
currency |
string | yes | ISO 4217. Must be enabled for your operator account (case-insensitive) |
gameId |
string | yes | A game id from GET /api/v1/games that is enabled for you (case-insensitive) |
language |
string | no | Preferred UI language: en, de, pt-BR (language with an optional region; pt_br is accepted too). If it is one of the languages enabled for you it is used; otherwise the same language without region (pt-BR → pt), then your default language. Never an error. The game client receives the result (in-game translations are rolled out game by game) |
returnUrl |
string | no | Your lobby URL. Accepted and reserved: not applied yet |
Unknown fields are ignored.
Response 200 (signed with an operator key, see response signature):
{
"gameUrl": "https://games.example/safari-king/?launch=3f9c…e1",
"launchExpiresAt": "2026-09-24T12:01:00.5160000+00:00"
}| Field | Type | Meaning |
|---|---|---|
gameUrl |
string (absolute URL) | Load it in an iframe or redirect to it within 60 seconds. It works once |
launchExpiresAt |
string (ISO 8601 date-time with offset) | When the launch code expires (60 s after creation) |
Errors: HTTP status plus ProblemDetails. Every response except the authentication 401 is signed.
| HTTP | code |
When |
|---|---|---|
| 401 | INVALID_SIGNATURE |
Unknown operator or key id, bad signature, missing nonce, body larger than 64 KiB, or source IP not allowed |
| 401 | STALE_TIMESTAMP |
X-ZooZ-Timestamp is missing or more than 300 s away from ZooZ's clock |
| 401 | REPLAY |
The nonce was already used |
| 403 | OPERATOR_DISABLED |
Your operator account is disabled |
| 400 | INVALID_REQUEST |
sessionId or playerId is missing or too long, or nickname is too long |
| 404 | GAME_NOT_FOUND |
Unknown or missing gameId (or the game has no web client configured) |
| 403 | GAME_NOT_ALLOWED |
The game exists but is not enabled for you |
| 400 | UNSUPPORTED_CURRENCY |
Unknown or missing currency, or one not enabled for you |
| 400 | (see note) | Body is not valid JSON, or a field has the wrong JSON type (for example a numeric playerId) |
| 500 | INTERNAL_ERROR |
Unexpected server error. Safe to retry with a new nonce |
Note: a body that cannot be parsed is rejected with HTTP 400. Depending on the server configuration, the body is either ProblemDetails with
INVALID_REQUESTor empty. Treat a 400 without acodeasINVALID_REQUEST.
The 401 body is not ProblemDetails. It is exactly {"status":"error","code":"…"}.
#GET /api/v1/games
Public, unsigned. It lists the games in the ZooZ catalog. Games that are not enabled for your account are also
listed, and launching them returns GAME_NOT_ALLOWED.
Response 200:
[
{ "id": "safari-king", "type": "slot", "name": "Safari King" },
{ "id": "sky-parrot", "type": "crash", "name": "Sky Parrot" },
{ "id": "jungle-mines", "type": "mines", "name": "Jungle Mines" },
{ "id": "monkey-dice", "type": "dice", "name": "Monkey Dice" },
{ "id": "jungle-plinko", "type": "plinko", "name": "Jungle Plinko" },
{ "id": "jungle-jewels", "type": "slot", "name": "Jungle Jewels" },
{ "id": "croc-crossing", "type": "crossing", "name": "Croc Crossing" },
{ "id": "tropic-treats", "type": "slot", "name": "Tropic Treats" }
]| Field | Type | Values |
|---|---|---|
id |
string | Use it as gameId |
type |
string | slot, crash, mines, dice |
name |
string | Display name |
#POST /api/v1/operator/conformance/runs
Signed with your operator keys, like /operator/sessions. ZooZ runs its wallet conformance test against your
configured wallet URL and returns the report. About 25 signed calls are made: correct ones, and deliberately wrong
ones that your wallet must refuse (wrong or missing signature, unknown key id, timestamps 10 minutes off, body
changed after signing, replayed nonce, a session of another player or currency, repeated debits and credits,
rollbacks before and after a debit, a debit larger than the balance, a negative amount).
The test moves real money on the session you give (debits, credits, rollbacks). A conformant wallet ends where it
started. Use a staging test player with at least 3× amount. At most one run at a time and 10 runs per hour.
Request:
{ "sessionId": "your-test-session", "playerId": "test-player-1", "currency": "EUR", "amount": 1.00 }| Field | Type | Notes |
|---|---|---|
sessionId |
string | A live session of your test player, as you would send it to /operator/sessions |
playerId |
string | The player of that session |
currency |
string | The session's currency |
amount |
number | Optional, default 1.00, max 100 |
Response 200 (signed): passed, warnings, failed, skipped, startBalance, endBalance and checks,
one entry per check:
{
"id": "debit.idempotent",
"category": "Debit",
"title": "Same debit again (new nonce)",
"expected": "status ok, same operatorTransactionId, balance still 49.00",
"outcome": "pass",
"actual": "HTTP 200, status ok, balance 49.00, signed",
"httpStatus": 200,
"elapsedMs": 38,
"request": "POST /wallet/debit\n{…}",
"response": "HTTP 200\n{…}"
}outcome is pass, warn (accepted by ZooZ, but not what the contract asks for, for example the wrong error
code), fail (must be fixed before go-live) or skipped. Go live only with failed: 0.
#Game client → ZooZ (for reference)
#POST /api/v1/sessions/exchange
Called by the game client with the launch parameter of the gameUrl. You never call it. It is described here
so that you know why a gameUrl works only once.
{ "launch": "3f9c…e1" }Response 200: { "token": "…", "expiresAt": "2026-09-24T20:00:00+00:00" }. The token is sent as
Authorization: Bearer on the game endpoints. It expires after 8 hours of inactivity by default.
| HTTP | code |
When |
|---|---|---|
| 401 | SESSION_NOT_FOUND |
The code is unknown, already used, or older than 60 s |
The code is consumed atomically. If two tabs race, exactly one of them wins. The game endpoints behind the token
(/api/v1/game/*, /api/v1/crash/*) are internal to the game clients and not part of this contract.
#ZooZ → Operator: seamless wallet
ZooZ calls POST {walletUrl}/{action}. For example, with the wallet URL https://api.casino.example/zooz/wallet,
a debit goes to https://api.casino.example/zooz/wallet/debit, and the signed path is /zooz/wallet/debit.
Request headers (all four actions):
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
ZooZ-RGS/1.0 (+https://zoozgames.com) |
X-ZooZ-Operator |
Your operator id |
X-ZooZ-Key-Id |
The ZooZ wallet key that signed the call |
X-ZooZ-Timestamp |
Unix seconds |
X-ZooZ-Nonce |
UUIDv4, new for every call and every retry |
X-ZooZ-Signature |
v1= + hex HMAC-SHA256 (Signing) |
Your response (all four actions): HTTP 200 with these headers, signed with a wallet key:
| Header | Value |
|---|---|
Content-Type |
application/json |
X-ZooZ-Key-Id |
Id of the wallet key that you signed with (normally the request's key id) |
X-ZooZ-Timestamp |
Unix seconds, now |
X-ZooZ-Signature |
v1= + hex HMAC-SHA256 over v1\nRESPONSE\n{requestNonce}\n{timestamp}\n{hex(sha256(body))} |
Success body:
{ "status": "ok", "balance": 998.00, "operatorTransactionId": "tx-9876" }Business error body:
{ "status": "error", "code": "INSUFFICIENT_FUNDS", "balance": 0.50 }| Field | Type | Required | Meaning |
|---|---|---|---|
status |
string | yes | ok or error |
balance |
number | yes on ok, recommended on error |
The player's balance after this call, in the major unit. For a replayed call, the current balance |
code |
string | on error |
One of the wallet codes |
operatorTransactionId |
string | recommended on ok |
Your id for the ledger entry. It must be the same on replays. ZooZ currently does not store it, but it helps reconciliation |
Any non-200 status, a missing or invalid signature, a response timestamp outside ±300 s or a malformed body
means outcome unknown. See the integration guide.
For request authentication failures, answer 401 with {"status":"error","code":"INVALID_SIGNATURE"} (or
STALE_TIMESTAMP / REPLAY).
#POST {walletUrl}/balance
When: when a game is opened, and whenever ZooZ needs a fresh balance (for example after a round whose credit is still pending).
{ "sessionId": "op-sess-7c1e", "playerId": "12345", "currency": "EUR" }| Field | Type | Meaning |
|---|---|---|
sessionId |
string | Your session id from the launch |
playerId |
string | Your player id |
currency |
string | The session currency |
Respond ok with balance. For an unknown session, a mismatched player or currency, or an expired session,
respond error with SESSION_INVALID. ZooZ then tells the player that the wallet is unavailable. Nothing is
retried automatically.
#POST {walletUrl}/debit
When: a bet is placed (one per round, or one per crash bet). No outcome exists yet.
{
"transactionId": "01997a3c5e2b7c1d9f00aa11bb22cc33-bet",
"roundId": "01997a3c5e2b7c1d9f00aa11bb22cc33",
"gameId": "safari-king",
"sessionId": "op-sess-7c1e",
"playerId": "12345",
"currency": "EUR",
"amount": 1.00
}| Field | Type | Meaning |
|---|---|---|
transactionId |
string | Idempotency key of this debit |
roundId |
string | Round (crash: the shared crash round) |
gameId |
string | Game id |
sessionId |
string | Your session id. It must be a live session |
playerId |
string | Must match the session |
currency |
string | Must match the session |
amount |
number | The bet, > 0. For a bonus buy (a slot feature bought directly, e.g. Jungle Jewels at 100× the bet) it is the price, bet × cost; for an ante bet spin (e.g. Tropic Treats, 1.25× the bet for a higher free spins chance) it is bet × 1.25. It never exceeds your max stake (if you set one) for the currency |
Take amount atomically if the balance allows it. Otherwise answer INSUFFICIENT_FUNDS and move nothing. If
this transactionId was already rolled back (or tombstoned), answer TRANSACTION_ROLLED_BACK. ZooZ never
retries a debit. A second debit with the same id is a duplicate delivery, so answer it with the first response.
#POST {walletUrl}/credit
When: the round (or crash bet) is settled. It is sent for losses too, with amount: 0. It may arrive long after
the session expired, and it may be retried.
Body: the same fields as the debit, with transactionId ending in -win and amount ≥ 0 (the total win of the
round, including all free spins).
Add amount atomically and answer ok. Accept an expired session (the session must exist, and the player and
currency must match). If you have a debit for this round, it must belong to the same player (in crash: this
player's debit in that round). Any error makes ZooZ retry later, so do not refuse a valid credit.
#POST {walletUrl}/rollback
When: a debit must be reversed, because its outcome was unknown, the game failed before an outcome existed, or (in crash) the bet was cancelled or voided.
{
"transactionId": "01997a3c5e2b7c1d9f00aa11bb22cc33-bet",
"roundId": "01997a3c5e2b7c1d9f00aa11bb22cc33",
"gameId": "safari-king",
"sessionId": "op-sess-7c1e",
"playerId": "12345",
"currency": "EUR",
"amount": 1.00
}transactionId is the id of the debit being reversed. amount is the debit amount. When you refund, use the
amount you stored for the debit. Accept an expired session.
| Situation | Answer |
|---|---|
| Debit found and not yet refunded | Refund it and answer ok |
| Already rolled back | The first answer (ok) |
| Debit was refused earlier | ok, nothing to refund |
| Debit never arrived | Store a tombstone and answer ok (TRANSACTION_NOT_FOUND is also accepted as success) |
#Wallet error codes
code |
Meaning | Typical call |
|---|---|---|
INSUFFICIENT_FUNDS |
Balance is lower than the bet | debit |
PLAYER_NOT_FOUND |
Player does not exist | debit, balance |
PLAYER_BLOCKED |
Player may not play (self-exclusion, compliance) | debit, balance |
SESSION_INVALID |
sessionId is unknown, has expired (debit and balance only), or does not match playerId or currency |
any |
WRONG_CURRENCY |
Currency not allowed for this player | debit |
LIMIT_EXCEEDED |
Responsible-gaming or bet limit reached | debit |
TRANSACTION_ROLLED_BACK |
This debit was already rolled back or tombstoned | debit |
TRANSACTION_NOT_FOUND |
(rollback only) the original debit never arrived. ZooZ treats this as success | rollback |
How ZooZ reacts to each code on each call is described in the integration guide.
#Errors (ProblemDetails)
ZooZ endpoints (except the authentication 401) return errors as RFC 7807
ProblemDetails (Content-Type: application/problem+json) with a stable code extension. Build your logic on
code and the HTTP status. title repeats the code, and detail is human-readable and may change.
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "UNSUPPORTED_CURRENCY",
"status": 400,
"detail": "Currency 'TRY' is not enabled for this operator.",
"code": "UNSUPPORTED_CURRENCY",
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}Quote traceId (when present) in support requests.
#Code catalogue
code |
HTTP | Seen by | Meaning |
|---|---|---|---|
INVALID_SIGNATURE |
401 | Operator | Authentication failed (key, signature, nonce, body size or IP). Plain JSON body, not ProblemDetails |
STALE_TIMESTAMP |
401 | Operator | Timestamp outside ±300 s. Plain JSON body |
REPLAY |
401 | Operator | Nonce already used. Plain JSON body |
OPERATOR_DISABLED |
403 | Operator, game client | Account disabled. Open sessions can finish a running round, but no new bet is taken |
GAME_NOT_ALLOWED |
403 | Operator, game client | Game not enabled for you (also for new bets in a session opened before the game was disabled) |
GAME_NOT_FOUND |
404 | Operator, game client | Unknown game |
UNSUPPORTED_CURRENCY |
400 | Operator, game client | Currency unknown or not enabled |
INVALID_REQUEST |
400 | Operator, game client | Missing or invalid field |
INTERNAL_ERROR |
500 | Everyone | Unexpected error |
SESSION_NOT_FOUND |
401 | Game client | Launch code or session token unknown, used or expired |
SESSION_EXPIRED |
401 | Game client | Game session expired. Open the game again with a new launch |
INVALID_BET |
400 | Game client | Bet not allowed for the currency, outside your limits (min/max bet, max win per round, max stake for a bonus buy), or changed mid-round. Checked before your wallet is called |
INVALID_ACTION |
400 | Game client | Action not valid in the current game state |
INSUFFICIENT_FUNDS |
402 | Game client | Your wallet answered INSUFFICIENT_FUNDS |
CONCURRENT_REQUEST |
409 | Game client | Another request for the same player and game is in progress |
WALLET_UNAVAILABLE |
503 | Game client | Your wallet was unreachable or refused the bet. No bet was placed |
SETTLEMENT_PENDING |
503 | Game client | A previous win is still being paid (credit retry pending) |
BETTING_CLOSED |
409 | Game client (crash) | Betting phase is over. A debited bet is rolled back |
BET_REJECTED |
400 | Game client (crash) | The table refused the bet. A debited bet is rolled back |
CASHOUT_REJECTED |
409 | Game client (crash) | Too late: the round already crashed |
TABLE_UNAVAILABLE |
503 | Game client (crash) | Crash table not running. A debited bet is rolled back |
The game client shows these codes to the player. They are listed here so that your support team can match
player reports to wallet traffic. For example, a spike of WALLET_UNAVAILABLE usually means your wallet is
slow or failing signature checks.