Illapa
// OPERATOR INTEGRATION · v1

Integration guide

A provably-fair crash game over a seamless single-wallet API. Player funds stay with you — the RGS never custodies money. You implement four wallet endpoints; we call them during a round. This page is the full contract.

https://<GAME_HOST>
● all systems operational
provably fair
integer minor units
HMAC-SHA256 · replay-safe

Money & model

You (the operator) hold the licence, KYC/AML and the wallet. The RGS (our remote game server) runs the game. You call us to launch and to verify; we call you to move money.

💶
Money = integer minor units (cents) + an ISO 4217 currency — always JSON integers, never float. Every wallet success returns the post-operation balance.
DIRECTION — who calls whom
operator ─ POST /v1/sessions ─────────▶ RGS       launch (you call us)
player   ─ opens launch_url ──────────▶ RGS       browser / client
RGS      ─ POST {wallet_base}/bet ────▶ operator  debit (we call you)
RGS      ─ POST {wallet_base}/win ────▶ operator  credit
RGS      ─ POST {wallet_base}/rollback▶ operator  void a bet
anyone   ─ GET /v1/verify/{round_id} ─▶ RGS       public provably-fair proof

Signing requests

Every request in both directions carries four headers. The signature covers a hash of the raw body, so it survives proxies that re-serialize. Reject anything older than 30 s or with a replayed nonce, and compare the signature in constant time. TLS required in production.

HEADERS
X-ZS-Account   : <operator_id>
X-ZS-Timestamp : <unix_millis>
X-ZS-Nonce     : <uuid-v4>
X-ZS-Signature : hex( HMAC_SHA256( secret,
                 timestamp + "." + nonce + "." + sha256(raw_body) ) )
import { createHmac, createHash, randomUUID } from "node:crypto";

// secret = launch secret (to the RGS) or wallet secret (verifying us)
function signHeaders(secret, rawBody) {
  const ts = Date.now().toString();
  const nonce = randomUUID();
  const bodyHash = createHash("sha256").update(rawBody).digest("hex");
  const sig = createHmac("sha256", secret)
    .update(`${ts}.${nonce}.${bodyHash}`)
    .digest("hex");
  return {
    "X-ZS-Account": OPERATOR_ID,
    "X-ZS-Timestamp": ts,
    "X-ZS-Nonce": nonce,
    "X-ZS-Signature": sig,
  };
}
VERIFY INBOUND · Node
import { createHmac, createHash, timingSafeEqual } from "node:crypto";

// verify an inbound RGS wallet call with your WALLET secret
function verify(secret, rawBody, h) {
  const ts = Number(h["x-zs-timestamp"]);
  if (Math.abs(Date.now() - ts) > 30_000) return false;   // STALE_REQUEST
  if (seenNonce(h["x-zs-nonce"])) return false;            // replay → STALE_REQUEST
  const bodyHash = createHash("sha256").update(rawBody).digest("hex");
  const expect = createHmac("sha256", secret)
    .update(`${ts}.${h["x-zs-nonce"]}.${bodyHash}`)
    .digest("hex");
  const a = Buffer.from(expect);
  const b = Buffer.from(h["x-zs-signature"] || "");
  return a.length === b.length && timingSafeEqual(a, b);   // constant-time
}

Two secrets

Two separate shared secrets per operator, both rotatable. Keep them distinct — a leaked launch secret must not let anyone sign wallet calls.

launch secret
POST /v1/sessions
operator → RGS (inbound)
wallet secret
/balance /bet /win /rollback
RGS → operator (outbound)
Anti-replay and idempotency are orthogonal: a logical retry reuses the same request_id but with a fresh nonce + timestamp. Dedupe on request_id (money); reject a repeated nonce (auth).
POST/v1/sessionsoperator → RGS· you call ussigned with launch secret

Launch a session

The one call you make to start play. Sign it with your launch secret, then redirect the player to the returned launch_url. Every later wallet call carries this session_id.

Body parameters
operator_id
stringrequired
Must equal the authenticated signer (X-ZS-Account) — otherwise 401 INVALID_SIGNATURE.
player_id
stringrequired
Your opaque id for the player. The RGS never receives PII.
game_id
stringrequired
The identifier we assign you at onboarding; use verbatim.
currency
stringrequired
ISO 4217, 3-letter, e.g. "EUR".
mode
stringrequired
"real" or "demo". demo never touches your wallet endpoints.
lang
stringoptional
UI language, e.g. "en".
country
stringoptional
ISO country, e.g. "BE".
return_url
stringoptional
Where the client returns after play.
deposit_url
stringoptional
Deposit / top-up link shown in-game.
REQUEST BODY
{
  "operator_id": "<operator_id>",
  "player_id":   "op-side-opaque-id",
  "game_id":     "<game_id>",
  "currency":    "EUR",
  "mode":        "real",
  "lang":        "en",
  "country":     "BE",
  "return_url":  "https://<OPERATOR_DOMAIN>/lobby",
  "deposit_url": "https://<OPERATOR_DOMAIN>/deposit"
}
RESPONSE200 OK
{
  "session_id": "ses_9f8c…",
  "launch_url":
    "https://<GAME_HOST>/?session=ses_9f8c…",
  "expires_at": 1750000000000
}
Session TTL defaults to 3600 s. Expiry is lazy — a call on a dead session is rejected and no money is routed.
mode:"demo" never calls your wallet endpoints.
GET/v1/verify/{round_id}public · no auth· anyone

Verify a round

Public, no HMAC. Anyone — you or a player — can independently prove a round. round_id must be digits. No money fields are ever exposed.

Path parameter
round_id
pathrequired
Digits only. The round to prove.
RESPONSE200 OK
{
  "round_id": "12345",
  "game_id": "<game_id>",
  "engine_id": "<game_id>",
  "engine_version": "…",
  "config_hash": "…",
  "server_seed_commit": "…",
  "server_seed_reveal": "…",   // null until crash
  "client_seed": "…",
  "nonce": 0,
  "crash_point": 2.5           // null until crash
}
server_seed_reveal and crash_point are null until the round has crashed — revealing early would leak the result.
After crash: sha256(server_seed_reveal) === server_seed_commit, and crash_point recomputes from the revealed seed + client_seed.
Unknown round → 404 SESSION_NOT_FOUND.
POST{wallet_base}/balanceRGS → operator· you implementsigned with wallet secret

Balance

The RGS asks you for the current wallet balance for an in-play session. Verify the signature with your wallet secret; return the balance in minor units.

REQUEST BODY
{
  "request_id": "…",
  "session_id": "…",
  "player_id":  "…",
  "game_id":    "<game_id>",
  "currency":   "EUR",
  "ts":         1750000000000
}
RESPONSE200 OK
{ "balance": 150000, "currency": "EUR" }
POST{wallet_base}/betRGS → operator· you implementsigned with wallet secret

Bet — debit

Fires when the round is derived. Debit the player and return the post-operation balance. Idempotent on request_id — a replay returns the same result and never double-debits.

Notable fields
request_id
stringrequired
RGS-generated, globally unique — the ONLY dedupe key.
amount
integerrequired
Stake in minor units. Reject unaffordable with 402 INSUFFICIENT_FUNDS.
ts
integerrequired
Unix millis — part of the signed body.
REQUEST BODY
{
  "request_id": "…",
  "round_id":   "…",
  "session_id": "…",
  "player_id":  "…",
  "game_id":    "<game_id>",
  "currency":   "EUR",
  "amount":     10000,
  "ts":         1750000000000
}
RESPONSE200 OK
{
  "request_id": "…",
  "balance":    140000,
  "currency":   "EUR",
  "status":     "OK"
}
Reject an unaffordable bet with 402 INSUFFICIENT_FUNDS — definitive, no retry.
POST{wallet_base}/winRGS → operator· you implementsigned with wallet secret

Win — credit

Fires at settlement. Always sent — even amount:0 on a loss — so every round closes with a clean bet+settle pair. Idempotent on request_id; the RGS retries until acknowledged, so a credit is never dropped.

Notable fields
ref_bet
stringrequired
The bet's request_id this win settles.
amount
integerrequired
Payout in minor units. 0 on a loss (still sent).
multiplier_bp
integerrequired
Payout multiplier in basis points (25000 = 2.5×).
round_final
booleanrequired
Marks the last settlement of the round.
REQUEST BODY
{
  "request_id":    "…",
  "round_id":      "…",
  "ref_bet":       "<bet request_id>",
  "session_id":    "…",
  "player_id":     "…",
  "game_id":       "<game_id>",
  "currency":      "EUR",
  "amount":        25000,
  "multiplier_bp": 25000,
  "round_final":   true,
  "ts":            1750000000000
}
RESPONSE200 OK
{
  "request_id": "…",
  "balance":    165000,
  "currency":   "EUR",
  "status":     "OK"
}
/win never fails terminally. The RGS retries with backoff until you ack — a credit survives restarts.
POST{wallet_base}/rollbackRGS → operator· you implementsigned with wallet secret

Rollback — void a bet

Voids a previously applied bet. Its own request_id is derived as <bet request_id>:rollback; ref_tx is the bet being reversed. Note there is no game_id in this body.

Notable fields
request_id
stringrequired
Derived: "<bet request_id>:rollback".
ref_tx
stringrequired
The bet request_id to reverse.
amount
integerrequired
The bet amount to give back, in minor units.
REQUEST BODY
{
  "request_id": "<bet request_id>:rollback",
  "ref_tx":     "<bet request_id to reverse>",
  "round_id":   "…",
  "session_id": "…",
  "player_id":  "…",
  "currency":   "EUR",
  "amount":     10000,
  "ts":         1750000000000
}
RESPONSE200 OK
{
  "request_id": "…",
  "balance":    150000,
  "status":     "OK"      // or "NOOP"
}
NOOP rule: if the referenced bet never applied (timed out and never landed) or is already voided, rollback is a no-op success with status:"NOOP" — not an error. Return the current balance.

Idempotency & retries

The money-critical core. request_id is generated by the RGS, globally unique, and is the only dedupe key. Persist processed request_ids and, on replay, return the original stored result without re-applying.

How the RGS classifies your response
200 + numeric balance
success
Success. Money moved (or was already applied on a replay).
5xx or code INTERNAL
ambiguous
Ambiguous — the RGS retries the same request_id.
4xx business error
definitive
Definitive — you rejected; no money moved.
timeout / network error
ambiguous
Ambiguous — the RGS retries.
/bet ambiguous, retries exhausted
The RGS issues /rollback (NOOP-safe), then fails the bet to the player. A player is never debited for a round that didn't run.
/win never fails terminally
The RGS retries with backoff until acknowledged. A credit not yet acked is retried later and survives restarts.

Round lifecycle

One round = exactly one /bet and one /win (plus at most one /rollback voiding the bet). That pairing is the reconciliation key.

ONE ROUND
client place_bet(amount)
  │
  ├─▶ /bet (debit)           ── fails ▶ reject bet to client (no round)
  │      │ OK
  ├─▶ engine derives round   ── errors ▶ /rollback(bet) ▶ reject
  │      │ crash point committed · round state frozen
  │   … round plays; player cashes out, or it busts …
  ├─▶ engine settles → outcome
  └─▶ /win (credit, amount maybe 0, round_final:true)  ── retries until acked

Reconciliation

Both sides ledger every transaction. Reconcile by round_id: each round shows the RGS's (bet, win[, rollback]) against your wallet movements; any mismatch is alerted. See the worked example below.

Error codes

Any non-OK body is { "error": { "code":"…", "message":"…" } } with an appropriate 4xx/5xx. The 10 canonical codes:

INVALID_SIGNATUREBad HMAC, or unknown/inactive account.
STALE_REQUESTTimestamp outside the 30 s window, or a replayed nonce.
SESSION_EXPIREDSession is past its TTL.
SESSION_NOT_FOUNDUnknown session_id (or unknown round on verify).
INSUFFICIENT_FUNDSPlayer cannot cover the bet.
PLAYER_LOCKEDPlayer is blocked on your side.
CURRENCY_MISMATCHRequest currency ≠ session/wallet currency.
TX_ALREADY_PROCESSEDInformational; return the original stored result.
MALFORMED_REQUESTBad JSON, missing field, or non-integer amount.
INTERNALYour transient failure — the RGS treats it as ambiguous and retries.

Onboarding

The RGS resolves the operator per X-ZS-Account on every inbound call. We set up your integration record together — the two secrets are generated and exchanged over a secure channel, your wallet base URL is registered, and your account is activated. Secrets are stored encrypted and rotatable at any time.

account id
Matches X-ZS-Account on every inbound call.
launch secret
Verifies your POST /v1/sessions.
wallet secret
Signs the RGS outbound wallet calls to you.
wallet base URL
Where the RGS sends /bet /win /rollback /balance.
active flag
An unknown OR inactive account is rejected identically (401 INVALID_SIGNATURE).

Worked example

One session → one bet → one win → reconciliation. Player starts with 10 000.00 EUR (1000000), bets 10.00 (1000), cashes out at 2.00× → win 20.00 (2000).

1
Launchoperator → RGS, launch secret
POST /v1/sessions {operator_id, player_id:"p-42", game_id, currency:"EUR", mode:"real"} → { session_id, launch_url, expires_at }
2
BetRGS → operator, wallet secret
POST /bet {request_id:"r1", amount:1000} → debit → { balance:999000, status:"OK" }. 1000000 − 1000 = 999000.
3
WinRGS → operator, wallet secret
POST /win {request_id:"r2", ref_bet:"r1", amount:2000, multiplier_bp:20000, round_final:true} → credit → { balance:1001000 }. 999000 + 2000 = 1001000.
4
Reconcileboth ledgers
start − bet + win = 1000000 − 1000 + 2000 = 1001000 ✓ — matches your wallet.
5
Verify (public)anyone
GET /v1/verify/{round_id} → sha256(server_seed_reveal) === server_seed_commit, crash_point recomputes.
Want the sandbox?
An integration sandbox with a reference wallet is available on request — run the full cycle before you write yours.
Request sandboxFor operators