Skip to content

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 }
]
}
FieldRequiredNotes
playerExternalIdThe player to charge and credit.
gameKeymystery_box.
modeopen (default) for atomic, prepay for the vault.
clientActionIdIdempotency key for this player action (≤80 chars). A retry with the same value returns the original round — your wallet is never debited twice.
currencyThe 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.
reelsOne 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’s prize.valueMinor come back in the currency the player transacted in — the same currency your wallet.debit was charged in, repriced from the operator base.
  • reels[].prize.itemId can be null for 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 its itemId is always present.
  • wonItems[].cashbackMinor is the amount the player could cash back for that item right now, in wonItems[].currency — the item’s own currency, which may differ from the round currency. It is null when 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 } ]
}
fundingMeaning
player (default)Normal play — LootBox Solutions calls wallet.debit for the price.
operatorYour 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 usual round_settled webhook 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: true and the box opens itself the moment the iframe boots.
  • Player-clicked — leave autoOpen off 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_entitlement webhook.

Errors

codeHTTPwhen
INSUFFICIENT_FUNDS422wallet refused — balance
PLAYER_BLOCKED403wallet refused — player state
WALLET_UNAVAILABLE503wallet timeout / 5xx / unconfigured
UNKNOWN_BOX / BOX_NOT_PLAYABLE404 / 409bad 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_FOUND404opening an unknown held round
ROUND_NOT_HELD409opening a round that isn’t in the vault (already opened/voided)
UNSUPPORTED_CURRENCY422currency not configured (or has no rate to price the box)
FX_RATE_MOVED422the 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/open with a session token. This S2S endpoint is the backend-driven equivalent.