Round lifecycle
Four calls cover all money movement: balance read, debit of the bet, credit of the result, and round-end notification. This section walks through one round end to end, then states the rules that keep money movements safe.
Sequence
Walkthrough
The examples use the session from the previous section: player 48213, game 101, currency USD, casino 66ffba…. Every call carries Authorization: Bearer $RELAY_TOKEN.
Balance read
Performed when the game loads and whenever the displayed balance is refreshed. Nothing is changed.
GET /api/account?sessionId=6f1e0d5c-…¤cy=USD200 { "id": "48213", "balance": 250.00, "username": "lucky_sam" }currencyis optional here; when present it is checked against the session.Bet: cash-out
Debit the stake. The game generates a
transactionIdand aroundIdthat are unique for this casino and keeps both: the settlement refers to them.POST /api/balance/cash-out{"sessionId": "6f1e0d5c-…","gameId": 101,"currency": "USD","roundId": "r-2026-09-21-000871-48213","transactionId": "bet-01J8Z1Q9K4X2Y7","amount": 10.00,"language": "en","game": {"gameData": { "grid": 25, "mines": 3, "seedHash": "a9f3…" }}}200 { "id": "48213", "balance": 240.00, "username": "lucky_sam" }On success the relay has recorded a
SUCCESStransaction and anOPENround holdinggameDataunchanged. Thegameobject belongs to the game: the relay stores it and never reads inside it, and it is not forwarded to the casino.When the player cannot cover the bet the casino answers
400, surfaced as{ "error": "FUND_WITHDRAW_FAILED" }. No round is opened. The game shows an insufficient-funds message.Settlement: cash-in
Every bet must be settled with exactly one credit, even a loss. A loss is a cash-in of
0with reasonBET_LOST. This is what closes the round on the casino side.Win
POST /api/balance/cash-in{"sessionId": "6f1e0d5c-…","gameId": 101,"currency": "USD","roundId": "r-2026-09-21-000871-48213","transactionId": "win-01J8Z1S2M7P0Q3","previousTransactionId": "bet-01J8Z1Q9K4X2Y7","reason": "NORMAL_WIN","amount": 24.50,"betAmount": 10.00,"appliedMultiplier": 2.45,"game": {"outcome": "CASHED_OUT","settlementData": { "revealed": [3, 7, 12], "multiplier": 2.45 }}}200 { "id": "48213", "balance": 264.50, "username": "lucky_sam" }Loss
POST /api/balance/cash-in{"sessionId": "6f1e0d5c-…","gameId": 101,"currency": "USD","roundId": "r-2026-09-21-000871-48213","transactionId": "lost-01J8Z1S2M7P0Q3","previousTransactionId": "bet-01J8Z1Q9K4X2Y7","reason": "BET_LOST","amount": 0,"betAmount": 10.00,"appliedMultiplier": 0,"game": { "outcome": "BUST", "settlementData": { "hitMine": 9 } }}200 { "id": "48213", "balance": 240.00, "username": "lucky_sam" }Points that commonly cause rejected requests:
appliedMultiplieris required on every credit. Send the real multiplier forNORMAL_WINandFREEBET_WIN; send0for every other reason. Omitting it is a400.reasonmust be one of the values in the table below. Anything else is a400and the casino is never called.previousTransactionIdis the bet'stransactionId. Send it for every reason that settles a bet.amountis the total paid to the player, stake included when the game returns the stake.betAmountis the original stake.- A second credit for the same round, for example a refund after a win, is accepted and recorded as a transaction; the round keeps its first settlement and the second is logged.
Round-end notification: round-ended
This carries no money and no session. It informs casinos that a round of the game has ended, so they can close their own records. Because a round of a multiplayer game ends for a casino as a whole rather than for one player, the casinos are named directly with
providerIds. The ids come fromGET /api/v1/providers/idsor from the sessions in play.POST /api/round-ended{"roundId": "r-2026-09-21-000871","providerIds": ["66ffba1337e994c3009ebf40", "6701c2d3e4f5a6b7c8d9e0f1"]}200 { "ok": true }Every id is resolved before the first notification is sent: an unknown or inactive casino fails the whole call with
404or403and nobody is notified. When one casino fails during delivery, the others are still notified and that casino's error is returned. Repeated ids are notified once.Whether a casino needs this notification is agreed between the platform and the casino. Games with a natural round end send it after the last settlement of the round; single-player games where each bet is its own round send it once per bet after the cash-in.
Identifier rules
- Both are strings generated by the game. Both are scoped to the casino: the relay keys transactions by
(providerId, transactionId)and rounds by(providerId, roundId). Globally unique ids (ULIDs, UUIDs, or a prefix plus a counter) satisfy this trivially. - The relay's round record holds one bet: one player, one stake, one settlement. Games with shared rounds in which many players bet give each player's bet its own
roundIdon the money routes, for example the shared round id plus the player id. A second cash-out reusing an existingroundIdstill moves money but is only logged as "round already open". - Each distinct money movement uses a fresh
transactionId. An id is reused only to repeat the same movement, as described under idempotency. - Identifiers must not contain
:; the casino's own validation rejects a colon in player identifiers and some casinos build cache keys from these values.
Idempotency
The relay writes a PENDING transaction before calling the casino and updates it afterwards. Sending the same transactionId again for the same casino therefore behaves predictably:
| Stored state | Behaviour | Response |
|---|---|---|
SUCCESS | Replay. The casino is not called again. | The stored balance response, 200. |
FAILED | A fresh attempt is made with the new body. | Whatever the casino now answers. |
PENDING, younger than 60 seconds | Another attempt is still waiting for the casino. | 409 { "error": "TRANSACTION_IN_FLIGHT" }. Wait and retry. |
PENDING, older than 60 seconds | Treated as abandoned; a fresh attempt is made. | Whatever the casino now answers. |
When a call to the relay times out, or the game process fails after sending a cash-out, the identical request is sent again with the same transactionId. The result is either the recorded outcome or one clean new attempt. The player is never debited twice.
Credit retry
When a cash-in fails because the casino was unavailable (408 or 502), the player is owed money and the credit must eventually go through. Two options:
- Repeat
POST /api/balance/cash-inwith the same body andtransactionId. Because the stored transaction isFAILED, the relay makes a fresh attempt. Suitable for immediate retries within the live session. - Use
POST /api/balance/cash-in/retrywith the same body. This is the casino contract's dedicated retry path. It differs from a plain cash-in in one important way: the relay can find the session in its two-month archive even after the one-day live session has expired, so a delayed credit for a finished round still works.
Retry scheduling is the game's responsibility: the relay does not queue or re-attempt anything on its own. A typical policy is exponential back-off starting at a few seconds, continuing for hours, with alerting on persistent failure.
POST /api/balance/cash-in/retry
{ …exactly the same body as the failed cash-in… }
200 { "id": "48213", "balance": 264.50, "username": "lucky_sam" }
Cash-in reasons
The reason tells the casino why money is coming back. Values marked "settles a bet" carry previousTransactionId.
| Reason | When to send it | Settles a bet | appliedMultiplier |
|---|---|---|---|
NORMAL_WIN | The player cashed out a normal bet. | yes | real value |
BET_LOST | The player lost a normal bet. amount is 0. | yes | 0 |
FREEBET_WIN | The player cashed out a freebet. Also send awardId, optionally campaignId and isLastFreebet. | no | real value |
FREEBET_LOST | The player lost a freebet. amount is 0. Also send awardId. | no | 0 |
REVERSE_FUND | The player cancelled the bet during the betting phase. Refund of the stake. | yes | 0 |
ROUND_CHANGED | Delayed cashout or refund: the game moved to a new round before the bet was settled. | yes | 0 |
PHASE_CHANGED | Delayed cashout or refund: the game moved to a phase that no longer accepts bets. | yes | 0 |
TIMEOUT_EXCEEDED | Delayed cashout or refund: the bet stayed unresolved past the time limit. | yes | 0 |
CASHOUT_FAILED | Refund because the debit returned a 500. Only when the casino has refund on credit fail enabled. | yes | 0 |
INTEGRATION_TIMED_OUT | Refund because the integration timed out. | yes | 0 |
INTERNAL_ERROR | Refund because the game failed to create the bet after the debit had succeeded. | yes | 0 |
DISCONNECT_REVERSE_FUND | Player disconnected during betting; stake refunded before the round started. Only when the casino has credit on disconnect enabled. | no | 0 |
DISCONNECT_NORMAL_WIN | Player disconnected mid-round; bet cashed out at the multiplier reached at that moment. Same condition. | no | real value |
DISCONNECT_FREEBET_WIN | Same as above for a freebet. | no | real value |
Money rules
- At most two decimal places on
amountandbetAmount.10.005is a400, not a rounding. Rounding is done in the game before sending. - Amounts are non-negative. A refund is a positive cash-in, never a negative cash-out.
- Send JSON numbers, not strings.
- Every
balancereturned is rounded half-up to two decimals by the relay, whatever the casino returned. - The session's currency is authoritative. The
currencyin the body is checked against it and a mismatch is a400(SESSION_CURRENCY_MISMATCH).
Error handling on money routes
Errors come back as { "error": "CODE" }. The full catalogue is in Errors; the decisions relevant mid-round are:
| Status / code | Meaning | Handling |
|---|---|---|
400 FUND_WITHDRAW_FAILED | Casino refused the debit, normally insufficient funds. | The bet is rejected. No round was opened. |
400 Bad Request | The request body failed validation. | The request is corrected. The casino was not called. |
403 AUTHENTICATION_TOKEN_EXPIRED | The casino no longer accepts the player's token. | The session ends; the player must relaunch. |
403 SESSION_EXPIRED | Session gone. | The session ends; the player must relaunch. Pending credits: use the retry route. |
408 INTEGRATION_TIMED_OUT | Casino took more than 10 seconds. The movement may or may not have happened on their side. | The request is repeated with the same transactionId; the idempotent replay resolves the ambiguity on the relay side. |
409 TRANSACTION_IN_FLIGHT | An earlier attempt is still running. | Repeat the identical request after a short wait. |
500 CASHOUT_FAILED | Casino failed the debit with a server error. | The bet is rejected. Where the casino has refund-on-credit-fail, a CASHOUT_FAILED cash-in may be expected; to be confirmed with the platform team. |
502 PROVIDER_API_FAILURE | Casino answered outside the contract. | Bet: rejected. Credit: retry scheduled. |