01 Overview
Your backend calls one endpoint on us before ever showing the player anything. From there, the game itself talks to our RGS, and our RGS is the only thing that ever calls your wallet — always signed, always over plain HTTPS.
- Your server calls
POST /api/launchwith a bearer token we issue you, and gets back alaunchUrl. - Your browser puts
launchUrlin an iframe. It carries a signed, short-lived session token — the raw player id never crosses the browser. - Every spin, our RGS calls your wallet: a signed
bet, then a signedwinif the spin paid. You never call us — we call you.
02 Launch API
POST /api/launch — server-to-server. Call this before rendering the game; the response is a URL you put straight into an iframe.
Auth
Authorization: Bearer <LAUNCH_SECRET — issued to you> — a shared secret we issue out of band. A missing or incorrect bearer is rejected before anything else is validated.
Request body
{
"playerId": "string",
"currency": "string",
"gameCode": "string",
"lang": "string (optional)",
"mode": "real (optional, default)"
}Response — 200
{ "launchUrl": "string" }Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid bearer token. |
400 | Unknown game code, game not allowlisted for your account, or currency doesn’t match your configured currency. |
Example
curl -s -X POST "$STUDIO/api/launch" \
-H "Authorization: Bearer $LAUNCH_SECRET" -H 'content-type: application/json' \
-d '{"playerId":"player-1","currency":"USD","gameCode":"gilded-reel"}'launchUrl expires 3,600,000ms after it’s issued. Once it does, spin and balance calls return 401 — just call launch again for a fresh token. lang and mode are accepted but currently ignored; there is no returnUrl and no demo mode — every launched session settles real wallet callbacks from the first spin.03 Wallet API you implement
You host four endpoints; we call them, signed, over HTTPS POST. Every one returns HTTP 200 with the outcome carried in the response body — see Status Codes before you wire up error handling.
POST /api/wallet/user/balance
Request
{ "user": "string", "currency": "string", "supplier_user": "string" }Response — 200
{ "status": "RS_OK", "user": "string", "balance": 125000 }POST /api/wallet/transaction/bet
Request
{
"user": "string",
"currency": "string",
"amount": 125000,
"transaction_uuid": "string",
"round_id": "string",
"game_code": "string",
"supplier_user": "string"
}Response — 200
{ "status": "RS_OK", "user": "string", "balance": 125000, "transaction_uuid": "string" }POST /api/wallet/transaction/win
Same request and response shape as bet above.
POST /api/wallet/transaction/rollback
Request
{ "user": "string", "transaction_uuid": "string", "supplier_user": "string" }Response — 200
{ "status": "RS_OK", "user": "string", "balance": 125000, "transaction_uuid": "string" }supplier_user identifies us to you on every call — use it to look up our registered public key (see Signing). currency is required on every call but fixed to one value per operator account.04 Signing
Every wallet request we send is signed with RSA-SHA256 over the raw request body bytes, base64-encoded, carried in the X-Hub88-Signature header. Verify it against our registered public key before trusting the request.
req.text()), never a parsed-and-re-serialized copy. Key order and whitespace are part of what was signed; re-stringifying the parsed JSON will not reproduce the same signature.Worked example
Body (the literal bytes that were signed)
{"user":"player-1","currency":"USD","amount":125000,"transaction_uuid":"11111111-1111-1111-1111-111111111111","round_id":"22222222-2222-2222-2222-222222222222","game_code":"gilded-reel","supplier_user":"studio-demo"}ChiefGaming public key (PEM, SPKI)
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtRHBX3K1QN8VvTqZFcbT eqOnBCBTPm0KM+sEP/nPkkI26ngzzzcGwNqg5xx/vb0Q3oxsm0YDCKuGARS4YXWw nU+XiTwue5l2cHkFMSodbZ2cWruf4eCLI/2bhJzabMX9Xmdsl3pvSqKIFzZ+CZrR +YeahFbB7gLzhLHO5Mt5aVdnnyzdt1MLxBNQMkN6o+Uah4NKRS8GSiU65aDN/yMb yrHsyBaWAEogQlyk4a/i9A92lC1Ye4+q/6ep7gZqttfFxm2FVLb4dXI4SYKOqyUt KUTFaFkeosHJVoAhpI5+YuC1a2ePFlWJhwIw3/ZOXD07+uLzmoPJyJi5T6z3al4Q ywIDAQAB -----END PUBLIC KEY-----
Resulting X-Hub88-Signature (base64)
ppA+Nd2Qsjst+VucojeHEnmecjpttIj0s/FKiEaM5Qt1pl0ydwmR5M7pDtsvGb9sl08isG2ShUWI0aMxD1p6NjdaFgASUcgWDBlf3QozkuELQEzojVA3F7kNIHPWEePyXZjCm614d3m6R2+I0q9Hi0n8LAv1ad9ZZ0xBUMwc8GagA9W2q9nSSFpanD/G5jjZ/Tl23mXpPi61vpzMVgRS6Xlvfd3A2I3MKQpYPWg7nYTZVgN3RDpD1SSArqU2mQ2nQelyWUGJDn8WtA/pIuHz35fRxt5rl/2hi5yMnYg1deOhtHXRggszvcWNDMTPhmh7AqbUkYvvS/2U7uFxm2AQNA==
This exact triple is a pinned test vector: reproduce the signature from the body above against the public key and it must verify; tamper with a single byte of the body and it must fail. Your own private key never leaves us — you only ever need the public key, to verify.
05 The closed status set
Every wallet response’s status field must be exactly one of these ten values — a closed set, not an example to extend:
status is not a rejection — it’s indeterminate: we can’t tell whether you applied the operation, and we treat it as distinct from a definite RS_ERROR_* result. Always respond 200 and put the outcome in status — never encode it as an HTTP status code.06 Idempotency
The same transaction_uuid must apply at most once.
- A retried
betorwinwith atransaction_uuidyou’ve already processed must returnRS_OKwith the original resulting balance — neverRS_ERROR_DUPLICATE_TRANSACTIONfor a same-uuid replay. rollbackreverses a specificbetby itstransaction_uuidand is itself idempotent — rolling back the same uuid more than once must be safe and must never double-reverse the balance.- The bet-after-rollback rule. If a rollback for a
transaction_uuidarrives before the matchingbetever does (out of order), the latebetmust respondRS_OKwith the balance unchanged — notRS_ERROR_TRANSACTION_ROLLED_BACK. The only invariant we require is that the bet never applies after a rollback of the same uuid; an unchanged-balanceRS_OKsatisfies that.
07 Money & sessions
Money scale
Every amount on the wire is an integer, scaled ×100,000. 1.25 units of currency is transmitted as the integer 125000. Range-guard every conversion at the JSON boundary — reject non-integers and anything outside a safe integer range on your side, exactly as we do on ours.
Session TTL
The session token in a launchUrl is valid for 1 hour from issuance. After that, spin and balance calls return 401 — call /api/launch again for a new session; there is nothing to refresh in-place.
Ready to see it end to end?
Play the live demo, or get in touch to have a launch secret issued.