--- title: ScatterKings for Operators description: Add ScatterKings slot games to your casino through OneHazel — a technical integration guide. Implement OneHazel's vendor-neutral wallet contract, sync the catalogue, and wire game launch. --- # ScatterKings for Operators Add **ScatterKings** (SK) slot games to your casino through OneHazel. **The integration direction:** OneHazel runs the ScatterKings connector — it speaks SK's protocol, signs and verifies SK's callbacks, and routes them. **Your job is to expose a wallet OneHazel can settle play against, sync the games to your storefront, and open the launch link.** You never write ScatterKings-specific code, never hold SK's signing keys, and never build an SK-shaped payload. ::: tip OneHazel ⇄ you — the contract OneHazel's connector calls a **vendor-neutral wallet & session API** on your side (`/provider/v1/*`) for every balance check, bet, win, rollback, and free-round. You implement that contract (or, on a OneHazel-supported platform, it's already built). OneHazel handles everything SK-specific — protocol, HMAC signatures, routing, currency conversion. The same contract serves every game provider, not just SK. ::: ## How a spin flows ``` player clicks Play → your storefront gets a launch link from OneHazel and opens it → OneHazel exchanges the link, starts the SK game, returns the game client → as the player spins, SK → OneHazel → YOUR wallet: POST /provider/v1/session/balance show balance POST /provider/v1/wallet/bet debit the stake POST /provider/v1/wallet/win credit a win POST /provider/v1/wallet/rollback reverse on error → your ledger is the source of truth; the in-game balance mirrors it ``` ScatterKings never calls you directly — OneHazel's connector does, after translating SK's protocol into the vendor-neutral contract below. ## Which path are you on? | Your platform | What you do | |---|---| | **On a OneHazel-supported platform** (the wallet contract is already built in) | **Config only** — mint a key (Step 1); OneHazel registers the connection and syncs the catalogue. Then Steps 3–5. | | **Your own / custom platform** | **Implement the receiving side** of the `/provider/v1/*` contract (Step 2), then config. | ::: tip Reference implementation A complete, code-verified reference implementation of this entire contract exists (SIM Casino, `OPERATOR_API.md`). Use it as a **worked sample to build against** — not a platform you adopt. An established operator implements the contract on their own wallet/ledger. ::: ## Step 1 — Get approved + mint a callback key ScatterKings approves each operator in writing before go-live; OneHazel coordinates that approval and provisioning for you. In parallel, mint an **integrator key with the `provider_callback` scope** on your platform — the credential OneHazel presents on every wallet call: ```bash POST /api/v1/admin/integrator-keys Authorization: Bearer { "name": "onehazel-scatterkings", "scopes": ["provider_callback"] } → { "key": "sim_live_<48 hex>", "last4": "…", "is_active": true } # shown once ``` Give OneHazel, over a secure channel (not email/chat): the **key**, your **platform base URL**, and your **operator identity**. ::: warning Scope matters A key without `provider_callback` authenticates but is rejected on every wallet call with `403 SCOPE_FORBIDDEN` (distinct from `401 AUTH_INVALID` for an unknown/revoked key). If wallet calls 403 after go-live, check the scope first. ::: ## Step 2 — Implement the wallet contract *Custom platforms only — on a supported platform this is built in; skip to Step 3.* OneHazel calls these endpoints on your platform. This is the **minimum surface to connect**. Auth on every call: `Authorization: Bearer sim_live_`. All money is `int64` **minor units** (`EUR 3.56` = `356`). | Endpoint | You implement | |---|---| | `POST /provider/v1/session/start` + `/session/exchange` | Mint a one-shot launch token, then exchange it for a wallet-capable session token. | | `POST /provider/v1/session/balance` | Return the wallet balance for a session token. | | `POST /provider/v1/wallet/bet` · `/win` · `/rollback` | Debit / credit / reverse against your ledger, keyed on `idempotency_key`. | | `POST /provider/v1/catalog/sync` | Upsert the games OneHazel pushes. | | `GET /api/v1/public/v2/games` | Public read of synced games — your lobby source. | | `POST /provider/v1/freerounds/*` | **Optional** — only if you run SK free-round campaigns; otherwise return `501`. | Four cross-cutting must-builds underneath those endpoints: 1. **Idempotency store** — persist `(idempotency_key → HTTP status + response body)` and replay it byte-for-byte (adding `"duplicate": true`) on any repeat. Globally unique across endpoints. This is what makes every mutating call safe to retry. 2. **Wallet ledger in `int64` minor units** — never floats. `bet` debits, `win` credits, `rollback` reverses by the echoed `metadata.amount_minor`. 3. **Session mint/verify** — a short-lived one-shot `plaunch_*` (~5-min TTL) exchanged for a longer-lived `psess_*`; verify on every wallet call. 4. **Public catalogue read** — serve the synced games so the storefront can launch them. ### Endpoint samples One flow throughout: player `p_abc`, EUR wallet at `10000` (€100.00), bet `350`, win `1200`. **Exchange the launch token → wallet session** — `POST /provider/v1/session/exchange` ```json // request { "idempotency_key": "ik_exch_", "launch_token": "plaunch_abcd1234…", "client_ip": "203.0.113.42" } // response { "session": { "token": "psess_ef5678…", "expires_at": "2026-05-25T13:00:00Z" }, "player": { "external_id": "p_abc", "currency": "EUR", "balance_minor": 10000 }, "duplicate": false } ``` **Balance** — `POST /provider/v1/session/balance` ```json // request { "session_token": "psess_ef5678…" } // response { "player_external_id": "p_abc", "currency": "EUR", "balance_minor": 10000 } ``` **Bet (debit)** — `POST /provider/v1/wallet/bet` ```json // request { "idempotency_key": "ik_bet_", "session_token": "psess_ef5678…", "round_external_id": "round_77a2", "amount_minor": 350, "currency": "EUR", "provider_transaction_id": "sk_tx_8841", "round_closed": false } // response { "status": "accepted", "player_external_id": "p_abc", "currency": "EUR", "balance_minor": 9650, "transaction_external_id": "tx_0a1b2c3d…", "duplicate": false } ``` **Win (credit)** — `POST /provider/v1/wallet/win` ```json // request { "idempotency_key": "ik_win_", "session_token": "psess_ef5678…", "bet_idempotency_key": "ik_bet_", "round_external_id": "round_77a2", "amount_minor": 1200, "currency": "EUR", "provider_transaction_id": "sk_tx_8842", "round_closed": true } // response { "status": "accepted", "player_external_id": "p_abc", "currency": "EUR", "balance_minor": 10850, "transaction_external_id": "tx_1b2c3d4e…", "duplicate": false } ``` **Rollback** — `POST /provider/v1/wallet/rollback` — reverses a prior bet **or** win by its `target_idempotency_key`. You **must echo the original amount** in `metadata.amount_minor` (the original request body isn't stored). ```json // request { "idempotency_key": "ik_rbk_", "session_token": "psess_ef5678…", "target_idempotency_key": "ik_win_", "round_external_id": "round_77a2", "provider_transaction_id": "sk_tx_8843", "metadata": { "amount_minor": 1200 } } // response { "status": "rolled_back", "player_external_id": "p_abc", "currency": "EUR", "balance_minor": 9650, "duplicate": false } ``` ### Response contract Wallet calls return a `status` string + the post-op `balance_minor`: | `status` | Meaning | HTTP | |---|---|---| | `accepted` | Bet/win applied | 200 | | `insufficient_funds` | Balance too low (bet only) — returns the unchanged balance | **402** | | `rolled_back` | Reversal applied | 200 | | `"duplicate": true` | Idempotent replay of a prior call — original body + status replayed | (replays original) | Error envelope is `{ "success": false, "error": { "code", "message" } }`. Key codes: `validation_error` 400 · `session_unknown` / `session_expired` / `session_revoked` 401 · `unknown_player` / `unknown_provider` / `unknown_game` 404 · `unknown_transaction` 404 (rollback) · `currency_mismatch` 409 · `bet_out_of_bounds` 400 · `launch_token_consumed` 409. **Session lifecycle** — `bet` requires an `active` session; **`win` and `rollback` are honored on an `expired` session** (the game runtime may queue a payout after the session lapses); `revoked` / `unknown` → 401 on everything. ### Free rounds (optional) If you run SK free-round campaigns, implement `freerounds/{issue,get,cancel,settle}`. The key rule: free-round **bets are funded by the provider** (no `wallet/bet` debits), and **winnings are paid as a single lump sum at result time** — so `settle` (mapped from SK's `freerounds.result`) credits `total_win_minor` to the wallet **exactly once** and closes the grant. Do **not** also credit free-round wins via `wallet/win`. Replays never double-credit; a re-settle of a closed grant returns `409` before any credit. ## Step 3 — Sync + show the games OneHazel pushes the SK catalogue to your `POST /provider/v1/catalog/sync` (upsert by `:`). Your storefront then reads the public catalogue (`GET /api/v1/public/v2/games`) and renders the SK titles on your slots page alongside your other providers. ::: warning Read the right catalogue Render from the catalogue `catalog/sync` writes to — **not** a separate hand-curated CMS list — or the games sync fine but never appear on your site. ::: ## Step 4 — Wire game launch When a player clicks an SK game, get the launch URL from OneHazel for that player + game, and open it — **embedded in an iframe or as a full-page redirect**. ::: tip Two-stage launch token (why it's safe) The launch URL carries only a single-use `plaunch_*` token — **never** a wallet-capable session token. OneHazel's connector exchanges it server-side (`/session/exchange`) for the `psess_*` used on wallet calls. A leaked launch URL can't touch the wallet, and a second exchange of the same token returns `409`. ::: ::: danger If a game won't embed Some game clients forbid being shown inside another site's frame (`X-Frame-Options`). If a game shows "refused to display," launch it as a **full-page redirect** (`window.location = launchUrl`) instead of an iframe. ::: ## Step 5 — Verify end-to-end 1. **Catalogue** — the SK titles appear on your storefront's slots page. 2. **Launch** — click a game; it loads (iframe or redirect). 3. **Balance** — the in-game balance matches the player's wallet (e.g. `€100.00`, not `0.00` and not `10000.00` — check your minor-units conversion). 4. **Bet / win** — a bet lowers the balance and a win raises it; both are reflected in your ledger (`GET /api/v1/players/{id}/wallet`). 5. **Free rounds** (if used) — issue a grant, play it out, confirm the lump-sum credit lands once at settle. ## Keeping it running - **New games / updates** — OneHazel re-runs `catalog/sync`; verify the new titles in your public catalogue. No operator code change. - **Going to production** — moving from the SK test environment to live is handled by OneHazel; you'll receive an updated launch link. ## Reference The full vendor-neutral contract — every endpoint, the complete error table, idempotency, session semantics, money rules, and a worked reference implementation — is in **`OPERATOR_API.md`** (the SIM Casino reference implementation you can build against). Free-round custom-bet modes and the exact catalogue shape are covered there. ::: tip Need help? If a game won't load, the in-game balance looks wrong, or a bet/win isn't reflected in your ledger, contact OneHazel with the game and player — OneHazel owns the ScatterKings integration end-to-end and will diagnose it. :::