Skip to content

Operator configuration

The wallet URL lives in Integration → Settings and your webhook endpoints in Integration → Webhooks. The two endpoints you must implement — the wallet RPC and the webhook receiver — are specified here.

Enabled games

LootBox Solutions enables the games your players can launch; ask us to enable or disable one. The available games are mystery_box and upgrader. A launch for a game that isn’t enabled fails with GAME_DISABLED.

The Upgrader’s own knobs — its win-chance presets, house edge, wager limits, and which wager modes (balance / owned items) are allowed — live separately under Settings → Upgrader, not in this enable list. See the Upgrader API.

Currencies & locales

Declare the currencies and locales your brand supports so launches and on-the-fly switching can validate against them. See Localization & currency for the full model.

Custom box attributes

Attach your own typed properties to boxes — a flag, a number, or a short label — without waiting on a code change for each one. Under Boxes → Settings you declare an attribute once, giving it a name, a type (Yes/No, Number, or Text), and a default; then you set its value per box on the box’s edit page. Declaring or changing an attribute is cosmetic — it never republishes the box or changes its odds.

Your declared attributes ride along on every box you read over the API, under customAttributes, in both the catalog and box detail. Only attributes you’ve declared are sent, and the values are typed — a Yes/No attribute is a JSON boolean, a Number is a JSON number, Text is a string:

"customAttributes": { "is_purchasable": true, "featured_rank": 3 }

Because they’re stored as box metadata, your declared attributes are also filterable and sortable in the catalog via meta.<name> (e.g. sort=-meta.featured_rank).

Wallet RPC

The synchronous endpoint LootBox Solutions calls for every money movement. You implement this. Set its HTTPS URL in Integration → Settings; leaving it empty disables spending (every open returns WALLET_UNAVAILABLE).

All calls are POST, JSON, HMAC-signed with your outbound secret, and carry a type field. Money operations also send an Idempotency-Key header equal to the body’s idempotencyKey.

wallet.debit / wallet.credit

Sent when the player spends (open / shipping cost) or is paid (cashback / refund).

{
"type": "wallet.debit",
"playerExternalId": "u_8431",
"amountMinor": 500,
"currency": "EUR",
"idempotencyKey": "round:r_01J…",
"reference": { "kind": "round", "publicId": "r_01J…" }
}

Respond:

// accepted
{ "accepted": true, "balanceAfterMinor": 11500, "providerTxnRef": "txn_xyz" }
// refused
{ "accepted": false, "errorCode": "INSUFFICIENT_FUNDS" }
  • balanceAfterMinor and providerTxnRef are optional but recommended — balanceAfterMinor lets the game app update the balance without a second call.

  • The reference object describes the event for your ledger/reconciliation. Its kind is round, cashback, refund or shipping; a cashback credit also carries the won item’s itemId and itemName, so you can label the payout on your side without a second lookup, and the payout’s split in the credit’s currency: basisMinor (the value the item is counted at), creditsMinor (the Credits this credit pays, equal to amountMinor) and uncredited (priceFallMinor, priceRiseMinor, creditsShareMinor: the part of the item’s value the cashback did not pay in Credits, by cause, each rounded up). Whether you pay any of it in Nuggets is your choice; if you do, book them with the credit. All three are 0 unless the item’s cashback is below 100%, see The cashback percentage and the fee, or the item is a collectible whose price moved, see Cashout.

  • Recognised refusal codes map to player-facing semantics:

    your errorCodeplayer sees
    INSUFFICIENT_FUNDS422 — not enough balance
    PLAYER_BLOCKED403 — your casino blocked this player
    anything else / no body503 — wallet unavailable

wallet.balance

A read for header display and heartbeat refresh.

{ "type": "wallet.balance", "playerExternalId": "u_8431", "currency": "EUR" }

Respond { "balanceMinor": 12500, "currency": "EUR" } (or { "errorCode": "…" }).

Rules you must honour

  • Timeout: LootBox Solutions waits 10 seconds. Transport failure, non-2xx, or unparseable JSON all map to WALLET_UNAVAILABLE; the player can retry, and nothing is lost.
  • Idempotency: the idempotencyKey is stable per real-world event — retries reuse it. Treat a duplicate as “already processed” and return the cached result. Never debit twice for the same key. Key shapes:
    • round:<roundPublicId> — opening a round.
    • cashback:inventory:<inventoryItemId> — cashing back an item.
    • refund:round:<roundPublicId> — refunding a held box.
    • shipping:player:<hash> — a shipping batch.
  • Currency: every call names the exact currency being moved. Process the amount in that currency. LootBox Solutions converts the box price once, at purchase, via the rate pinned for the player’s session — the amount and currency are final, and your wallet never does a second conversion. When a player changes display currency mid-session, the next purchase’s calls carry the new currency.

Webhooks

The async endpoint LootBox Solutions POSTs domain events to. Create one or more in Integration → Webhooks, each with its URL and the events it receives. Every endpoint has its own signing secret (Reveal in the list), and each delivery is HMAC-signed with the receiving endpoint’s secret, not your outbound secret. Same envelope; at-least-once delivery with exponential backoff.

A 2xx (any body) marks the dispatch delivered. A 4xx/5xx retries until the budget is exhausted, then parks the row as failed for reconciliation in the admin Integration screen. Dedupe on idempotencyKey.

Event types

Two kinds travel the same channel. Subscribe to the ones you need.

Instructions — you must act on these:

typeWhenYou should
bonus_entitlementA bonus prize was wonGrant the bonus in your system
fulfillment_noticeOne of your shipping orders changed, whoever changed it (the player, your desk, the platform’s desk)Update your records / notify the player; on a cancel, refund your own shipping charge when the platform refunded nothing

Observational — mirror state, no in-band settlement:

typeWhen
round_settledA round resolved (loyalty/XP, analytics)
box_version_publishedA box’s new version went live; priceMinor is in the payload’s currency (your pricing currency, which changes when you convert the store to another one)
box_updatedBox metadata changed
box_archivedA box was archived
kyc_status_changedA KYC verification request changed status
settings_changedThe platform changed your operator settings. The payload carries no values (dataVersion, occurredAt only): re-read GET /config

Example: bonus_entitlement

{
"type": "bonus_entitlement",
"playerExternalId": "u_8431",
"idempotencyKey": "bonus:reel:917",
"payload": {
"itemId": 12,
"name": "Free spin",
"valueMinor": 0,
"currency": "EUR",
"roundPublicId": "r_01J…",
"metadata": { }
}
}

See Concepts for how bonus vs. cashback are split (cashback is a synchronous wallet credit, not a webhook).

Example: fulfillment_notice

Sent once for every change to one of your shipping orders, whatever it came from (your S2S call, the player’s game app, goods bought with Nuggets) and whoever made the change. order is the order as GET /shipping/orders/{publicId} returns it after the change: replace your copy with it. Each change is its own delivery with its own idempotencyKey; order them by occurredAt.

{
"type": "fulfillment_notice",
"playerExternalId": "u_8431",
"idempotencyKey": "fulfillment_notice:ord_3f2a…:9b1c…",
"payload": {
"dataVersion": 2,
"occurredAt": "2026-06-16T14:05:00Z",
"playerExternalId": "u_8431",
"changes": ["cancelled"],
"order": {
"publicId": "ord_3f2a…",
"status": "cancelled",
"currency": "EUR",
"shippingCostMinor": 700,
"address": { "name": "Ada Lovelace", "line1": "12 Analytical Way", "city": "London", "region": "Greater London", "postalCode": "EC1A 1BB", "country": "GB" },
"contact": { "email": "ada@example.com" },
"userNotes": null,
"createdAt": "2026-06-16T12:00:00Z",
"carrier": null,
"trackingNumber": null,
"trackingUrl": null,
"deliveredAt": null,
"replacementsSentAt": null,
"items": [],
"realizedTotalCostMinor": null,
"itemsValueTotalMinor": 0
},
"shippingRefundedMinor": 0,
"shippingRefundedCurrency": "EUR"
}
}
FieldNotes
changesWhat changed, one or more of: created (a new order), items_added (more items joined the player’s open order), item_removed (an item left the order to be cashed back), status, cancelled, tracking (carrier, tracking number or link), address, backorder (an item’s waitingForStock turned), replacements_sent, details (the player’s note, the shipping cost or your recorded cost).
orderThe order shape after the change. A cancelled order has no lines left.
shippingRefundedMinor, shippingRefundedCurrencySet only when changes holds cancelled, else null. What the platform gave back to the player’s wallet for shipping: the fee it charged when the player shipped from the game app, 0 when it charged nothing (an order you created over S2S, goods bought with Nuggets, an order emptied by a cashback). With 0, a shipping fee you charged the player is yours to refund.

Example: round_settled

Observational — a round resolved. Mirror it for loyalty/XP or analytics; there is nothing to settle in-band (the money already moved through the wallet RPC). outcome is the game-specific result blob (reels, won items, …).

{
"type": "round_settled",
"playerExternalId": "u_8431",
"idempotencyKey": "round_settled:r_01J…",
"payload": {
"dataVersion": 2,
"occurredAt": "2026-06-03T12:00:00Z",
"roundPublicId": "r_01J…",
"gameKey": "mystery_box",
"betMinor": 4200,
"payoutMinor": 1500,
"currency": "EUR",
"playerBetMinor": 4560,
"playerCurrency": "USD",
"funding": "player",
"boxId": 42,
"outcome": { },
"facts": {
"currency": "USD",
"reels": [
{ "reelIndex": 0, "paidMinor": 4560, "itemValueMinor": 1628, "itemType": "physical" }
],
"deal": null
}
}
}

betMinor / currency is the canonical operator leg — the round denominated in your own currency, the value to use for reporting and reconciliation.

playerBetMinor / playerCurrency is the same wager in the currency the player actually paid, captured at the FX rate pinned for their session. Use it to show the wager in the player’s own currency. Both fields are null when the round had no currency conversion (the player and operator currency were the same) — fall back to betMinor / currency, which then already equals the player’s currency.

funding is player or operator — it tells you whether the player paid for this open or you funded it (operator-funded), so you can reconcile a free box differently from a paid round. boxId is the box this round opened, letting you attribute the settle back to the grant that funded it.

Round facts (facts, since dataVersion 2)

facts is what you need to give the player something back for a round on your side — for example, a reward for the part of the price a prize did not cover. It is present on every round and always filled, in the player’s currency at the rate pinned for the round (on a round with no conversion it simply carries your own currency). The purchase and Upgrader answers carry the same block with the same figures, so you can act on the answer and dedupe the webhook on roundPublicId.

FieldTypeNotes
facts.currencystringThe player’s currency; every amount in facts is in it.
facts.reels[]arrayOne entry per box opened; empty for an Upgrader round.
facts.reels[].reelIndexintThe reel, as in the round.
facts.reels[].paidMinorintThe part of the round’s price that paid for this reel. The reels add up to exactly what the wallet was debited. On an operator-funded round nobody was debited: it is the price the reel stands for, and funding tells you the player did not pay it.
facts.reels[].itemValueMinorintThe value of the item won, as priced in the box the player bought (a later price change never moves it). Converted amounts round down.
facts.reels[].itemTypestring | nullThe item’s type: physical, digital, bonus or nuggets. A nuggets reel is a Nuggets prize: credit the player Nuggets worth itemValueMinor at your rate.
facts.dealobject | nullSet on an Upgrader round, null on a box round.
facts.deal.stakeCountedMinorintThe stake as counted: what the player put up, in money or in items. A miss loses all of it.
facts.deal.stakeUncreditedobjectThe part of the staked items’ value their stake did not count in Credits, by cause: priceFallMinor (the fall of a collectible counted at today’s value), priceRiseMinor (the rise of a collectible counted at its value at win time) and creditsShareMinor (the cashback fee). A staked item counts for the Credits part of what cashing it back pays; this is the rest. Whether you pay any of it in Nuggets when the items are put up is your choice. Each part rounded up; all 0 for a Credits stake and for an item with no cashback of its own, which counts for its whole value.

The amounts in facts are facts only: the platform awards nothing from them.

Example: kyc_status_changed

Observational — a KYC verification request moved through its lifecycle. One event fires per status change, including creation (the first event for a request is its PENDING). Signed with the same HMAC envelope as every outbound call.

{
"type": "kyc_status_changed",
"playerExternalId": "u_8431",
"idempotencyKey": "kyc:812:CONFIRMED",
"payload": {
"dataVersion": 2,
"requestId": 812,
"playerExternalId": "u_8431",
"status": "CONFIRMED",
"verdict": "confirmed",
"occurredAt": "2026-07-07T12:00:00Z",
"submittedAt": "2026-07-07T12:00:00Z",
"decidedAt": "2026-07-08T09:30:00Z",
"reasons": [],
"retryable": null,
"identity": {
"legalName": "Maria da Silva",
"dateOfBirth": "1994-03-12",
"taxId": "123.456.789-09",
"addressLine1": "Rua das Flores 123",
"addressLine2": null,
"addressCity": "São Paulo",
"addressState": "SP",
"addressPostalCode": "01310-100",
"addressCountry": "BR"
}
}
}
FieldTypeNotes
dataVersionintPayload schema version, currently 2.
requestIdintThe verification request. A redo or re-verification opens a new request — later events carry the new id. Ids only grow, so it doubles as a sort key.
playerExternalIdstringYour player id, mirrored from the mint.
statusstringPENDING, SUBMITTED, CONFIRMED, or REJECTED.
verdictstring | nullconfirmed or rejected once decided; null while the request is open. Branch on this rather than re-deriving it from status.
occurredAtstringWhen the change happened (ISO 8601 UTC).
submittedAtstring | nullWhen the player finished the wizard; null until then.
decidedAtstring | nullWhen the review decision landed; null until then.
reasonsstring[]Rejection reason codes; always [] unless verdict is rejected.
retryablebool | nullWhether the player may simply try again, or the refusal is final. null unless rejected. Take it as sent — one final reason makes the whole rejection final, and that classification is done here so you never have to re-derive it.
identityobject | nullThe verified identity — see below. Present only on a confirmation that verified a complete identity; null on every other event.

Every key is present on every event — verdict, reasons, retryable and identity are simply null/empty while they don’t apply. Branch on values, never on key existence.

  • Idempotency: the key is kyc:{requestId}:{status}. A request only moves forward, and a redo gets a fresh requestId, so a repeated key is always a redelivery of the same event — dedupe on it like every other dispatch.
  • Subscription: delivery is per endpoint — add kyc_status_changed to your webhook endpoint’s subscribed events in Integration → Webhooks, or the event is never sent to it.
  • The identity block is the one place PII crosses this seam. On a confirmation, the reviewed identity — legal name, date of birth (YYYY-MM-DD), tax/national id, address — is delivered for you to persist as your own customer record; the platform verified it against the player’s documents, and confirming is the assertion that the two agree. addressLine2 and addressState may be null (optional fields); a confirmation that never captured a complete identity carries identity: null rather than a partial block, and that is never a reason to erase a record you already hold. Documents and the contact email still never appear in any webhook — see data handling.