Win
Register a winning or losing bet result
155 calls your wallet (signed with X-Marbles-Signature) to register a win or loss — the settlement of a bet. For winning bets, add the win amount to the player's balance. For losing bets, the amount is 0.
Endpoint
POST /winRequest
POST /win HTTP/1.1
Host: your.game.api
X-Marbles-Signature: <signature>
Content-Type: application/json
{
"requestId": "8df0475e-5069-483a-8205-f6089997abc9",
"transactionId": "9ea48131-3a0f-4067-94d0-3212e7e25abb",
"referenceTransactionId": "ea0240f5-483d-434b-a8d4-04dabf61cde3",
"clientSessionId": "0k3cz83bb3h2vn53ocnc7pxw9",
"clientPlayerId": "02mnrpyv2qd9jbwhoniyimxsy",
"roundId": "17cc81fd-df13-4ca4-857d-de0f766dc372",
"gameId": "f1c0b104-f29d-44a9-ae93-e8afcbe3feb9",
"amount": 1000000,
"currency": "USD",
"roundClosed": true,
"timestamp": "2025-06-15T14:30:00Z"
}Request Fields
| Field | Type | Description |
|---|---|---|
requestId | string | Unique request identifier (UUID) |
transactionId | string | Unique transaction identifier for this win |
referenceTransactionId | string | The original bet transaction ID |
clientSessionId | string | The player's session identifier |
clientPlayerId | string | The player's unique identifier |
roundId | string | Per-bet round reference — the same value this bet carried on /bet. Unique per bet, not per game round (see Round IDs) |
gameId | string | The game identifier |
amount | int64 | Win amount (0 for losses), an integer in the currency's wire precision (see Currencies) |
currency | string | ISO 4217 currency code |
isFree | boolean | true if the original bet was a freebet |
rewardUuid | string | The clientRewardId from the original freebet grant (only present when isFree is true) |
roundClosed | boolean | Whether this transaction closes the round (see below) |
timestamp | string | RFC 3339 UTC timestamp of when the request was created. Fractional seconds vary by callback — parse leniently, do not pin an exact format string |
Round Lifecycle
On /win, roundClosed is always true — each /win settles and closes its own per-bet round, and no further transactions follow for that bet. (The true/false distinction only matters on /rollback.)
Use roundClosed to finalize round records in your system. When true, you can safely mark the round as complete for reporting and reconciliation purposes. No /rollback will be sent for a bet after its /win callback — /win (including the amount: 0 loss form) is the terminal callback for that bet.
Win vs Loss
- Win:
amount > 0- Add this amount to the player's balance - Loss:
amount = 0- No balance change needed (bet was already deducted)
Freebet Results
When the original bet was a freebet, the request includes:
{
"requestId": "...",
"transactionId": "...",
"referenceTransactionId": "...",
"amount": 1500000,
"currency": "USD",
"isFree": true,
"rewardUuid": "promo-winter-2025-001",
"roundClosed": true
}isFreeistrueto indicate the original bet was a freebetrewardUuidcontains yourclientRewardIdfor tracking which promotion this freebet belongs to- For wins: Credit the
amountas real money to the player - For losses:
amountis0, no action needed
Freebet winnings are real money. When a freebet wins, credit the full win amount to the player's balance. See Freebets & Rewards for more details.
Response Contract
Always respond HTTP 200; put the outcome in status. We read the status field, not the HTTP code, to decide what happens to the settlement.
The full status set lives in Error codes — that is the canonical home for the enum across all callbacks. For /win the relevant outcomes are:
status | When you return it | What 155 does |
|---|---|---|
SUCCESS | Win/loss settled, balance updated | Settlement recorded |
DUPLICATE_TRANSACTION_ERROR | You already applied this exact transactionId | Accepted exactly like SUCCESS — recorded, not retried. Return it only for a genuinely already-applied transaction; returning it for one you did not apply permanently strands the payout |
UNKNOWN_ERROR | Unexpected operator-side failure | 155 retries |
/win recognises only SUCCESS and DUPLICATE_TRANSACTION_ERROR as applied. Any other status — including BONUS_ERROR — or a non-200 or timeout means the settlement was not recorded: the same /win, with the same transactionId, will be re-sent periodically until you accept it. Re-sends can continue for hours, and a settlement can also be re-delivered much later (e.g. after an outage) — your transactionId dedupe must not expire quickly. See Settlement timing & retries.
Never return a non-200 HTTP status or a bare error body — always respond 200 with one of the status values above. A non-200 is read as a transport failure and may trigger retries before the body is parsed.
The one exception is authentication: if you verify X-Marbles-Signature and it fails, reject the request with a non-2xx (e.g. 401) — that's an auth failure, not a win outcome. The status values above apply only to requests that passed signature verification. (A bad signature never occurs on genuine 155 traffic, so this can't affect real wins.)
Idempotency
Networks retry, so 155 may resend a callback it isn't certain landed. Dedupe by transactionId so a retry never moves money twice. The returned status for a replay differs by callback — this is the most common point of confusion:
| Callback | Dedupe key | Replay behaviour | Returned status | Balance effect |
|---|---|---|---|---|
/bet | transactionId | Rejected as a duplicate bet | DUPLICATE_TRANSACTION_ERROR | unchanged |
/win | transactionId | Treated as the original settlement | SUCCESS (DUPLICATE_TRANSACTION_ERROR also accepted) | unchanged |
/rollback | referenceTransactionId | Treated as the original rollback | SUCCESS (DUPLICATE_TRANSACTION_ERROR also accepted) | unchanged |
/rollback dedupes on a different key
On /bet and /win the transactionId is stable across retries and is your dedupe key. On /rollback it is not stable between attempts — dedupe rollbacks on referenceTransactionId instead. See /rollback § Idempotency.
The asymmetry is deliberate. A replayed /bet is surfaced as DUPLICATE_TRANSACTION_ERROR — a duplicate is not a new bet, and flagging it tells 155 the stake was already debited. A replayed /win or /rollback returns plain SUCCESS: the settlement is simply re-confirmed, the money already moved once, and there is nothing to flag. In both cases the balance is unchanged — return success without crediting again; do not credit the win a second time and do not cancel the original credit.
Worked Examples
Success — first /win
The win settles and the player's balance reflects the credit.
HTTP/1.1 200 OK
X-Marbles-Signature: <signature>
Content-Type: application/json
{
"status": "SUCCESS",
"requestId": "8df0475e-5069-483a-8205-f6089997abc9",
"clientPlayerId": "02mnrpyv2qd9jbwhoniyimxsy",
"currency": "USD",
"balance": 1000000
}Replayed /win — same transactionId
155 resent the callback. You return the original settlement result: still SUCCESS, balance unchanged (you do not credit the win again).
HTTP/1.1 200 OK
X-Marbles-Signature: <signature>
Content-Type: application/json
{
"status": "SUCCESS",
"requestId": "8df0475e-5069-483a-8205-f6089997abc9",
"clientPlayerId": "02mnrpyv2qd9jbwhoniyimxsy",
"currency": "USD",
"balance": 1000000
}Response Fields
| Field | Type | Description |
|---|---|---|
status | string | "SUCCESS" |
requestId | string | Echo back the request ID |
clientPlayerId | string | Echo back the player ID |
currency | string | ISO 4217 currency code |
balance | int64 | New balance after win/loss, in the currency's wire precision (see Currencies) |
Error Response
HTTP/1.1 200 OK
X-Marbles-Signature: <signature>
Content-Type: application/json
{
"status": "UNKNOWN_ERROR",
"requestId": "8df0475e-5069-483a-8205-f6089997abc9",
"clientPlayerId": "02mnrpyv2qd9jbwhoniyimxsy"
}Common Mistakes
- Rejecting a
/winreplay. A replayed/winreturnsSUCCESS(preferred) —DUPLICATE_TRANSACTION_ERRORis also accepted as applied. What you must never do is treat the replay as a failure (see Idempotency). - Re-crediting on a replay. The money already moved on the first
/win; a replay must not add the win amount again, and must not reverse the original credit. - Returning a non-200 HTTP status (or a bare error body). Always respond
200with the outcome instatus.
See the Response Contract above for what 155 does with each status, and Error codes for the canonical enum.