Casino API · v1
Casino API
The Casino API gives you the providers and games you can offer, and a link that opens a game for one of your players. The player's money is kept in one of two ways. With the transfer wallet, HZURA keeps a casino balance for each player and you move money in and out. With the seamless wallet, the money stays in your own wallet and HZURA asks your wallet for every bet and win.
Overview
You never send an account id: your API key already says who you are. Send only the documented JSON fields; any other field is refused. Launch and wallet requests can be at most 8 KiB.
This API does not create bonuses, jackpots, free spins or promotions. When a game gives a free round or a prize, it is paid like any other bet or win. There is no route to look up a single transaction later.
| Method | Path | What it does |
|---|---|---|
| GET | /api/v1/casino/providers | The providers you can offer, with a logo for each. |
| GET | /api/v1/casino/games | One page of the games you can offer, most popular first. |
| POST | /api/v1/casino/games/{gameId}/launch | A link that opens one game for one player. |
| POST | /api/v1/casino/wallet | Transfer wallet only: check, add, take out or undo money. |
Providers
- API key required
- Not rate limited
Endpoint
/api/v1/casino/providersThe providers you can offer.
Use this before the games list when you want a provider menu. Each id is the provider filter on the games list, written exactly the same, including upper and lower case.
You only see providers that are available to your account. To add or remove one, ask HZURA. No providers is still a success: data is []. The list is complete in one response. There is no page number.
image is a HZURA URL for the provider logo. Show that URL as returned. It is empty until the logo has been stored. Do not add query parameters to it.
Authentication
Your API key, sent from a server on your IP allow-list. Casino always needs a key. See Authentication.
Request
This endpoint does not take parameters or a request body.
Response
200Every provider you can offer.
{
"data": [
{
"id": "provider-1",
"name": "Provider",
"image": "https://pub.example.r2.dev/casino/providers/provider-1/0123456789abcdef.avif"
}
]
}Example request
curl -sS "https://api.hzura.com/api/v1/casino/providers" \
-H "Authorization: Bearer YOUR_API_KEY"Errors
| HTTP | error | When |
|---|---|---|
| 403 | ip_not_allowed | Access refused: check your key and server IP address. |
| 403 | casino_access_disabled | Casino is switched off for your account. |
| 503 | security_config_unavailable | Access could not be checked right now. Try again shortly. |
| 503 | casino_catalog_unavailable | The provider list is not available right now. Try again shortly. |
| 503 | redis_unavailable | Temporarily unavailable, or took too long. Try again shortly. |
Games
- API key required
- Not rate limited
Endpoint
/api/v1/casino/gamesThe games you can offer, most popular first, one page at a time.
You only see games that are live and made available to your account, most popular first. popularity_rank is that place: 0 is the most popular, and null means it has not been ranked yet. Filters and pages keep that order. To add or remove a game provider for your account, ask HZURA.
No games is still a success: data is [] and total is 0. A page past the end also gives an empty data, with the real total.
images is a list of { "type", "url" }. Each url is a HZURA URL. Use it as returned. supported_devices, client_types, supported_currencies, languages, volatility, features and themes describe the game. Some can be empty or missing, so treat them as optional.
Authentication
Your API key, sent from a server on your IP allow-list. Casino always needs a key. See Authentication.
Request
Parameters
| Field | Type | In | Required | Description |
|---|---|---|---|---|
| provider | string | query | No | Only games from this provider. Use an id from GET /api/v1/casino/providers, written exactly the same, including upper and lower case. Anything else is invalid_provider. At most 200 characters. |
| category | string | query | No | Only games in this category. Use an id from categories, written exactly the same. Anything else is invalid_category. At most 200 characters. |
| search | string | query | No | Text to find in the game name or description. Upper and lower case do not matter. At most 100 characters. |
| page | integer | query | No | Page number, 1 or more. Default 1. |
| limit | integer | query | No | Games per page, from 1 to 100. Default 50. |
| currency | string | query | No | Only games that support this currency. It must be one of your account's currencies; upper or lower case both work. At most 16 characters. Without it, your account's main currency is used, if one is set. |
| language | string | query | No | Only games available in this language, such as en-US (en_US works too). A two-letter code like en does not match. At most 32 characters. |
| device | string | query | No | Only games for this device, matched against supported_devices or client_types, such as desktop or mobile. Upper and lower case do not matter. At most 64 characters. |
Response
200One page of games.
{
"data": [
{
"id": "DS-example",
"name": "Example",
"provider_id": "provider-1",
"provider_name": "Provider",
"category": "CASINO/SLOT",
"categories": [{ "id": "SLOT", "name": "Slot" }],
"description": null,
"images": [],
"supported_devices": ["desktop", "mobile"],
"client_types": ["HTML5"],
"supported_currencies": [{ "id": "EUR", "name": "Euro" }],
"languages": [{ "id": "en_US", "name": "English" }],
"demo_support": true,
"free_round_support": false,
"rtp": "96.5",
"volatility": null,
"features": [],
"themes": [],
"release_date": null,
"popularity_rank": 0
}
],
"pagination": { "page": 1, "limit": 1, "total": 1, "total_pages": 1 }
}Example request
curl -sS "https://api.hzura.com/api/v1/casino/games?limit=1" \
-H "Authorization: Bearer YOUR_API_KEY"Errors
| HTTP | error | When |
|---|---|---|
| 400 | invalid_query | A filter is empty or too long. |
| 400 | invalid_page | page is not a whole number of 1 or more. |
| 400 | invalid_limit | limit is not between 1 and 100. |
| 400 | unsupported_currency | This currency is not one of your account's currencies. |
| 400 | invalid_provider | No provider has this id. |
| 400 | invalid_category | No category has this id. |
| 403 | ip_not_allowed | Access refused: check your key and server IP address. |
| 403 | casino_access_disabled | Casino is switched off for your account. |
| 503 | security_config_unavailable | Access could not be checked right now. Try again shortly. |
| 503 | casino_catalog_unavailable | The games list is not available right now. Try again shortly. |
| 503 | redis_unavailable | Temporarily unavailable, or took too long. Try again shortly. |
Launch Game
- API key required
- Not rate limited
Endpoint
/api/v1/casino/games/{gameId}/launchGet a link that opens one game for one player.
gameId is a game's id from the games list. It is 1 to 128 characters: it starts with a letter or digit, then letters, digits, ., _ or -.
The answer is just url. Open it for the player, for example in a new tab or a frame. After the game, the player returns to return_url when you send one. Leave it out and HZURA uses the page set up for your account.
A real launch starts a game session for this player that lasts 6 hours. New bets need a session that has not ended. Wins and refunds for a round that started in time are still paid after it ends. A demo launch is play money: it never touches a wallet.
Authentication
Your API key, sent from a server on your IP allow-list. Casino always needs a key. See Authentication.
Request
Headers
| Field | Type | Required | Description |
|---|---|---|---|
| Content-Type | — | Yes | application/json. A body that is not valid JSON, or any other content type, is 400 invalid_launch. |
| Authorization | — | Yes | Bearer YOUR_API_KEY, or send the key in X-Api-Key. |
Parameters
| Field | Type | In | Required | Description |
|---|---|---|---|---|
| gameId | string | path | Yes | A game id from the games list. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
| player_id | string | Yes | Your own id for this player. 1 to 64 characters: starts with a letter or digit, then letters, digits, ., _ or -. The first successful launch registers the player. |
| currency | string | No | Optional. Three letters, upper or lower case. It must be the player's currency: your account's main currency on the first launch, and the same currency after that. |
| language | string | No | Optional. Only lets the launch work if the game has this language. At most 16 characters. It does not change the language the game opens in; see below. |
| device | string | No | Optional. desktop or mobile, upper or lower case. |
| mode | string | No | Optional. real (the default) or demo, upper or lower case. demo only works when the game has demo_support. |
| display_name | string | No | Optional. The name the player sees inside the game. At most 50 characters, no control characters. Used for this launch only. |
| return_url | string | No | Optional. An http or https page the player returns to after the game. Leave it out, or send it blank, and HZURA uses the page set up for your account. Do not put a username or password in the URL. returnUrl is the same field. |
Response
200The game link.
{
"url": "https://games.example/launch/session"
}Example request
curl -sS -X POST "https://api.hzura.com/api/v1/casino/games/DS-example/launch" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"player_id":"player-1","currency":"EUR","device":"desktop","mode":"real","display_name":"Alex"}'{
"player_id": "player-1",
"currency": "EUR",
"language": "en-US",
"device": "desktop",
"mode": "real",
"display_name": "Alex"
}Errors
| HTTP | error | When |
|---|---|---|
| 400 | invalid_launch | The body, the game id, display_name, or an unknown field cannot be used. |
| 400 | unsupported_currency | The currency is not this player's currency. |
| 400 | player_not_configured | Casino is not fully set up for your account yet (country, currency or language). The player is not registered. Contact HZURA. |
| 403 | ip_not_allowed | Access refused: check your key and server IP address. |
| 403 | casino_access_disabled | Casino is switched off for your account. |
| 403 | game_not_available | This game cannot be opened this way, for example demo on a game without demo_support. |
| 404 | game_not_found | No game has this id. |
| 413 | payload_too_large | The body is over 8 KiB. |
| 502 | launch_authentication_failed | The game could not be opened right now. Try again shortly. |
| 502 | launch_failed | The game refused to open, or gave no link. |
| 503 | security_config_unavailable | Access could not be checked right now. Try again shortly. |
| 503 | launch_not_configured | Game launch is not available right now. Contact HZURA if it continues. |
| 503 | redis_unavailable | Temporarily unavailable, or took too long. Try again shortly. |
| 503 | player_store_unavailable | Players could not be looked up right now. Try again shortly. |
| 503 | wallet_not_configured | Real-money launch is not ready for your account. On the seamless wallet, your wallet URL or signing secret is not set up yet. |
| 503 | wallet_store_unavailable | The game session could not be started. Try again shortly. |
| 503 | casino_catalog_unavailable | The games list is not available right now. Try again shortly. |
| 504 | launch_unavailable | The game took too long to answer. You can safely send the same launch again. |
Language: the game opens in your account's language, set up with HZURA (or HZURA's default if you have none). language in the body is only a check that the game has that language, like the games list filter. en-US and en_US mean the same; a two-letter code like en is not enough.
Device: without device, the game opens for desktop when it supports desktop (or lists no device), otherwise for mobile. If it supports neither, you get game_not_available.
Country and currency: on the first launch the player gets your account's country and main currency. They stay the same for that player from then on.
Players
There is no separate "create player" step. The first successful launch registers your player_id. Every later launch and wallet request uses that same id.
Your players are yours only. The same player_id in another account is a different player, and no one else can see or move your players' money.
The wallet does not register players. Launch a game for a player before using the transfer wallet for them. If a launch fails after the player was registered, just launch again with the same player_id.
On the seamless wallet, HZURA always sends your own player_id to your wallet.
Wallet Modes
Your account uses one wallet type, chosen with HZURA. All of your players use it for every game.
| Wallet type | Where the money is | What you build |
|---|---|---|
| Transfer | HZURA keeps a casino balance for each player. | Move money in and out with POST /api/v1/casino/wallet. Bets and wins change that balance on their own; you are not called. |
| Seamless | In your own wallet. | One HTTPS address that HZURA calls for every balance check, bet, win and refund. See Seamless Wallet below. POST /api/v1/casino/wallet answers wallet_mode_mismatch. |
To switch from transfer to seamless, every player's casino balance must first be taken out. The seamless wallet needs your wallet address and a shared signing secret before real-money games can open. Demo games work with both.
Wallet
This route is the transfer wallet. It moves money between your platform and the player's casino balance at HZURA. On the seamless wallet it answers 409 wallet_mode_mismatch.
operation is exactly BALANCE, DEBIT, CREDIT or ROLLBACK. CREDIT adds money to the player's casino balance. DEBIT takes money out of it, back to you. ROLLBACK undoes one of your own DEBIT or CREDIT requests. BALANCE shows the balance.
While the player plays, bets and wins change the same balance, so it can be different every time you check. Each request either completes fully or is refused. There is never a "pending" result.
A successful answer always has operation, status, currency and balance. balance is a number written as text, without extra zeros: 5.00 is "5" and 12.50 is "12.5". Money requests also return your transaction_id. BALANCE does not.
status is success. If you repeat a request that was later undone, it is rolled_back.
If casino is switched off for your account, CREDIT is refused with casino_access_disabled. BALANCE, DEBIT and ROLLBACK keep working, so players' money can still be taken out.
- API key required
- Not rate limited
Endpoint
/api/v1/casino/walletCheck, add, take out or undo money on a player's casino balance.
Authentication
Your API key, sent from a server on your IP allow-list. Casino always needs a key. See Authentication.
Request
Headers
| Field | Type | Required | Description |
|---|---|---|---|
| Content-Type | — | Yes | application/json. A body that is not valid JSON, or any other content type, is 400 invalid_wallet. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
| player_id | string | Yes | The player_id you used at launch. Same rules as at launch. The wallet does not register players. |
| operation | string | Yes | BALANCE, DEBIT, CREDIT or ROLLBACK, in capitals. Anything else is invalid_wallet. |
| transaction_id | string | No | Needed for DEBIT, CREDIT and ROLLBACK. Your own unique id for this request; never reuse it for a different request, even for another player. 1 to 128 characters, same rules as player_id. |
| amount | string or number | No | Needed for DEBIT, CREDIT and ROLLBACK. Best sent as text, such as "12.50". It must be above zero, with no sign and no e notation, at most 12 digits before the point and 8 after, and at most 21 characters. "05" is not allowed. |
| currency | string | No | Optional. If you send it, it must be the player's currency in three capital letters. eur is invalid_wallet; a different currency is unsupported_currency. |
| reference_transaction_id | string | No | Needed for ROLLBACK. The transaction_id of the successful DEBIT or CREDIT you want to undo, for the same player. 1 to 128 characters. |
Response
200The balance, or a finished money request.
{
"operation": "DEBIT",
"status": "success",
"currency": "EUR",
"balance": "7.5",
"transaction_id": "withdraw-1001"
}Errors
| HTTP | error | When |
|---|---|---|
| 400 | invalid_wallet | The body, amount, operation or currency is not valid, or a rollback amount does not match. |
| 400 | unsupported_currency | The currency is not this player's currency. |
| 403 | ip_not_allowed | Access refused: check your key and server IP address. |
| 403 | casino_access_disabled | CREDIT while casino is switched off, or casino is not set up for your account. |
| 404 | player_not_found | No player with this player_id. Launch a game for them first. |
| 404 | wallet_transaction_not_found | The transaction to undo was not found for this player. |
| 409 | insufficient_balance | Not enough balance for this DEBIT, or to undo this CREDIT. |
| 409 | wallet_mode_mismatch | Your account uses the seamless wallet. |
| 409 | wallet_idempotency_conflict | This transaction_id was already used for a different request. |
| 409 | wallet_already_rolled_back | That transaction was already undone. |
| 409 | wallet_transaction_not_reversible | That transaction failed, or cannot be undone. |
| 413 | payload_too_large | The body is over 8 KiB. |
| 503 | security_config_unavailable | Access could not be checked right now. Try again shortly. |
| 503 | wallet_not_configured | The wallet is not ready for your account. Contact HZURA. |
| 503 | wallet_store_unavailable | Temporarily unavailable. Repeat with the same transaction_id. |
BALANCE
Shows the player's casino balance, after every bet and win so far. A player who has never had money starts at 0.
curl -sS -X POST "https://api.hzura.com/api/v1/casino/wallet" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"player_id":"player-1","operation":"BALANCE"}'{
"operation": "BALANCE",
"status": "success",
"currency": "EUR",
"balance": "0"
}DEBIT
Takes amount out of the player's casino balance, back to you. Add it to the player's account on your platform once the answer arrives.
If the balance is too low, you get insufficient_balance and nothing moves. That transaction_id stays refused; to try a smaller amount, use a new transaction_id.
curl -sS -X POST "https://api.hzura.com/api/v1/casino/wallet" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"player_id":"player-1","operation":"DEBIT","transaction_id":"withdraw-1001","amount":"5.00","currency":"EUR"}'If the balance was 12.5, the answer is:
{
"operation": "DEBIT",
"status": "success",
"currency": "EUR",
"balance": "7.5",
"transaction_id": "withdraw-1001"
}CREDIT
Adds amount to the player's casino balance. Take the money from the player's account on your platform first. The player can bet with it as soon as the answer arrives.
curl -sS -X POST "https://api.hzura.com/api/v1/casino/wallet" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"player_id":"player-1","operation":"CREDIT","transaction_id":"deposit-1001","amount":"12.50","currency":"EUR"}'{
"operation": "CREDIT",
"status": "success",
"currency": "EUR",
"balance": "12.5",
"transaction_id": "deposit-1001"
}ROLLBACK
Undoes one of your successful DEBIT or CREDIT requests for the same player. transaction_id is a new id for the undo itself. reference_transaction_id is the request you are undoing. amount and currency must be the same as in that request; a different amount is 400 invalid_wallet.
Undoing a DEBIT puts the money back on the casino balance. Undoing a CREDIT takes it out again. If the player has already spent part of it, you get insufficient_balance.
Each request can be undone once; a second undo is wallet_already_rolled_back. A refused request cannot be undone (wallet_transaction_not_reversible), and an unknown one is wallet_transaction_not_found. You can only undo your own money requests, not bets or wins from games.
curl -sS -X POST "https://api.hzura.com/api/v1/casino/wallet" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"player_id":"player-1","operation":"ROLLBACK","transaction_id":"rb-1001","reference_transaction_id":"withdraw-1001","amount":"5.00","currency":"EUR"}'{
"operation": "ROLLBACK",
"status": "success",
"currency": "EUR",
"balance": "12.5",
"transaction_id": "rb-1001"
}Safe retries
Your transaction_id makes retries safe. If you send the same request again with the same transaction_id, you get the first answer back and money does not move twice. So after a network error or a 503, simply send the same request again.
| You send again | You get |
|---|---|
The same transaction_id and the same request | The first answer. Money moves only once. |
The same transaction_id with anything changed in the request | 409 wallet_idempotency_conflict |
The same transaction_id after a refusal | The same refusal, such as insufficient_balance. |
Seamless Wallet
With the seamless wallet, the player's money stays with you. You give HZURA one HTTPS address. HZURA sends it a JSON POST for every balance check, bet, win and refund in a game. Your answer is passed to the game, so the player always sees your balance.
Games only wait a short time, so your wallet must answer within 3.5 seconds, and accept the connection within 1 second. Redirects are not followed. An answer bigger than 16 KiB counts as no answer.
Every request is signed with a secret that only you and HZURA know. Refuse any request whose signature does not match.
| Header | Value |
|---|---|
| Content-Type | application/json |
| X-Hzura-Timestamp | The time HZURA sent the request, in Unix seconds. |
| X-Hzura-Signature | HMAC-SHA256 of {X-Hzura-Timestamp}.{raw body} with your signing secret, in lowercase hex. |
import crypto from "node:crypto";
// rawBody is the exact request body bytes, before JSON parsing.
export function isFromHzura(headers, rawBody, secret) {
const timestamp = headers["x-hzura-timestamp"];
const signature = headers["x-hzura-signature"];
if (!timestamp || !signature) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return (
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}Request
The fields have the same names as on POST /api/v1/casino/wallet. Fields that do not apply are left out. New optional fields may be added later, so ignore any field you do not know.
| Field | Sent on | Meaning |
|---|---|---|
operation | Always | BALANCE, DEBIT (take money), CREDIT (give money) or ROLLBACK (refund a DEBIT). |
player_id | Always | Your player id, the one you sent at launch. |
currency | Always | The player's currency, such as EUR. |
transaction_id | DEBIT, CREDIT, ROLLBACK | A unique id for this money movement. Use it to make sure you apply it only once. |
reference_transaction_id | ROLLBACK | The transaction_id of the DEBIT to refund. |
amount | DEBIT, CREDIT, ROLLBACK | A number written as text. Two decimals for normal currencies, up to eight for BTC, ETH, XRP, LTC, BCH, BNB and BUSD. It can be "0.00" for a free-round DEBIT or a losing CREDIT. |
reason | DEBIT, CREDIT, ROLLBACK | BET on DEBIT. WIN or REWARD on CREDIT; REWARD is a promotion or tournament prize. REFUND on ROLLBACK. |
round_id | When known | The game round. One round can have several debits and credits. |
game_id | When known | The game's id, the gameId used at launch. |
round_completed | When known | true when the round is finished. |
bonus | Bonus rounds | An object with type (FREE_ROUND or FEATURE_TRIGGER), and sometimes promo_code and bet_amount (what the free round's bet is worth). |
{
"operation": "DEBIT",
"player_id": "player-1",
"currency": "EUR",
"transaction_id": "7316495845",
"amount": "10.00",
"reason": "BET",
"round_id": "1nnrtsy56",
"game_id": "TK-froggrog",
"round_completed": false
}{
"operation": "ROLLBACK",
"player_id": "player-1",
"currency": "EUR",
"transaction_id": "7316495900",
"reference_transaction_id": "7316495845",
"amount": "10.00",
"reason": "REFUND",
"round_id": "1nnrtsy56",
"game_id": "TK-froggrog",
"round_completed": true
}Response
When it worked, answer 200 with the player's balance after this request. balance is text or a number, and never below zero. You can also send your own transaction_id for this movement (1 to 128 letters, digits, -, _, . or :); it is shown to the game as the reference. Leave it out on BALANCE.
{
"balance": "90.00",
"transaction_id": "your-txn-1001"
}To refuse, answer with a 4xx status and one of the error codes below.
{
"error": "insufficient_balance",
"message": "Balance is too low"
}| `error` | Use it when | What the player sees on a bet |
|---|---|---|
insufficient_balance | The balance is too low for the bet. | Not enough money. |
player_blocked | The player is not allowed to play. | The account is blocked. |
player_not_found | You do not know this player_id. | The bet is refused. |
invalid_request | The request cannot be applied, for example a DEBIT that was already refunded by a ROLLBACK. | The bet is refused. |
A refused DEBIT stays refused. If the same transaction_id comes again, HZURA gives the same refusal itself and does not call you.
Do not refuse a CREDIT or a ROLLBACK: the win or refund has already happened in the game. A refusal counts as no answer, and the same transaction_id is sent again later.
No answer within 3.5 seconds, a 5xx, a redirect, an unknown error code, or an answer that cannot be read all mean "unknown". Nothing is recorded, and the same transaction_id is sent again, or a ROLLBACK follows for that bet.
Rules
- Apply each
transaction_idonly once. If you already applied it, answer200with the balance and the sametransaction_idyou gave the first time. Do not move money again. - A
ROLLBACKrefunds theDEBITnamed inreference_transaction_id. If you never applied that debit, or already refunded it, answer200with the current balance and move no money. - A
ROLLBACKcan arrive before itsDEBIT. Remember the refunded id, and if thatDEBITarrives later, refuse it withinvalid_request. - A
CREDITorROLLBACKcan arrive after the game session ended, or after you blocked the player. Apply it anyway. BALANCEmoves no money. It is sent when a game opens, and whenever the game needs the balance.- Every attempt is signed again with a new
X-Hzura-Timestamp, so a repeatedtransaction_idcomes with a different signature.