Shubdx Integration Guide
Single‑document reference · zero to live on Shubdx
https://api74658.topOne integration gives you access to hundreds of casino games from dozens of studios. Your players' money always lives in your wallet — Shubdx never holds funds.
| Side | Direction | What it is |
|---|---|---|
| Platform API | You → Shubdx | REST API to browse games and open game sessions. |
| Wallet Webhooks | Shubdx → You | HTTP calls to your backend to authorise sessions and move money in real time. |
The player flow in one paragraph: Your backend calls POST /api/v1/sessions, receives a launch_url, and redirects the player there. Shubdx serves the game. As the player plays, Shubdx calls your webhook to debit bets and credit wins. You respond with the new balance. When the player closes the game, they go back to your lobby.
2. Onboarding
When you sign up, our team provisions an operator account and shares:
| Credential | Where you use it |
|---|---|
| API key | X-API-Key header on every Platform API call. |
| Webhook signing secret | Verify the X-Webhook-Signature header on every webhook you receive. |
You provide us with:
| You provide | Purpose |
|---|---|
| Webhook URL | Your HTTPS endpoint where Shubdx delivers wallet events. |
| Allowed currencies | So we activate the right game limits. |
| Default lobby URL | Where the in-game back button sends the player when return_url is omitted. |
3. Environments & base URLs
Shubdx provides the following environments:
| Environment | Base URL | Purpose |
|---|---|---|
| Sandbox | https://api74658.top | Integration testing with demo data |
| Production | https://api74658.top | Live environment |
All API endpoints are relative to the base URL. For example: https://api74658.top/api/v1/games
4. Authentication
Every Platform API request must carry your API key as a request header:
X-API-Key: Provided by the Shubdx
A missing or invalid key returns 401:
{ "error": { "code": 401, "message": "Invalid or missing API key" } }Verify you're connected — make this call right after onboarding:
curl -s https://api74658.top/api/v1/games \ -H "X-API-Key: <YOUR_API_KEY>"
A 200 response with a data array means authentication is working.
5. Conventions
Money
All monetary amounts are integers in the currency's minor units (never floats).
| Currency | Minor unit | Example: €15.50 |
|---|---|---|
| EUR | 1 cent = 1 | 1550 |
Identifiers
BGA-generated identifiers are prefixed strings — always store and compare them as strings:
| Prefix | Resource |
|---|---|
gm_… | Game |
gs_… | Game session |
txn_… | Transaction |
fs_… | Free spins campaign |
Identifiers you generate (and we echo back):
| You generate | Used for |
|---|---|
player_id | Your immutable ID for the player. |
freespins_id | Unique per bonus grant. |
6. Game catalogue
List games
Returns all games available to your account, paginated.
| Param | Type | Default | Description |
|---|---|---|---|
studio | string | — | Filter by studio name (e.g. igrosoft) |
category | string | — | Filter by category (e.g. slots, live-casino) |
page | int | 1 | Page number, 1-based |
per_page | int | 50 | Items per page, max 200 |
Example
curl -s "https://api74658.top/api/v1/games?category=slots&per_page=2" \ -H "X-API-Key: <YOUR_API_KEY>"
Response 200
{
"data": [
{
"id": "gm_4c27e7a7f3c75086a20e",
"slug": "crazymonkey",
"name": "Crazy Monkey",
"studio": "igrosoft",
"category": "slots",
"is_active": true,
"platforms": {
"desktop": true,
"mobile": true
},
"thumbnail_url": "https://api74658.top/images/igrosoft/528-326/crazymonkey"
}
],
"meta": { "total": 243, "page": 1, "per_page": 2, "total_pages": 122 }
}Get a single game details
curl -s https://api74658.top/api/v1/games/crazymonkey \ -H "X-API-Key: <YOUR_API_KEY>"
Unknown slug → 404.
Studio logos
Returns logo URLs for each studio. Use size=big|small and color=white|black|color to get the right variant for your UI.
7. Launching a game session
A session represents one player opening one game. Create it server-side, then redirect the player's browser to the returned launch_url.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
player_id | string | ✅ | Your immutable identifier for the player. |
game_slug | string | ✅ | From the catalogue. |
studio | string | ✅ | From the catalogue. |
currency | string | ✅ | Supported currency (see list below). |
balance | int | ✅ | Player's current balance in minor units (shown in-game on load). |
player_group | string | ❌ | Player segment; defaults to default. |
language | string | ❌ | 2-letter code, defaults to en. |
device | string | ❌ | desktop (default) or mobile. |
return_url | string | ❌ | URL for the in-game back-to-lobby button. Falls back to your account's default lobby URL if omitted. |
freespins_id | string | ❌ | Attach a free spins campaign to this session. |
LIST OF SUPPORTED CURRENCIES
Example
curl -s -X POST https://api74658.top/api/v1/sessions \
-H "X-API-Key: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"player_id": "user_12345",
"game_slug": "crazymonkey",
"studio": "igrosoft",
"currency": "EUR",
"balance": 150000,
"language": "en",
"device": "desktop",
"return_url": "https://yourcasino.com/lobby"
}'Response 201
{
"session_id": "gs_01j9x7p3session000",
"launch_url": "https://api74658.top/play/gs_01j9x7p3session000",
"expires_at": "2026-06-06T18:22:01Z"
}What to do with the response:
- Save
session_id— you'll see it in all subsequent webhook events. - Redirect the player's browser to
launch_url. Shubdx serves the game directly. - Do not cache or reuse
launch_url— it is bound to a single player session.
Session lifetime: sessions stay alive while the game is open.
Errors
| Status | Reason |
|---|---|
404 | game_slug not found in the catalogue. |
422 | Game is temporarily disabled. |
503 | Platform temporarily unavailable — retry with exponential backoff. |
Complete launch flow
8. Demo (fun-play) sessions
Demo sessions use play credits — no real wallet is involved.
| Field | Type | Required | Description |
|---|---|---|---|
game_slug | string | ✅ | From the catalogue. |
studio | string | ✅ | From the catalogue. |
currency | string | ✅ | Currency to display. |
language | string | ❌ | Default en. |
device | string | ❌ | desktop (default) or mobile. |
return_url | string | ❌ | Back-to-lobby URL. |
demo_balance | int | ❌ | Starting demo credits in minor units; default 500000. |
Response 201
{
"launch_url": "https://api74658.top/demo/2f1c9a8b7d6e5f4a",
"expires_at": "2026-06-06T18:22:01Z"
}Demo sessions use a sliding 15-minute expiry — each in-game request resets the timer. The session expires only after 15 minutes of inactivity. No webhook events are sent for demo sessions.
9. Wallet webhooks
While a player is in a game, Shubdx POSTs events to your Webhook URL so you can authorise sessions and move money on your players' wallets in real time.
You implement one HTTPS base URL (the Webhook URL you registered during onboarding). Shubdx POSTs each webhook type to a dedicated sub-path under that URL:
POST {your_webhook_url}/session/verify
POST {your_webhook_url}/balance
POST {your_webhook_url}/bet/create
POST {your_webhook_url}/bet/win
POST {your_webhook_url}/trx/cancel
POST {your_webhook_url}/trx/complete
POST {your_webhook_url}/freerounds/start
POST {your_webhook_url}/freerounds/spin
POST {your_webhook_url}/freerounds/completeEach route must verify the signature on every request.
Webhook envelope
Every webhook has the same JSON envelope:
{
"id": "wh_01j9z3a8delivery00",
"occurred_at": "2026-06-06T14:25:10.412Z",
"session_id": "gs_01j9x7p3session000",
"data": { "...": "route-specific fields" }
}| Field | Description |
|---|---|
id | Unique delivery ID. |
occurred_at | When the event happened (UTC). |
session_id | The game session this event belongs to. |
data | Route-specific payload (see each route below). |
Request headers
| Header | Description |
|---|---|
X-Webhook-Signature | sha256=<hex> — HMAC-SHA256 of the raw body. |
Content-Type | application/json |
Verifying signatures
Every webhook request carries an X-Webhook-Signature header. Its value is sha256=<hex> — an HMAC-SHA256 of the raw request body.
The signing secret is the Webhook Secret issued to you by the Shubdx manager when your account is provisioned. Keep it confidential; treat it like a password.
Compute HMAC-SHA256 over the raw request body bytes using that secret, and compare it to the X-Webhook-Signature header in constant time. Always reject requests where the signature doesn't match.
Python
import hmac
import hashlib
def verify_webhook(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)Node.js
const crypto = require("crypto");
function verifyWebhook(rawBody, header, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}PHP
function verifyWebhook(string $rawBody, string $header, string $secret): bool {
$expected = "sha256=" . hash_hmac("sha256", $rawBody, $secret);
return hash_equals($expected, $header);
}POST session/verify
Called once when the player opens the game. Confirm the player exists and return their current balance.
data
{ "player_id": "user_12345", "currency": "EUR", "game_slug": "crazymonkey" }Your response 200
{ "player_id": "user_12345", "player_group": "vip", "balance": 150000 }If you cannot verify the player, return any non-2xx — the game will not load.
POST balance
Called whenever the game needs a fresh balance to display (e.g. after a spin).
data
{ "player_id": "user_12345", "currency": "EUR", "game_slug": "crazymonkey" }Your response 200
{ "balance": 149500, "currency": "EUR" }POST bet/create
The player placed a bet. Debit the player's wallet and return the new balance.
data
{ "transaction_id": "txn_01j9y2k5bet000000", "player_id": "user_12345", "amount": 500, "currency": "EUR" }Your response 200 (bet accepted)
{ "balance": 149500 }Insufficient funds — return HTTP 402:
{ "error": "insufficient_funds" }Shubdx shows the player a "not enough balance" prompt and keeps the session alive.
transaction_id, return the current balance and do not debit again. Do not return an error.POST bet/win
The round finished with a win. Credit the player's wallet.
data
{ "transaction_id": "txn_01j9y9w4payout000", "player_id": "user_12345", "amount": 1200, "currency": "EUR" }Your response 200
{ "balance": 150700 }Rounds that result in zero win send no bet/win request.
POST trx/cancel
Sent when a bet/create could not be confirmed (e.g. your endpoint timed out). Shubdx has already cancelled the round. If you did debit the player, refund them.
data
{ "transaction_id": "txn_01j9y2k5bet000000", "original_type": "bet", "amount": 500, "currency": "EUR" }Your response 200 — acknowledged.
{}Shubdx retries this request with exponential backoff until you return 2xx.
POST trx/complete
Sent when a bet/win could not be confirmed. Ensures the win is credited exactly once. If you did not yet credit the win, do so now.
data
{ "transaction_id": "txn_01j9y9w4payout000", "original_type": "payout", "amount": 1200, "currency": "EUR" }Your response 200 — acknowledged.
{}Shubdx retries until acknowledged.
Delivery & retry guarantees
| Route class | Delivery | If you fail / time out |
|---|---|---|
session/verify, balance, bet/create, bet/win, freerounds/* | Synchronous — Shubdx waits for your response | For bets: round is cancelled → trx/cancel. For wins: trx/complete. |
trx/cancel, trx/complete | Asynchronous notifications | Retried: 30 s → 2 min → 10 min → 1 h → 24 h |
This guarantees no money is ever lost or double-applied even across network failures.
10. Free spins (bonus rounds)
Free spins let you grant players a number of bonus rounds that are played without deducting from their real balance. The total win is credited at the end.
Attaching a campaign
Pass your freespins_id in the session launch request:
curl -s -X POST https://api74658.top/api/v1/sessions \
-H "X-API-Key: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"player_id": "user_12345",
"game_slug": "crazymonkey",
"studio": "Pragmatic Play",
"currency": "EUR",
"balance": 150000,
"freespins_id": "promo_welcome_42"
}'freespins_id is your identifier for the grant. Use a unique value per player per grant — it prevents the same campaign from being activated twice.
Campaign lifecycle
POST freerounds/start
The player started the bonus. Return spin configuration.
data
{ "freespins_id": "promo_welcome_42", "player_id": "user_12345", "game_slug": "crazymonkey", "currency": "EUR" }transaction_id — none of the freerounds routes carry one. The entire campaign is identified by freespins_id; use it for idempotency checks instead. The session_id is available in the envelope top level if you need to tie the grant to the session.Your response 200
{ "total_spins": 20, "bet_level": 1, "coin_value": 100 }| Field | Type | Meaning |
|---|---|---|
total_spins | int | Number of free spins the engine will execute. |
bet_level | int | An index into the game's internal bet table. Valid values are game-specific — check the game's documentation or use 1 if unsure. |
coin_value | int | Coin denomination in minor units (e.g. 100 = €0.01). |
These three values come from your bonus campaign configuration. Return them as-is — the engine uses them verbatim to configure the spins. You do not calculate the stake yourself; the engine resolves bet_level against the game's own bet table.
If you cannot activate (e.g. promo already used), return a non-2xx — the bonus is cancelled.
POST freerounds/spin
Called once per completed spin. Informational — do not move money here.
data
{ "freespins_id": "promo_welcome_42", "spin_number": 7, "spin_payout": 250, "total_payout": 1750 }Your response 200
{}These are not retried.
POST freerounds/complete
Campaign finished. Credit the total win to the player's wallet.
data
{ "freespins_id": "promo_welcome_42", "player_id": "user_12345", "total_payout": 4750, "currency": "EUR" }Your response 200
{ "balance": 154750 }freespins_id, return current balance without crediting again.11. Querying status
Get a transaction
Every wallet movement carries a transaction_id in the webhook. Look it up for reconciliation.
Response 200
{
"transaction_id": "txn_01j9y2k5bet000000",
"type": "bet",
"amount": 500,
"currency": "EUR",
"status": "completed",
"balance_after": 149500,
"created_at": "2026-06-06T14:25:10Z"
}Transaction types: bet · payout · cancel · finalize
Transaction statuses: processing · completed · rejected · pending_cancel · cancelled · pending_finalize · finalized · stuck
Get a free spins campaign
Response 200
{
"freespins_id": "fs_01j9z8c2promo0000",
"status": "completed",
"total_spins": 20,
"spins_completed": 20,
"last_spin_payout": 500,
"total_payout": 4750,
"currency": "EUR",
"created_at": "2026-06-06T14:30:00Z",
"completed_at": "2026-06-06T14:33:12Z"
}Free spins statuses: pending · activating · active · completing · completed · cancelled
12. Rate limiting
Requests to the Platform API are limited per minute per endpoint. The default limit is configurable per operator — contact us to raise it.
When the limit is exceeded, Shubdx returns:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{ "error": { "code": 429, "message": "Rate limit exceeded" } }Always honour Retry-After. Implement exponential backoff with jitter for retrying 429 and 503 responses.
13. Error reference
Platform API errors
{ "error": { "code": 404, "message": "Game not found" } }| HTTP status | Meaning | Action |
|---|---|---|
401 | Invalid or missing API key | Check X-API-Key. |
402 | Insufficient funds | Player balance too low. |
404 | Resource not found | Verify game_slug, transaction_id, or freespins_id. |
409 | Duplicate transaction / free spins already used | Already processed; idempotency applies. |
422 | Game not available for launch | Game temporarily disabled. |
429 | Rate limit exceeded | Back off; honour Retry-After. |
503 | Platform temporarily unavailable | Retry with exponential backoff. |
500 | Internal error | Retry; contact support if persistent. |
Webhook response errors
When you return non-2xx on a webhook, the effect depends on the event:
| Your non-2xx on | Effect |
|---|---|
POST session/verify | Game does not load. |
POST bet/create (non-402) | Round outcome is undefined; Shubdx cancels and sends trx/cancel. |
POST bet/create with 402 | Clean insufficient-funds rejection; session stays alive. |
POST bet/win | Win delivery retried as trx/complete. |
POST freerounds/start | Bonus is cancelled. |
POST freerounds/complete | Total payout delivery retried. |
14. Integration checklist
Platform API
- Store the API key securely (environment variable or secrets manager).
- Send
X-API-Keyon every request. - Fetch the game catalogue.
- Redirect the player to
launch_url. - Handle
429withRetry-Afterbackoff. - Handle
503with exponential backoff.
Wallet webhooks
- Expose a publicly reachable HTTPS base URL for webhooks.
- Verify
X-Webhook-Signatureon every request — reject mismatches. - Implement
POST session/verify— returnplayer_id,player_group,balance. - Implement
POST balance— returnbalance,currency. - Implement
POST bet/create— debit, return newbalance; return402for insufficient funds. - Implement
POST bet/win— credit, return newbalance. - Idempotency: process each
transaction_idat most once. - Implement
POST trx/cancel— refund the bet if it was debited; acknowledge with{}. - Implement
POST trx/complete— credit the win if not yet done; acknowledge with{}. - Implement free spins routes if you offer bonus campaigns.
Go-live
- Complete end-to-end testing in sandbox (full round cycle: session → bet → payout).
- Verify idempotency: replay the same
transaction_idand confirm no double-charge. - Verify signature rejection: send a request with an invalid signature and confirm your endpoint rejects it.
- Confirm your production webhook URL is reachable from the internet.
- Monitor webhook delivery health for the first 24 h after go-live.
15. Minimal implementation examples
Minimal webhook receiver — Python (FastAPI)
import hmac
import hashlib
from fastapi import FastAPI, Request, Response
app = FastAPI()
WEBHOOK_SECRET = "<your_webhook_signing_secret>"
def verify_signature(raw_body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header or "")
async def verified(request: Request):
raw = await request.body()
if not verify_signature(raw, request.headers.get("X-Webhook-Signature", "")):
raise HTTPException(status_code=401)
return (await request.json())["data"]
@app.post("/Your_slots_folder/session/verify")
async def session_verify(request: Request):
data = await verified(request)
player = db.get_player(data["player_id"])
if not player:
return Response(status_code=404)
return {"player_id": player.id, "player_group": player.group, "balance": player.balance_minor}
@app.post("/Your_slots_folder/balance")
async def balance(request: Request):
data = await verified(request)
balance = db.get_balance(data["player_id"], data["currency"])
return {"balance": balance, "currency": data["currency"]}
@app.post("/Your_slots_folder/bet/create")
async def bet_create(request: Request):
data = await verified(request)
txn_id = data["transaction_id"]
if db.transaction_exists(txn_id):
return {"balance": db.get_balance(data["player_id"], data["currency"])}
if db.get_balance(data["player_id"], data["currency"]) < data["amount"]:
return Response(
status_code=402,
content='{"error":"insufficient_funds"}',
media_type="application/json",
)
return {"balance": db.debit(data["player_id"], data["amount"], txn_id)}
@app.post("/Your_slots_folder/bet/win")
async def bet_win(request: Request):
data = await verified(request)
txn_id = data["transaction_id"]
if db.transaction_exists(txn_id):
return {"balance": db.get_balance(data["player_id"], data["currency"])}
return {"balance": db.credit(data["player_id"], data["amount"], txn_id)}
@app.post("/Your_slots_folder/trx/cancel")
async def trx_cancel(request: Request):
data = await verified(request)
db.maybe_refund(data["transaction_id"], data["amount"])
return {}
@app.post("/Your_slots_folder/trx/complete")
async def trx_complete(request: Request):
data = await verified(request)
db.ensure_credited(data["transaction_id"], data["amount"])
return {}
@app.post("/Your_slots_folder/freerounds/start")
async def freerounds_start(request: Request):
data = await verified(request)
campaign = db.get_campaign(data["freespins_id"])
if not campaign or campaign.used:
return Response(status_code=409)
return {"total_spins": campaign.total_spins, "bet_level": campaign.bet_level, "coin_value": campaign.coin_value}
@app.post("/Your_slots_folder/freerounds/spin")
async def freerounds_spin(request: Request):
await verified(request)
return {}
@app.post("/Your_slots_folder/freerounds/complete")
async def freerounds_complete(request: Request):
data = await verified(request)
if db.campaign_credited(data["freespins_id"]):
return {"balance": db.get_balance(data["player_id"], data["currency"])}
return {"balance": db.credit_campaign(data["player_id"], data["total_payout"], data["freespins_id"])}Minimal session launcher — Python
import httpx
SHUBDX_API_KEY = "<your_api_key>"
SHUBDX_BASE_URL = "https://api74658.top"
def launch_game_session(player_id: str, game_slug: str, studio: str,
currency: str, balance_minor: int, language: str = "en",
device: str = "desktop", return_url: str | None = None) -> dict:
"""Returns { session_id, launch_url, expires_at }."""
resp = httpx.post(
f"{SHUBDX_BASE_URL}/api/v1/sessions",
headers={"X-API-Key": SHUBDX_API_KEY},
json={
"player_id": player_id,
"game_slug": game_slug,
"studio": studio,
"currency": currency,
"balance": balance_minor,
"language": language,
"device": device,
**({"return_url": return_url} if return_url else {}),
},
timeout=10,
)
resp.raise_for_status()
return resp.json()16. Glossary
- Platform API
- REST endpoints you call (browse games, create sessions).
- Wallet Webhooks
- Callbacks from Shubdx to your backend for balance operations.
- session_id
- Unique game session (prefix
gs_). - transaction_id
- Unique bet/win identifier (
txn_) – use for idempotency. - freespins_id
- Your identifier for a bonus grant.
- minor units
- Integer cents (e.g. 1 EUR = 100).