Shubdx Integration Guide

Single‑document reference · zero to live on Shubdx

api Base URL: https://api74658.top
hub 1. How it works

One 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.

┌────────────────┐ Platform API (REST) ┌────────────────────────┐ │ │ ─────────────────────►│ │ │ Your Backend │ │ Shubdx Platform │ │ │ ◄─────────────────────│ │ └────────────────┘ Wallet Webhooks └────────────────────────┘ ▲ │ │ player's browser │ serves the game └──────────────── launch_url ───────────────┘
SideDirectionWhat it is
Platform APIYou → ShubdxREST API to browse games and open game sessions.
Wallet WebhooksShubdx → YouHTTP 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:

CredentialWhere you use it
API keyX-API-Key header on every Platform API call.
Webhook signing secretVerify the X-Webhook-Signature header on every webhook you receive.

You provide us with:

You providePurpose
Webhook URLYour HTTPS endpoint where Shubdx delivers wallet events.
Allowed currenciesSo we activate the right game limits.
Default lobby URLWhere the in-game back button sends the player when return_url is omitted.
warning API keys are shown once. At creation time the key is displayed in full and then stored only as a hash on our side. Copy it immediately and keep it secret.

3. Environments & base URLs

Shubdx provides the following environments:

EnvironmentBase URLPurpose
Sandboxhttps://api74658.topIntegration testing with demo data
Productionhttps://api74658.topLive 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).

CurrencyMinor unitExample: €15.50
EUR1 cent = 11550

Identifiers

BGA-generated identifiers are prefixed strings — always store and compare them as strings:

PrefixResource
gm_…Game
gs_…Game session
txn_…Transaction
fs_…Free spins campaign

Identifiers you generate (and we echo back):

You generateUsed for
player_idYour immutable ID for the player.
freespins_idUnique per bonus grant.

6. Game catalogue

List games

GET /api/v1/games

Returns all games available to your account, paginated.

ParamTypeDefaultDescription
studiostringFilter by studio name (e.g. igrosoft)
categorystringFilter by category (e.g. slots, live-casino)
pageint1Page number, 1-based
per_pageint50Items 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

GET /api/v1/games/{slug}
curl -s https://api74658.top/api/v1/games/crazymonkey \
  -H "X-API-Key: <YOUR_API_KEY>"

Unknown slug → 404.

Studio logos

GET /api/v1/providers
GET /api/v1/providers/{provider}/logo?size=big&color=white

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.

POST /api/v1/sessions

Request body

FieldTypeRequiredDescription
player_idstringYour immutable identifier for the player.
game_slugstringFrom the catalogue.
studiostringFrom the catalogue.
currencystringSupported currency (see list below).
balanceintPlayer's current balance in minor units (shown in-game on load).
player_groupstringPlayer segment; defaults to default.
languagestring2-letter code, defaults to en.
devicestringdesktop (default) or mobile.
return_urlstringURL for the in-game back-to-lobby button. Falls back to your account's default lobby URL if omitted.
freespins_idstringAttach a free spins campaign to this session.

LIST OF SUPPORTED CURRENCIES

ADAAEDAFNALL AMDANGAOAARS AUDAWGAZNBAM BBDBCBDTBET BGNBHDBIFBMD BMZBNBBNDBOB BOVBRLBSDBTC BTNBWPBYNBYR BZDCADCDFCHE CHFCHWCLFCLP CNYCOPCOUCRC CUCCUPCVECZK DJFDKKDLSDOGE DOPDZDEGPERN ETBETHEURFJD FKPFUNGBPGC GELGHSGIPGMD GNFGTQGYDHKD HNLHRKHTGHUF IDRILSINRIQD IRRIRR2IRTISK JCJMDJODJPY KESKGSKHRKMF KPWKRWKRW2KWD KYDKZTLAKLBP LKRLRDLSLLTC LYDMADMDLMGA MKDMMKMNTMOP MROMURMVRMWK MXNMXVMYRMZN NADNGNNIONOK NPRNZDOMRPAB PENPGKPHPPKR PLNPYGQARRBX RONRSDRUBRWF SARSBDSBSSC SCRSDGSEKSGD SHPSLLSOLSOS SRDSSPSTDSVC SYPSZLTHBTJS TKNTMTTNDTOM TOPTRXTRYTTD TWDTZSUAHUGX USDUSDCUSDTUSN UYIUYUUZSVEF VNDVUVWSTXAF XCDXOFXPFXRP YERZARZMWZWL mBCHmBTCmDASHmETH mLTCmXMRuBTCuETH uLTC

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:

  1. Save session_id — you'll see it in all subsequent webhook events.
  2. Redirect the player's browser to launch_url. Shubdx serves the game directly.
  3. 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

StatusReason
404game_slug not found in the catalogue.
422Game is temporarily disabled.
503Platform temporarily unavailable — retry with exponential backoff.

Complete launch flow

Player Your Backend Shubdx │ │ │ │ POST /api/v1/sessions│ │ │──────────────────────►│ │ │ │ POST /sessions (create) │ │ │───────────────────────────►│ │ │ 201 { session_id } │ │ │◄───────────────────────────│ │ 201 { launch_url } │ │ │◄──────────────────────│ │ │ │ │ │ GET launch_url (browser redirect) │ │───────────────────────────────────────────────────►│ │ game HTML / JS / assets │ │◄───────────────────────────────────────────────────│ │ │ │ │ ─ ─ ─ POST session/verify ─ ─ ─ ─ ─ ─►│ │ │◄───────────────────────────│ (Shubdx → Your Backend) │ │ { player_id, player_group,│ │ │ balance } │ │ │───────────────────────────►│ │ (game loads) │ │ │ │ │ │ spin (in-browser) │ │ │───────────────────────────────────────────────────►│ │ │◄───────────────────────────│ POST bet/create │ │ { balance } (debit ok) │ │ │───────────────────────────►│ │ │◄───────────────────────────│ POST bet/win │ │ { balance } (credit ok) │ │ │───────────────────────────►│ │ show result │ │ │◄───────────────────────────────────────────────────│
All financial operations (session verify, bet, balance) are Shubdx → Your Backend callbacks. Your backend is the single source of truth for player balances.

8. Demo (fun-play) sessions

Demo sessions use play credits — no real wallet is involved.

POST /api/v1/sessions/demo
FieldTypeRequiredDescription
game_slugstringFrom the catalogue.
studiostringFrom the catalogue.
currencystringCurrency to display.
languagestringDefault en.
devicestringdesktop (default) or mobile.
return_urlstringBack-to-lobby URL.
demo_balanceintStarting 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/complete

Each 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" }
}
FieldDescription
idUnique delivery ID.
occurred_atWhen the event happened (UTC).
session_idThe game session this event belongs to.
dataRoute-specific payload (see each route below).

Request headers

HeaderDescription
X-Webhook-Signaturesha256=<hex> — HMAC-SHA256 of the raw body.
Content-Typeapplication/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);
}
Verify against the raw bytes you received before any JSON parsing or re-serialisation. Reformatting the body will break the signature check.

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.

Idempotency: if you have already processed this 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.

Idempotency: same rule — if already credited, return current balance.

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 classDeliveryIf you fail / time out
session/verify, balance, bet/create, bet/win, freerounds/*Synchronous — Shubdx waits for your responseFor bets: round is cancelled → trx/cancel. For wins: trx/complete.
trx/cancel, trx/completeAsynchronous notificationsRetried: 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

You: POST /api/v1/sessions { freespins_id: "promo_welcome_42" } │ ▼ ┌── POST freerounds/start ──────► you return { total_spins, bet_level, coin_value } │ ├── POST freerounds/spin ────────► (one per spin; informational; just acknowledge) ├── POST freerounds/spin │ … (N times) │ └── POST freerounds/complete ────► you credit total_payout, return { balance }

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" }
No 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 }
FieldTypeMeaning
total_spinsintNumber of free spins the engine will execute.
bet_levelintAn index into the game's internal bet table. Valid values are game-specific — check the game's documentation or use 1 if unsure.
coin_valueintCoin 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 }
Idempotency: if already credited this freespins_id, return current balance without crediting again.

11. Querying status

Get a transaction

GET /api/v1/transactions/{transaction_id}

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

GET /api/v1/freespins/{freespins_id}

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 statusMeaningAction
401Invalid or missing API keyCheck X-API-Key.
402Insufficient fundsPlayer balance too low.
404Resource not foundVerify game_slug, transaction_id, or freespins_id.
409Duplicate transaction / free spins already usedAlready processed; idempotency applies.
422Game not available for launchGame temporarily disabled.
429Rate limit exceededBack off; honour Retry-After.
503Platform temporarily unavailableRetry with exponential backoff.
500Internal errorRetry; contact support if persistent.

Webhook response errors

When you return non-2xx on a webhook, the effect depends on the event:

Your non-2xx onEffect
POST session/verifyGame does not load.
POST bet/create (non-402)Round outcome is undefined; Shubdx cancels and sends trx/cancel.
POST bet/create with 402Clean insufficient-funds rejection; session stays alive.
POST bet/winWin delivery retried as trx/complete.
POST freerounds/startBonus is cancelled.
POST freerounds/completeTotal payout delivery retried.

14. Integration checklist

Platform API

  • Store the API key securely (environment variable or secrets manager).
  • Send X-API-Key on every request.
  • Fetch the game catalogue.
  • Redirect the player to launch_url.
  • Handle 429 with Retry-After backoff.
  • Handle 503 with exponential backoff.

Wallet webhooks

  • Expose a publicly reachable HTTPS base URL for webhooks.
  • Verify X-Webhook-Signature on every request — reject mismatches.
  • Implement POST session/verify — return player_id, player_group, balance.
  • Implement POST balance — return balance, currency.
  • Implement POST bet/create — debit, return new balance; return 402 for insufficient funds.
  • Implement POST bet/win — credit, return new balance.
  • Idempotency: process each transaction_id at 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_id and 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).