Purchase
POST /api/s2s/purchase opens one or more boxes for a player from your backend.
It supports the two purchase flows:
atomic (buy & open now) and pre-pay (buy now, open later).
Either way, LootBox Solutions calls your wallet.debit
for the total before anything is drawn. A refused debit voids the purchase and
moves no money.
Atomic — buy & open now
The default. Debit, draw, and settle happen in one call; the response carries the outcome. Not refundable — the result is realized immediately.
POST https://{operator}.app.lootboxsolutions.com/api/s2s/purchase{ "playerExternalId": "u_8431", "gameKey": "mystery_box", "mode": "open", "clientActionId": "5f2c…-per-click-uuid", "currency": "EUR", "reels": [ { "reelIndex": 0, "boxVersionId": 1201 } ]}| Field | Required | Notes |
|---|---|---|
playerExternalId | ✅ | The player to charge and credit. |
gameKey | ✅ | mystery_box. |
mode | — | open (default) for atomic, prepay for the vault. |
clientActionId | ✅ | Idempotency key for this player action (≤80 chars). A retry with the same value returns the original round — your wallet is never debited twice. |
currency | — | The currency to charge the player in. Defaults to the operator base. When it differs from the base, the price is converted at the current rate and the wallet is debited in it; if the rate has moved past the operator’s tolerance the open is refused with FX_RATE_MOVED, so re-fetch the price and retry. |
reels | ✅ | One or more box-open slots. reelIndex ≥ 0, boxVersionId ≥ 1. Use the boxVersionId you rendered — opening a superseded version (the box was republished since) is refused with BOX_NOT_PLAYABLE, so prompt the player to refresh. Multiple reels = a multi-open (one debit for the total). |
Response — 200
{ "round": { "publicId": "r_01J…", "status": "settled", "betMinor": 500, "payoutMinor": 320, "currency": "EUR", "settledAt": "2026-06-03T12:00:05Z", "reels": [ { "reelIndex": 0, "boxVersionId": 1201, "prize": { "itemId": 906, "name": "…", "valueMinor": 320 } } ], "wonItems": [ { "inventoryItemId": 88, "itemId": 906, "name": "…", "cashbackMinor": 80, "currency": "EUR", "state": "unresolved" } ], "fairness": { "serverSeedHash": "…", "clientSeed": "…", "nonce": 42, "algorithmKey": "hmac_sha256_v1" } }, "balanceAfterMinor": 11680}The win carries the same itemId you read from box detail,
so you can map the outcome back to the prize you rendered (e.g. land a reel on
that tile).
- Amounts are in the player’s currency.
round.betMinor,payoutMinor, and each reel’sprize.valueMinorcome back in thecurrencythe player transacted in — the same currency yourwallet.debitwas charged in, repriced from the operator base. reels[].prize.itemIdcan benullfor a cash/bonus reward that isn’t a catalog item, so guard the prize-tile mapping against it.wonItems[]only ever lists fulfillable items, so itsitemIdis always present.wonItems[].cashbackMinoris the amount the player could cash back for that item right now, inwonItems[].currency— the item’s own currency, which may differ from the round currency. It isnullwhen the item isn’t cashable (no cashback configured, or the operator has it off).
A refused debit returns the voided round and the casino’s error code:
{ "error": { "code": "INSUFFICIENT_FUNDS", "message": "…" }, "round": { "publicId": "r_01J…", "status": "voided" }, "balanceAfterMinor": 0}Pre-pay — open later
mode: "prepay" debits now and parks the box unopened in the player’s vault.
The outcome is drawn later, when the player opens it. A pre-paid box is
cancellable/refundable while it stays unopened.
{ "playerExternalId": "u_8431", "gameKey": "mystery_box", "mode": "prepay", "clientActionId": "8a1d…", "reels": [ { "reelIndex": 0, "boxVersionId": 1201 } ]}Response — 201
{ "round": { "publicId": "r_01J…", "status": "held", "betMinor": 500, "currency": "EUR", "boxVersionId": 1201, "purchasedAt": "2026-06-03T12:00:05Z" }, "balanceAfterMinor": 11500}The price, currency, and odds are snapshotted at purchase — a later edit to the box does not change what the player bought.
Opening a held box
The player opens from the iframe, or your backend opens on their behalf:
POST https://{operator}.app.lootboxsolutions.com/api/s2s/purchase{ "playerExternalId": "u_8431", "mode": "open", "roundPublicId": "r_01J…", "clientActionId": "…" }This draws and settles the held round (no new debit). The response matches the atomic settled shape above. A retried open returns the already-settled round.
Operator-funded (bonus / free) open
LootBox Solutions does not model bonuses — your casino owns them entirely (eligibility, rules, limits, accounting). When your backend decides a player should get a box for free — a promo, a comp, a reward, for any reason — it tells LootBox Solutions to open the box without debiting the player’s wallet. LootBox Solutions trusts that instruction because it arrives signed with your API key.
Set funding: "operator":
{ "playerExternalId": "u_8431", "gameKey": "mystery_box", "mode": "open", "funding": "operator", "clientActionId": "…", "reels": [ { "reelIndex": 0, "boxVersionId": 1201 } ]}funding | Meaning |
|---|---|
player (default) | Normal play — LootBox Solutions calls wallet.debit for the price. |
operator | Your casino funds this open. No wallet.debit is made. Draw, payout, and winnings are otherwise identical to a paid open. |
- Casino-initiated only.
funding: "operator"is honoured only on the signed S2S surface — your backend, or a launch your backend mints with it (see below). A player cannot grant themselves a free box: the in-iframe player API ignores the flag. The trust anchor is your API-key signature. - Winnings still pay out. A win is credited to the player as usual
(
wallet.credit/ inventory) — you fund the open, the player keeps the prize. - Reconciliation, not bonus logic. The round and its
history row are tagged
funding: "operator", and the usualround_settledwebhook fires, so you can attribute the cost on your side. LootBox Solutions records what happened, never why — the bonus rationale lives entirely in your system. - Operator-funded opens cost the player nothing, so they aren’t refundable via cancel.
Free box in the iframe
To give a player a free box inside the iframe, your backend mints a
launch with funding: "operator". Because the
launch is signed by your backend, the open runs with no wallet debit. This is the
“casino gives you a free box” counterpart to the widget’s paid “Buy now”. Two
shapes:
- Auto-play — add
target.autoOpen: trueand the box opens itself the moment the iframe boots. - Player-clicked — leave
autoOpenoff and the box page renders an “Open your free box” call-to-action; the open runs when the player clicks it.
Pair funding: "operator" with fundingRounds
to cap how many free opens the session grants. The cap applies to the target
box only — any other box the player opens during the session is player-funded
as normal, so a free-box session can’t be turned into unlimited free play.
Operator-funded boxes (this) are distinct from bonus prizes (a player winning a bonus item), which are delivered to you as a
bonus_entitlementwebhook.
Errors
| code | HTTP | when |
|---|---|---|
INSUFFICIENT_FUNDS | 422 | wallet refused — balance |
PLAYER_BLOCKED | 403 | wallet refused — player state |
WALLET_UNAVAILABLE | 503 | wallet timeout / 5xx / unconfigured |
UNKNOWN_BOX / BOX_NOT_PLAYABLE | 404 / 409 | bad target box, or a superseded/inactive version — only a box’s current version is openable, so a republished box means “refresh and retry” |
ROUND_NOT_FOUND | 404 | opening an unknown held round |
ROUND_NOT_HELD | 409 | opening a round that isn’t in the vault (already opened/voided) |
UNSUPPORTED_CURRENCY | 422 | currency not configured (or has no rate to price the box) |
FX_RATE_MOVED | 422 | the exchange rate moved past tolerance since the price was shown — re-quote and retry |
Same effect, two callers. The game app drives the equivalent player-API
rounds/openwith a session token. This S2S endpoint is the backend-driven equivalent.