Skip to main content
Section 3

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

Game backendRelayCasino walletGET /api/account?sessionIdbalance{ id, balance, username }POST /balance/cash-out (bet)debitround OPEN · balance after debitPOST /balance/cash-in (win / loss)creditround SETTLED · balance after creditPOST /round-ended { providerIds }notify (no money){ ok: true }

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.

  1. Balance read

    Performed when the game loads and whenever the displayed balance is refreshed. Nothing is changed.

    GET /api/account?sessionId=6f1e0d5c-…&currency=USD

    200 { "id": "48213", "balance": 250.00, "username": "lucky_sam" }

    currency is optional here; when present it is checked against the session.

  2. Bet: cash-out

    Debit the stake. The game generates a transactionId and a roundId that 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 SUCCESS transaction and an OPEN round holding gameData unchanged. The game object 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.

  3. Settlement: cash-in

    Every bet must be settled with exactly one credit, even a loss. A loss is a cash-in of 0 with reason BET_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:

    • appliedMultiplier is required on every credit. Send the real multiplier for NORMAL_WIN and FREEBET_WIN; send 0 for every other reason. Omitting it is a 400.
    • reason must be one of the values in the table below. Anything else is a 400 and the casino is never called.
    • previousTransactionId is the bet's transactionId. Send it for every reason that settles a bet.
    • amount is the total paid to the player, stake included when the game returns the stake. betAmount is 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.
  4. 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 from GET /api/v1/providers/ids or 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 404 or 403 and 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 roundId on the money routes, for example the shared round id plus the player id. A second cash-out reusing an existing roundId still 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 stateBehaviourResponse
SUCCESSReplay. The casino is not called again.The stored balance response, 200.
FAILEDA fresh attempt is made with the new body.Whatever the casino now answers.
PENDING, younger than 60 secondsAnother attempt is still waiting for the casino.409 { "error": "TRANSACTION_IN_FLIGHT" }. Wait and retry.
PENDING, older than 60 secondsTreated as abandoned; a fresh attempt is made.Whatever the casino now answers.
Recovery from network failures.

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:

  1. Repeat POST /api/balance/cash-in with the same body and transactionId. Because the stored transaction is FAILED, the relay makes a fresh attempt. Suitable for immediate retries within the live session.
  2. Use POST /api/balance/cash-in/retry with 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.

ReasonWhen to send itSettles a betappliedMultiplier
NORMAL_WINThe player cashed out a normal bet.yesreal value
BET_LOSTThe player lost a normal bet. amount is 0.yes0
FREEBET_WINThe player cashed out a freebet. Also send awardId, optionally campaignId and isLastFreebet.noreal value
FREEBET_LOSTThe player lost a freebet. amount is 0. Also send awardId.no0
REVERSE_FUNDThe player cancelled the bet during the betting phase. Refund of the stake.yes0
ROUND_CHANGEDDelayed cashout or refund: the game moved to a new round before the bet was settled.yes0
PHASE_CHANGEDDelayed cashout or refund: the game moved to a phase that no longer accepts bets.yes0
TIMEOUT_EXCEEDEDDelayed cashout or refund: the bet stayed unresolved past the time limit.yes0
CASHOUT_FAILEDRefund because the debit returned a 500. Only when the casino has refund on credit fail enabled.yes0
INTEGRATION_TIMED_OUTRefund because the integration timed out.yes0
INTERNAL_ERRORRefund because the game failed to create the bet after the debit had succeeded.yes0
DISCONNECT_REVERSE_FUNDPlayer disconnected during betting; stake refunded before the round started. Only when the casino has credit on disconnect enabled.no0
DISCONNECT_NORMAL_WINPlayer disconnected mid-round; bet cashed out at the multiplier reached at that moment. Same condition.noreal value
DISCONNECT_FREEBET_WINSame as above for a freebet.noreal value

Money rules

  • At most two decimal places on amount and betAmount. 10.005 is a 400, 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 balance returned is rounded half-up to two decimals by the relay, whatever the casino returned.
  • The session's currency is authoritative. The currency in the body is checked against it and a mismatch is a 400 (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 / codeMeaningHandling
400 FUND_WITHDRAW_FAILEDCasino refused the debit, normally insufficient funds.The bet is rejected. No round was opened.
400 Bad RequestThe request body failed validation.The request is corrected. The casino was not called.
403 AUTHENTICATION_TOKEN_EXPIREDThe casino no longer accepts the player's token.The session ends; the player must relaunch.
403 SESSION_EXPIREDSession gone.The session ends; the player must relaunch. Pending credits: use the retry route.
408 INTEGRATION_TIMED_OUTCasino 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_FLIGHTAn earlier attempt is still running.Repeat the identical request after a short wait.
500 CASHOUT_FAILEDCasino 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_FAILURECasino answered outside the contract.Bet: rejected. Credit: retry scheduled.