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 }] }clientActionIdis required (≤80 chars). The game app generates a UUID per click and caches it insessionStorage, so a mid-flight reload reuses it and the wallet is never debited twice for one click.reelsis 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 moreunresolveditems with anaddress+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 apendingorder; 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’swaitingForStockand 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;clientSeedin 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).