Skip to content

Player API

The player API (/api/play/*) is the in-iframe surface. The embedded game app drives it for you — it’s documented here for diagnostics, and so you can build a native client if you ever want one. All routes (except session/init) require Authorization: Bearer <sessionToken>. CORS is open on /api/play/*.

For backend-driven integrations, the S2S API offers the same effects (purchase / cashout / cancel / history) signed with your API key.

Session

POST /api/play/session/init

Body { "launchToken": "lt_…" }. Exchanges a launch token for a session token and the bootstrap. The token is consumed; a second call returns LAUNCH_TOKEN_CONSUMED.

{
"sessionToken": "ps_…",
"expiresAt": "…",
"player": { "externalId": "u_8431", "balanceMinor": 12000, "balanceFetchedAt": "…" },
"tenant": { "name": "<your brand>", "locale": "en", "currency": "EUR", "logoUrl": "…" },
"theme": { "tokens": { "brandColor": "#3b82f6" } },
"game": {
"key": "mystery_box",
"maxReels": 1,
"target": { "boxId": 42, "boxVersionId": 1201, "name": "Dragon Box", "priceMinor": 500, "currency": "EUR", "freeOpens": 1 }
},
"fairness": { "serverSeedHash": "…", "clientSeed": "…", "nextNonce": 42, "algorithmKey": "hmac_sha256_v1" },
"policies": { "cashbackEnabled": true, "shippingEnabled": true, "refundEnabled": true }
}

The bootstrap is frozen for the session. balanceMinor may be null if the wallet balance couldn’t be read — a null balance never blocks play.

The game block reflects the launched game. For an Upgrader session (game.key is "upgrader", launched with gameKey: "upgrader") there is no box target; instead the block carries an upgrader object with the selector config — the win-chance presets, default/max chance, wager limits, and which wager modes are enabled — the same config shape the Upgrader prizes call returns, so the game app can render the trade-up controls on the first frame.

game.target.freeOpens is the number of operator-funded opens this box page advertises — set when the launch was minted with funding: "operator" (and fundingRounds, if capped); 0 for a normal paid box. While it’s above zero the game app shows an “Open your free box” CTA and skips the balance check; it counts down locally as each free open settles, reverting to the paid price once spent.

The policies block tells the game app which actions are available this session, so it can show or hide controls up front:

  • cashbackEnabled — a won item may be cashed back to the wallet (cashout).
  • shippingEnabled — won physical items may be shipped (fulfillment).
  • refundEnabled — a pre-bought, unopened box may be cancelled and refunded (cancel), per the operator/box refund policy. Already-opened rounds are never refundable, so this only affects vault boxes awaiting their auto-open.

POST /api/play/session/heartbeat

Bumps idle expiry (default 30 min) and refreshes balance. Returns { expiresAt, balanceMinor, balanceFetchedAt }. A wallet failure nulls the balance fields without erroring.

POST /api/play/session/logout

Revokes the session. Idempotent (204).

Open a round

POST /api/play/rounds/open

{ "clientActionId": "<uuid-per-click>", "reels": [{ "reelIndex": 0, "boxVersionId": 1201 }] }
  • clientActionId is required (≤80 chars). The game app generates a UUID per click and caches it in sessionStorage, so a mid-flight reload reuses it and the wallet is never debited twice for one click.
  • reels is a non-empty array; reelIndex ≥ 0, boxVersionId ≥ 1.
// settled
{ "round": { "publicId": "r_…", "status": "settled", "betMinor": 500, "payoutMinor": 320 }, "balanceAfterMinor": 11680 }
// debit refused → voided
{ "error": { "code": "INSUFFICIENT_FUNDS" }, "round": { "publicId": "r_…", "status": "voided" }, "balanceAfterMinor": 0 }

(Pre-pay/vault opens follow the same response shapes; see Purchase for the held-box flow.)

Inventory

GET /api/play/inventory?state=<state>[,<state>…]

Won items for the session player. States: unresolved, ship_requested, fulfilled, cashed_back, expired, forwarded_to_casino. Cursor-paginated { items: [...], cursor: { next, prev } }. Always scoped to the session player.

Rows for collectibles carry the same three additive fields as the S2S list — isCollectible, currentRevisionId, wonValueMinor — and their valueMinor is the item’s current value. They are absent on every other row; treat them as optional.

Cash back an item

POST /api/play/inventory/{id}/cashback — sync wallet credit then state flip to cashed_back. Only unresolved items qualify. Failure leaves the item untouched and returns the casino’s error code. (S2S equivalent: Cashout.)

The answer’s item carries cashbackMinor (the Credits credited), uncredited (the part of the item’s value not paid in Credits, by cause: priceFallMinor, priceRiseMinor, creditsShareMinor, as on Cashout) and basisMinor (the value the item was counted at), in item.currency.

For a collectible, send { "expectedRevisionId": <currentRevisionId> } in the body — the value the player confirmed. If it moved in the meantime (either direction), nothing settles and the call returns 409:

{
"error": {
"code": "PRICE_CHANGED",
"message": "The item's value changed. Confirm the new amount.",
"current": { "revisionId": 21981, "valueMinor": 11819, "cashbackMinor": 5910, "cashbackUncredited": { "priceFallMinor": 0, "priceRiseMinor": 0, "creditsShareMinor": 0 }, "currency": "EUR" }
}
}

Re-prompt with current.cashbackMinor and retry with current.revisionId. The embedded game app does exactly this; a custom client must implement its own re-confirm step — see Collectibles. The figures in current are in the operator currency, so reprice them for display if your surface works in the player’s wallet currency.

Omitting expectedRevisionId keeps the previous behaviour (settle at the value in force when the call lands).

Shipping

  • POST /api/play/shipping/orders — request shipping for one or more unresolved items with an address + contact. All-or-nothing: a non-shippable item rejects the whole batch (INVENTORY_NOT_SHIPPABLE), and so do items adding up to less than the shipping minimum (SHIPMENT_BELOW_MINIMUM, checked before any debit). The order’s shipping cost is the shipping cost of its most expensive item; if it is > 0, that amount is debited from the wallet once, first; a batch refused after that debit (an item shipped or cashed out in the meantime) is credited back at once. With shipping off, SHIPPING_DISABLED.
  • POST /api/play/shipping/orders/{publicId}/cancel — cancel a pending order; its items go back to the inventory. The shipping fee the player was charged for it is credited back to the wallet (shipping:refund:<publicId> idempotency key), exactly once, whoever cancels: the player, your desk or the platform’s. An order the player was never charged for (one you created over S2S, or goods bought with Nuggets) refunds nothing.
  • GET /api/play/shipping/orders / GET /api/play/shipping/orders/{publicId} — the player’s orders, by opaque public id (fo_…), with each line’s waitingForStock and the order status.

Fairness

  • GET /api/play/fairness/state — the current commitment.
  • POST /api/play/fairness/rotate — reveal the current server seed and rotate to a new pair; clientSeed in the body sets the new seed (omit to keep the previous). See Fairness & verification.

Wallet

POST /api/play/wallet/balance

Explicit balance refresh; 503 on wallet failure (no server-side caching).