openapi: 3.1.0

info:
  title: Spartacus Operator API v1
  version: 1.5.0
  summary: Seamless-wallet integration for Spartacus games.
  description: |
    Spartacus games run in the player's browser and settle against the operator's wallet in real
    time. Spartacus holds no player funds and processes no deposits or withdrawals.

    An integration consists of three endpoints the operator calls and five webhooks the operator
    implements. The skins and players endpoints are optional. A webhook added later is sent only
    to operators who enable it at onboarding.

    ## Base URLs

    | Environment | URL |
    |---|---|
    | Sandbox | `https://api.sandbox.spartacus.invalid` |
    | Production | `https://api.spartacus.invalid` |

    ## Authentication

    Requests to Spartacus use a bearer API key:

    ```
    Authorization: Bearer tk_live_51H8xKfL2eZvKYlo2C0BqRs9T
    ```

    Sandbox keys are prefixed `tk_test_`. Multiple keys are valid at once to allow rotation
    without downtime.

    Webhook requests from Spartacus are signed with HMAC-SHA256:

    ```
    X-Telos-Signature: t=1789041923,v1=5257a869e7c1f0b3d4a29e8c7b6f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d
    ```

    The signed payload is `{t}.{raw request body}`. Compute the expected signature with the
    shared secret, compare it in constant time, and reject requests where `t` differs from the
    current time by more than 300 seconds. Ed25519 signing is available on request.

    - Each digest is 64 lowercase hexadecimal characters (HMAC-SHA256).
    - `t` is recomputed for every delivery attempt, so a retry carries a fresh `t` and signature
      over the same body bytes.
    - Verify over the raw received bytes, before parsing the JSON.
    - During a secret rotation the header carries up to 3 `v1=` entries; accept the request if
      any one of them matches. The header is at most 256 bytes (12 + 3 × 68 = 216 bytes for 3
      entries).
    - `X-Telos-Request-Id` and `X-Telos-Attempt` are not signed, so a signed request can
      be replayed within the 300 seconds. The defence is `transaction_uuid` idempotency on
      `bet`, `win` and `rollback`; `player` and `balance` are read-only.

    ## Amounts

    Amounts are strings containing an integer number of minor units. The scale is given by
    `decimals`.

    | Currency | `decimals` | Example | Value |
    |---|---|---|---|
    | `EUR` | 2 | `"1000"` | 10.00 EUR |
    | `USDT` | 6 | `"10000000"` | 10 USDT |
    | `BTC` | 8 | `"100000"` | 0.001 BTC |
    | `ETH` | 18 | `"10000000000000000"` | 0.01 ETH |
    | `GC` | 2 | `"1000"` | 10.00 GC |
    | `SC` | 2 | `"1000"` | 10.00 SC |

    Parse amounts with an arbitrary-precision integer type. JSON numbers are IEEE-754 doubles and
    cannot represent values above 2^53 exactly.

    `decimals` is included in every request that carries an amount. If it differs from the value
    configured for that currency, return `currency_mismatch` without moving funds.

    Crypto and social currencies use the same fields as fiat. Spartacus performs no currency
    conversion.

    ## Idempotency

    Every wallet webhook carries a `transaction_uuid`. Store it with the result.

    | Condition | Response |
    |---|---|
    | Unknown `transaction_uuid` | Process the request and store the result |
    | Known `transaction_uuid`, identical body | Return the stored result without moving funds |
    | Known `transaction_uuid`, different body | Return `idempotency_conflict` without moving funds |

    Retries repeat the request body byte for byte. Retain transaction records for at least 120
    days.

    ## Errors

    Errors use a consistent envelope:

    ```json
    { "error": { "code": "insufficient_funds", "message": "Balance 4.00 below stake 10.00" } }
    ```

    A `4xx` response is final and ends the operation, except that a `401` on `win` or `rollback`
    is retried like a `5xx` for the 24-hour retry window, each attempt with a fresh `t` and
    signature, and Spartacus raises an alert. A `5xx` response is indeterminate and Spartacus
    retries it. Return `5xx` only when the outcome of the request is unknown. No `4xx` moves
    funds.

    Operators return exactly these codes, each under its condition:

    | Code | Webhook(s) | Condition | Status |
    |---|---|---|---|
    | `validation_error` | all | Body is not JSON, does not match the schema, or a required header is missing or malformed. No funds move | 400 |
    | `unauthorized` | all | Signature missing, malformed, not matching, or `t` outside ±300 s; or `X-Telos-Version` not `2026-11-01` | 401 |
    | `token_invalid` | `player` | Launch token unknown or expired | 401 |
    | `user_blocked` | `player`, `bet` | Player blocked or self-excluded | 403 |
    | `bet_not_allowed` | `bet` | Stake outside the operator's own limits for this player (loss, wager or time limits) | 403 |
    | `insufficient_funds` | `bet` | Balance below `amount` | 402 |
    | `user_not_found` | `balance`, `bet`, `win` | Unknown `user` | 404 |
    | `transaction_not_found` | `win` | Unknown `bet_transaction_uuid` | 404 |
    | `idempotency_conflict` | `bet`, `win`, `rollback` | Known `transaction_uuid`, different body bytes | 409 |
    | `transaction_rolled_back` | `bet` | `transaction_uuid` was named by an earlier `rollback` (reversed or recorded `not_found`) | 409 |
    | `transaction_rolled_back` | `win` | `bet_transaction_uuid` was rolled back | 409 |
    | `transaction_settled` | `win` | `bet_transaction_uuid` names a bet that already has a `win`. Spartacus never sends this in normal operation; receiving it signals a Spartacus bug, which Spartacus alerts on | 409 |
    | `transaction_settled` | `rollback` | `rollback_transaction_uuid` names a bet that already has a `win`. Spartacus never sends this in normal operation; receiving it signals a Spartacus bug, which Spartacus alerts on | 409 |
    | `currency_mismatch` | `bet`, `win`, `rollback` | `decimals` differs from the configured value, or the currency is not enabled for the player | 422 |

    Not errors: a repeated request with the same `transaction_uuid` and identical body returns
    the stored result (for `rollback`, the stored `rolled_back` or `not_found`); a `rollback`
    whose target is unknown, or whose `user` is unknown, returns `200` with `status:
    "not_found"` and records the target — unless the target is a transaction the operator
    already holds, which is never marked.

    ## Conventions

    | | |
    |---|---|
    | Transport | HTTPS, TLS 1.2 or later |
    | Content type | `application/json; charset=utf-8` |
    | Identifiers | UUIDv7 (RFC 9562), canonical form. Opaque; do not parse. |
    | Timestamps | RFC 3339, UTC, millisecond precision |
    | Pagination | Opaque cursors. Pass `next_cursor` from the previous page. |
    | Webhook timeout | 2 seconds |
    | Webhook retries | `win` and `rollback` retry for 24 hours with exponential backoff and jitter. `bet` is never retried. |
    | Payload version | `X-Telos-Version`, fixed per operator at onboarding |
    | Unknown fields | Both sides ignore JSON fields they do not know, in requests and responses |

    Identifiers are not credentials. Generate the launch `token` from a cryptographically secure
    random source.

  license:
    name: Proprietary — Spartacus integration contract
    identifier: LicenseRef-Spartacus-Proprietary
  contact:
    name: Spartacus integration team
    url: https://spartacus.invalid/integration
    email: integration@spartacus.invalid

servers:
  - url: https://api.sandbox.spartacus.invalid
    description: Sandbox
  - url: https://api.spartacus.invalid
    description: Production

tags:
  - name: Games
    description: Game catalogue, assets, limits and currencies.
  - name: Sessions
    description: Player game sessions.
  - name: Rounds
    description: Round outcomes and fairness verification data.
  - name: Skins
    description: Cosmetic skins and player entitlements. Optional.
  - name: Players
    description: Player statistics and round history. Optional.
  - name: Wallet webhooks
    description: |
      Endpoints implemented by the operator and called by Spartacus. All five are served under a
      single base URL registered at onboarding and accept `POST` with a JSON body.

x-tagGroups:
  - name: API
    tags: [Games, Sessions, Rounds, Skins, Players]
  - name: Webhooks
    tags: [Wallet webhooks]

security:
  - apiKey: []

paths:
  /v1/games:
    get:
      tags: [Games]
      operationId: listGames
      summary: List games
      description: |
        Returns the games enabled for the authenticated operator, including assets, translations,
        categorisation, and per-currency limits.

        Asset and translation URLs are public and cacheable. `{dpr}` accepts `1`, `2` or `3`.
        `{lang}` falls back to `en` when a locale has no localised asset.
      responses:
        '200':
          description: Games enabled for this operator.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GameList'
              examples:
                default:
                  value:
                    games:
                      - code: crash
                        name: Apex Crash
                        provider: spartacus
                        released: '2026-11-01'
                        status: live
                        category: crash
                        subcategory: multiplayer
                        tags: [crash, instant, multiplayer, racing, provably-fair]
                        features: [auto_cashout, leaderboard, live_players, provably_fair, solo_mode]
                        platforms: [mobile, desktop]
                        orientation: [portrait, landscape]
                        rtp: 9500
                        volatility: high
                        max_multiplier: '10000.00'
                        multiplier_decimals: 2
                        round_seconds: 20
                        demo: true
                        max_bets_per_round: 1
                        languages: [en, pt-BR, es, tr]
                        default_language: en
                        jurisdictions: [CW, MT, BR]
                        currencies:
                          EUR: {decimals: 2, min_bet: '10', max_bet: '50000', max_win: '500000'}
                          BTC: {decimals: 8, min_bet: '1000', max_bet: '5000000', max_win: '50000000'}
                          GC: {decimals: 2, min_bet: '10', max_bet: '50000', max_win: '500000'}
                          SC: {decimals: 2, min_bet: '10', max_bet: '50000', max_win: '500000'}
                        themes:
                          - {id: apex-racing, name: Apex Racing, default: true}
                          - {id: neon-circuit, name: Neon Circuit, default: false}
                        assets:
                          tile_square: https://cdn.spartacus.invalid/crash/tile-1x1@{dpr}x.webp
                          tile_portrait: https://cdn.spartacus.invalid/crash/tile-3x4@{dpr}x.webp
                          tile_landscape: https://cdn.spartacus.invalid/crash/tile-16x9@{dpr}x.webp
                          tile_wide: https://cdn.spartacus.invalid/crash/tile-21x9@{dpr}x.webp
                          logo: https://cdn.spartacus.invalid/crash/logo.svg
                          icon: https://cdn.spartacus.invalid/crash/icon.svg
                          background: https://cdn.spartacus.invalid/crash/bg@{dpr}x.webp
                          preview_video: https://cdn.spartacus.invalid/crash/preview.mp4
                          screenshots:
                            - https://cdn.spartacus.invalid/crash/shot-1@2x.webp
                          localized: https://cdn.spartacus.invalid/crash/{lang}/tile-1x1@{dpr}x.webp
                        translations: https://cdn.spartacus.invalid/crash/i18n/{lang}.json
                      - code: mines
                        name: Apex Mines
                        provider: spartacus
                        status: live
                        category: mines
                        rtp: 9700
                        volatility: medium
                        max_multiplier: '10000.0000'
                        multiplier_decimals: 4
                        demo: false
                        max_bets_per_round: 1
                        languages: [en]
                        default_language: en
                        currencies:
                          EUR: {decimals: 2, min_bet: '100', max_bet: '50000', max_win: '500000'}
                        themes:
                          - {id: apex-mines, name: Apex Mines, default: true}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '5XX': {$ref: '#/components/responses/ServerError'}

  /v1/sessions:
    post:
      tags: [Sessions]
      operationId: createSession
      summary: Create session
      description: |
        Creates a game session and returns a launch URL. The launch URL is single use and expires
        60 seconds after creation. Redirect or embed the player at that URL.

        Spartacus resolves `token` to a player by calling the `player` webhook.

        Listing more than one currency enables an in-game currency switcher. Switching is rejected
        while the player has an open bet. No conversion is performed between currencies.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSessionRequest'
            examples:
              realMoney:
                summary: Single currency
                value:
                  token: 3f9a1c07d2b84e5f9c1a4408bd7e2f61
                  game_code: crash
                  currencies: [EUR]
                  lang: pt-BR
                  mode: real
                  lobby_url: https://casino.example.invalid/lobby
              social:
                summary: Gold and Sweeps with a currency switcher
                value:
                  token: 8c21be40f7a95d1344e2cb90117fa3de
                  game_code: crash
                  currencies: [GC, SC]
                  mode: real
                  theme: neon-circuit
              demo:
                summary: Demo play
                value:
                  token: b570d9ae1c4f28a6772e1d0cc4419f83
                  game_code: crash
                  currencies: [EUR]
                  mode: demo
      responses:
        '201':
          description: Session created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSessionResponse'
              examples:
                default:
                  value:
                    session_id: 0199c0f1-8f21-7a3b-9c44-1f2b3c4d5e6f
                    url: https://play.spartacus.invalid/s/xK7pB2mK9tR4vN6sL0wZ8yH3
                    expires_at: '2026-09-20T11:46:23.000Z'
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/OperatorDraining'}
        '422': {$ref: '#/components/responses/Unprocessable'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '5XX': {$ref: '#/components/responses/ServerError'}

  /v1/rounds/{round_id}:
    parameters:
      - $ref: '#/components/parameters/RoundId'
    get:
      tags: [Rounds]
      operationId: getRound
      summary: Retrieve round
      description: |
        Returns a round's outcome, fairness verification data, and the bets placed by this
        operator's players.

        The `bets` array contains one entry per bet. A player may place more than one bet per
        round in games where `max_bets_per_round` is greater than 1.
      responses:
        '200':
          description: The round.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Round'
              examples:
                crashed:
                  value:
                    round_id: 0199c0f4-aaaa-7bbb-8ccc-ddddeeeeffff
                    game_code: crash
                    status: crashed
                    crash_multiplier: '3.17'
                    currency: EUR
                    started_at: '2026-09-20T11:50:03.250Z'
                    ended_at: '2026-09-20T11:50:22.489Z'
                    fairness:
                      server_seed: 1d7a3e9c5f0b6d2a8e4c1f7b93d5a0e6c28f4b4f2c8b1d9e7a3c5f0b6d2a8e4c
                      drand_round: 21445873
                      verify_url: https://verify.spartacus.invalid/r/0199c0f4-aaaa-7bbb-8ccc-ddddeeeeffff
                    bets:
                      - user: player-9f3c1a77
                        transaction_uuid: 0199c0f5-2222-7bbb-8ccc-333344445555
                        bet: '1000'
                        win: '2370'
                        multiplier: '2.37'
                        status: won
                        settle_reason: cash_out
                        rank: 1
                      - user: player-c1d2e3f4
                        transaction_uuid: 0199c0f5-7777-7eee-8fff-888899990000
                        bet: '500'
                        win: '0'
                        multiplier: null
                        status: lost
                        settle_reason: lost
                        rank: null
                open:
                  value:
                    round_id: 0199c0f6-1111-7aaa-8bbb-ccccddddeeee
                    game_code: mines
                    status: open
                    started_at: '2026-09-20T12:00:00.000Z'
                    fairness:
                      commit: ce00f477584ef0a54801c25ce2823214e0bcf3559e7149e1cc39e05c9f46ff3f
                      client_seed: a1b2c3d4
                      nonce: '17'
                      bet_close: '2026-09-20T12:00:05.000Z'
                      drand_round: 21445990
                    bets: []
                settled:
                  value:
                    round_id: 0199c0f6-1111-7aaa-8bbb-ccccddddeeee
                    game_code: mines
                    status: settled
                    currency: EUR
                    started_at: '2026-09-20T12:00:00.000Z'
                    ended_at: '2026-09-20T12:00:41.300Z'
                    fairness:
                      commit: ce00f477584ef0a54801c25ce2823214e0bcf3559e7149e1cc39e05c9f46ff3f
                      server_seed: 7118cd977393e75eff86f7324c56e15e983442e304db25884bb4128d3f6a9358
                      client_seed: a1b2c3d4
                      nonce: '17'
                      bet_close: '2026-09-20T12:00:05.000Z'
                      drand_round: 21445990
                      drand_randomness: 1c1c65e8f2de96f1f1dd8a3b574871477a13cc8fbd46b591e988206170735238
                      placement: [3, 11, 27]
                      rotation: {rotation_seq: 4, revealed_at: '2026-09-20T13:00:00.000Z', last_nonce: '17'}
                      verify_url: https://verify.spartacus.invalid/r/0199c0f6-1111-7aaa-8bbb-ccccddddeeee
                    bets:
                      - user: player-9f3c1a77
                        transaction_uuid: 0199c0f6-2222-7bbb-8ccc-333344445555
                        bet: '1000'
                        win: '2120'
                        multiplier: '2.1200'
                        status: won
                        settle_reason: board_cleared
                        rank: 1
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '5XX': {$ref: '#/components/responses/ServerError'}

  /v1/skins:
    get:
      tags: [Skins]
      operationId: listSkins
      summary: List skins
      description: |
        Returns the skin catalogue. Skins are released independently of games, so this is a
        separate resource from `/v1/games`.
      parameters:
        - name: game_code
          in: query
          required: false
          description: Restrict results to one game.
          schema: {type: string, maxLength: 64}
          example: crash
      responses:
        '200':
          description: Skin catalogue.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SkinList'
              examples:
                default:
                  value:
                    skins:
                      - id: livery-crimson
                        name: Crimson
                        game_code: crash
                        theme: apex-racing
                        rarity: rare
                        default: false
                        unlock_hint: Reach level 7
                        assets:
                          thumbnail: https://cdn.spartacus.invalid/skins/crimson/thumb@{dpr}x.webp
                          preview: https://cdn.spartacus.invalid/skins/crimson/preview@{dpr}x.webp
                      - id: livery-starter
                        name: Starter White
                        game_code: crash
                        theme: apex-racing
                        rarity: common
                        default: true
                        assets:
                          thumbnail: https://cdn.spartacus.invalid/skins/starter/thumb@{dpr}x.webp
                          preview: https://cdn.spartacus.invalid/skins/starter/preview@{dpr}x.webp
        '401': {$ref: '#/components/responses/Unauthorized'}
        '5XX': {$ref: '#/components/responses/ServerError'}

  /v1/players/{user}/skins/{skin_id}:
    parameters:
      - $ref: '#/components/parameters/User'
      - $ref: '#/components/parameters/SkinId'
    put:
      tags: [Skins]
      operationId: unlockSkin
      summary: Unlock skin
      description: |
        Grants a skin to a player. Idempotent.

        Spartacus does not derive entitlements. A player has access to a skin only after this
        endpoint is called for it.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UnlockSkinRequest'
            examples:
              permanent:
                value: {reason: level_up}
              temporary:
                value: {reason: promo, expires_at: '2026-12-31T23:59:59.000Z'}
      responses:
        '200':
          description: Skin unlocked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SkinUnlock'
              examples:
                default:
                  value:
                    user: player-9f3c1a77
                    skin_id: livery-crimson
                    unlocked: true
                    unlocked_at: '2026-09-21T09:14:02.118Z'
        '400':
          description: Invalid request.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
              examples:
                default:
                  value: {error: {code: validation_error, message: 'expires_at is not an RFC 3339 date-time'}}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '5XX': {$ref: '#/components/responses/ServerError'}
    delete:
      tags: [Skins]
      operationId: revokeSkin
      summary: Revoke skin
      description: |
        Removes a skin from a player. Idempotent. If the skin was selected, the player reverts to
        the theme's default skin at their next launch.
      responses:
        '204':
          description: Skin revoked.
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '5XX': {$ref: '#/components/responses/ServerError'}

  /v1/players/{user}:
    parameters:
      - $ref: '#/components/parameters/User'
    get:
      tags: [Players]
      operationId: getPlayer
      summary: Retrieve player
      description: |
        Returns a player's unlocked skins and aggregate statistics. Statistics are keyed by
        currency.
      parameters:
        - name: game_code
          in: query
          required: false
          description: Restrict statistics to one game.
          schema: {type: string, maxLength: 64}
          example: crash
      responses:
        '200':
          description: The player.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Player'
              examples:
                default:
                  value:
                    user: player-9f3c1a77
                    skins:
                      unlocked: [livery-starter, livery-crimson]
                      selected: livery-crimson
                    stats:
                      EUR:
                        rounds_played: 1284
                        total_bet: '1284000'
                        total_win: '1245300'
                        net: '-38700'
                        biggest_win: '47400'
                        best_multiplier: '47.40'
                        first_played_at: '2026-06-02T18:22:41.000Z'
                        last_played_at: '2026-09-21T09:02:17.400Z'
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '5XX': {$ref: '#/components/responses/ServerError'}

  /v1/players/{user}/rounds:
    parameters:
      - $ref: '#/components/parameters/User'
    get:
      tags: [Players]
      operationId: listPlayerRounds
      summary: List player rounds
      description: |
        Returns a player's round history, most recent first. Each entry includes `round_id` for
        use with `GET /v1/rounds/{round_id}`.
      parameters:
        - name: game_code
          in: query
          required: false
          description: Restrict results to one game.
          schema: {type: string, maxLength: 64}
        - name: limit
          in: query
          required: false
          description: Maximum entries per page.
          schema: {type: integer, minimum: 1, maximum: 200, default: 50}
        - name: cursor
          in: query
          required: false
          description: Cursor from the previous page's `next_cursor`.
          schema: {type: string, maxLength: 512}
      responses:
        '200':
          description: One page of round history.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlayerRoundPage'
              examples:
                default:
                  value:
                    rounds:
                      - round_id: 0199c0f4-aaaa-7bbb-8ccc-ddddeeeeffff
                        game_code: crash
                        settled_at: '2026-09-20T11:50:22.489Z'
                        currency: EUR
                        bet: '1000'
                        win: '2370'
                        multiplier: '2.37'
                        crash_multiplier: '3.17'
                        status: won
                        settle_reason: cash_out
                    next_cursor: eyJhZnRlciI6IjAxOTljMGY0In0
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '5XX': {$ref: '#/components/responses/ServerError'}

webhooks:
  player:
    post:
      tags: [Wallet webhooks]
      operationId: webhookPlayer
      summary: Resolve player
      description: |
        `POST {base}/player`

        Sent once, when a player opens a game, to resolve the launch token to a player account.
        No funds are affected.

        Return `user_blocked` for a blocked or self-excluded player. Do not return a balance;
        Spartacus calls `balance` immediately afterwards.
      security:
        - webhookSignature: []
      parameters:
        - $ref: '#/components/parameters/SignatureHeader'
        - $ref: '#/components/parameters/RequestIdHeader'
        - $ref: '#/components/parameters/AttemptHeader'
        - $ref: '#/components/parameters/VersionHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlayerRequest'
            examples:
              default:
                value:
                  token: 3f9a1c07d2b84e5f9c1a4408bd7e2f61
                  game_code: crash
                  ip: 203.0.113.42
      responses:
        '200':
          description: Player resolved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlayerResponse'
              examples:
                default:
                  value:
                    user: player-9f3c1a77
                    country: BR
                    lang: pt-BR
                    nickname: Piloto42
        '400': {$ref: '#/components/responses/PlayerBadRequest'}
        '401': {$ref: '#/components/responses/PlayerUnauthorized'}
        '403': {$ref: '#/components/responses/PlayerForbidden'}
        '5XX': {$ref: '#/components/responses/WalletServerError'}

  balance:
    post:
      tags: [Wallet webhooks]
      operationId: webhookBalance
      summary: Get balance
      description: |
        `POST {base}/balance`

        Returns the player's current balance for the requested currencies. Read-only and safe to
        retry.

        Sent after `player`, after a currency switch, and periodically while a session is idle.
      security:
        - webhookSignature: []
      parameters:
        - $ref: '#/components/parameters/SignatureHeader'
        - $ref: '#/components/parameters/RequestIdHeader'
        - $ref: '#/components/parameters/AttemptHeader'
        - $ref: '#/components/parameters/VersionHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BalanceRequest'
            examples:
              default:
                value:
                  user: player-9f3c1a77
                  currencies: [GC, SC]
                  game_code: crash
      responses:
        '200':
          description: Current balances.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceResponse'
              examples:
                default:
                  value:
                    balances: {GC: '250000', SC: '1200'}
        '400': {$ref: '#/components/responses/BalanceBadRequest'}
        '401': {$ref: '#/components/responses/BalanceUnauthorized'}
        '404': {$ref: '#/components/responses/BalanceNotFound'}
        '5XX': {$ref: '#/components/responses/WalletServerError'}

  bet:
    post:
      tags: [Wallet webhooks]
      operationId: webhookBet
      summary: Debit stake
      description: |
        `POST {base}/bet`

        Debits a stake from the player's balance. Validate funds, betting limits and player status
        here; no other webhook performs these checks.

        Sent once and never retried. If the request times out, Spartacus sends a `rollback` for
        the same `transaction_uuid`. Spartacus does not retry a `bet`, but a network path may
        still deliver one twice: a duplicate delivery must still be replayed from the stored
        result, never debited a second time.

        The `transaction_uuid` of a bet identifies that bet in the `win` and `rollback` webhooks
        and in the rounds API.
      security:
        - webhookSignature: []
      parameters:
        - $ref: '#/components/parameters/SignatureHeader'
        - $ref: '#/components/parameters/RequestIdHeader'
        - $ref: '#/components/parameters/AttemptHeader'
        - $ref: '#/components/parameters/VersionHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BetRequest'
            examples:
              default:
                value:
                  user: player-9f3c1a77
                  currency: EUR
                  decimals: 2
                  game_code: crash
                  round_id: 0199c0f4-aaaa-7bbb-8ccc-ddddeeeeffff
                  transaction_uuid: 0199c0f5-2222-7bbb-8ccc-333344445555
                  amount: '1000'
                  created_at: '2026-09-20T11:49:58.101Z'
      responses:
        '200':
          description: Stake debited, or a stored result replayed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WalletResponse'
              examples:
                default:
                  value:
                    balances: {EUR: '11400'}
                    transaction_id: your-ref-88120391
                    processed_at: '2026-09-20T11:49:58.198Z'
        '400': {$ref: '#/components/responses/BetBadRequest'}
        '401': {$ref: '#/components/responses/BetUnauthorized'}
        '402': {$ref: '#/components/responses/BetPaymentRequired'}
        '403': {$ref: '#/components/responses/BetForbidden'}
        '404': {$ref: '#/components/responses/BetNotFound'}
        '409': {$ref: '#/components/responses/BetConflict'}
        '422': {$ref: '#/components/responses/BetUnprocessable'}
        '5XX': {$ref: '#/components/responses/WalletServerError'}

  win:
    post:
      tags: [Wallet webhooks]
      operationId: webhookWin
      summary: Credit settlement
      description: |
        `POST {base}/win`

        Credits the settlement of a bet. Sent exactly once for every debited bet, including losing
        bets, which carry `amount: "0"`.

        The payout of a bet is credited exactly once. A retry of the same `win` carries the same
        `transaction_uuid` and replays the stored result. If `bet_transaction_uuid` names a bet
        that already has a `win` under a different `transaction_uuid`, return `409`
        `transaction_settled` and move no funds. Spartacus never sends this in normal operation;
        receiving it signals a Spartacus bug, which Spartacus alerts on.

        Must be accepted regardless of session or player state. Retried for 24 hours on an
        indeterminate response. A settlement is never rolled back.
      security:
        - webhookSignature: []
      parameters:
        - $ref: '#/components/parameters/SignatureHeader'
        - $ref: '#/components/parameters/RequestIdHeader'
        - $ref: '#/components/parameters/AttemptHeader'
        - $ref: '#/components/parameters/VersionHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WinRequest'
            examples:
              won:
                summary: Cashed out at 2.37x
                value:
                  user: player-9f3c1a77
                  currency: EUR
                  decimals: 2
                  game_code: crash
                  round_id: 0199c0f4-aaaa-7bbb-8ccc-ddddeeeeffff
                  transaction_uuid: 0199c0f6-3333-7ccc-8ddd-444455556666
                  bet_transaction_uuid: 0199c0f5-2222-7bbb-8ccc-333344445555
                  amount: '2370'
                  multiplier: '2.37'
                  crash_multiplier: '3.17'
                  settle_reason: cash_out
                  round_closed: true
                  created_at: '2026-09-20T11:50:12.870Z'
              lost:
                summary: Losing bet
                value:
                  user: player-c1d2e3f4
                  currency: EUR
                  decimals: 2
                  game_code: crash
                  round_id: 0199c0f4-aaaa-7bbb-8ccc-ddddeeeeffff
                  transaction_uuid: 0199c0f6-9999-7fff-8000-aaaabbbbcccc
                  bet_transaction_uuid: 0199c0f5-7777-7eee-8fff-888899990000
                  amount: '0'
                  crash_multiplier: '3.17'
                  settle_reason: lost
                  round_closed: true
                  created_at: '2026-09-20T11:50:22.489Z'
      responses:
        '200':
          description: Settlement credited, or a stored result replayed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WalletResponse'
              examples:
                default:
                  value:
                    balances: {EUR: '13770'}
                    transaction_id: your-ref-88120477
                    processed_at: '2026-09-20T11:50:12.955Z'
        '400': {$ref: '#/components/responses/WinBadRequest'}
        '401': {$ref: '#/components/responses/WinUnauthorized'}
        '404': {$ref: '#/components/responses/WinNotFound'}
        '409': {$ref: '#/components/responses/WinConflict'}
        '422': {$ref: '#/components/responses/WinUnprocessable'}
        '5XX': {$ref: '#/components/responses/WalletServerError'}

  rollback:
    post:
      tags: [Wallet webhooks]
      operationId: webhookRollback
      summary: Reverse stake
      description: |
        `POST {base}/rollback`

        Returns a stake for a bet that produced no outcome. Only a `bet` is reversed, and only one
        that has not been settled.

        If `rollback_transaction_uuid` is unknown, or `user` is unknown, return `200` with
        `status: "not_found"` and record the identifier. A `bet` arriving later with that
        `transaction_uuid` must be rejected with `transaction_rolled_back`.

        Never record the identifier of a transaction you already hold. A `rollback` whose
        `user` does not own the named transaction is answered `200` `status: "not_found"`
        with nothing moved and nothing recorded: marking it would poison the owner's own
        `transaction_uuid`, so a caller who cannot be identified as the owner can never
        disarm another player's bet.

        If `rollback_transaction_uuid` names a bet that already has a `win`, return `409`
        `transaction_settled` and move no funds. Spartacus never sends this in normal operation;
        receiving it signals a Spartacus bug, which Spartacus alerts on.

        Must be accepted regardless of session or player state. Retried for 24 hours on an
        indeterminate response.
      security:
        - webhookSignature: []
      parameters:
        - $ref: '#/components/parameters/SignatureHeader'
        - $ref: '#/components/parameters/RequestIdHeader'
        - $ref: '#/components/parameters/AttemptHeader'
        - $ref: '#/components/parameters/VersionHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RollbackRequest'
            examples:
              default:
                value:
                  user: player-9f3c1a77
                  currency: EUR
                  decimals: 2
                  transaction_uuid: 0199c0fa-5555-7eee-8fff-666677778888
                  rollback_transaction_uuid: 0199c0f5-2222-7bbb-8ccc-333344445555
                  amount: '1000'
                  reason: bet_timeout
                  created_at: '2026-09-20T11:48:02.500Z'
              player_left:
                summary: The player left before the round started
                value:
                  user: player-9f3c1a77
                  currency: EUR
                  decimals: 2
                  transaction_uuid: 0199c0fa-6666-7eee-8fff-777788889999
                  rollback_transaction_uuid: 0199c0f5-3333-7bbb-8ccc-444455556666
                  amount: '1000'
                  reason: player_left
                  created_at: '2026-09-20T11:48:03.000Z'
      responses:
        '200':
          description: Stake reversed, or the target transaction was unknown.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RollbackResponse'
              examples:
                reversed:
                  summary: Target found and reversed
                  value:
                    balances: {EUR: '12400'}
                    transaction_id: your-ref-88120512
                    status: rolled_back
                    processed_at: '2026-09-20T11:48:02.611Z'
                unknown:
                  summary: Target unknown
                  value:
                    balances: {EUR: '12400'}
                    transaction_id: your-ref-88120513
                    status: not_found
        '400': {$ref: '#/components/responses/RollbackBadRequest'}
        '401': {$ref: '#/components/responses/RollbackUnauthorized'}
        '409': {$ref: '#/components/responses/RollbackConflict'}
        '422': {$ref: '#/components/responses/RollbackUnprocessable'}
        '5XX': {$ref: '#/components/responses/WalletServerError'}

components:

  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: |
        Bearer API key issued per environment. Sandbox keys are prefixed `tk_test_`, production
        keys `tk_live_`. Multiple keys may be active at once.
    webhookSignature:
      type: apiKey
      in: header
      name: X-Telos-Signature
      description: |
        HMAC-SHA256 signature over `{t}.{raw request body}`, formatted as
        `t=<unix seconds>,v1=<hex digest>`. Each digest is 64 lowercase hexadecimal characters.
        Up to 3 `v1=` entries are sent during a secret rotation; the request is valid if any one
        matches.

  parameters:
    RoundId:
      name: round_id
      in: path
      required: true
      description: Round identifier.
      schema: {$ref: '#/components/schemas/Uuid'}
    User:
      name: user
      in: path
      required: true
      description: Operator player identifier, URL-encoded.
      schema: {type: string, maxLength: 128}
      example: player-9f3c1a77
    SkinId:
      name: skin_id
      in: path
      required: true
      description: Skin identifier from `GET /v1/skins`.
      schema: {type: string, maxLength: 64}
      example: livery-crimson
    SignatureHeader:
      name: X-Telos-Signature
      in: header
      required: true
      description: HMAC-SHA256 signature over `{t}.{raw request body}`.
      schema: {type: string, maxLength: 256}
      example: t=1789041923,v1=5257a869e7c1f0b3d4a29e8c7b6f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d
    RequestIdHeader:
      name: X-Telos-Request-Id
      in: header
      required: true
      description: Unique per delivery attempt. Include it in support requests.
      schema: {$ref: '#/components/schemas/Uuid'}
    AttemptHeader:
      name: X-Telos-Attempt
      in: header
      required: false
      description: |
        Delivery attempt number, starting at 1. Informational. Excluded from the signature and
        from the request body so that retries remain byte-identical.
      schema: {type: integer, minimum: 1}
      example: 3
    VersionHeader:
      name: X-Telos-Version
      in: header
      required: true
      description: |
        Payload version, fixed per operator at onboarding. `2026-11-01` is the only v1 payload
        version; a new payload version is opt-in per operator at onboarding.
      schema:
        type: string
        enum: ['2026-11-01']
        description: |
          Open: new values may be added; treat an unknown value as an error (`401
          unauthorized`).
      example: '2026-11-01'

  responses:
    BadRequest:
      description: Invalid request.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          examples:
            default:
              value: {error: {code: validation_error, message: 'currencies must not be empty'}}
    Unauthorized:
      description: Missing, unknown or expired API key.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          examples:
            default:
              value: {error: {code: unauthorized, message: 'Unknown API key'}}
    NotFound:
      description: Resource not found for this operator.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          examples:
            default:
              value: {error: {code: not_found, message: 'No such round'}}
    Unprocessable:
      description: Request understood but cannot be processed.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          examples:
            default:
              value: {error: {code: currency_mismatch, message: 'BTC is not enabled for crash'}}
    RateLimited:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema: {type: integer}
          example: 2
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          examples:
            default:
              value: {error: {code: rate_limited, message: 'Rate limit exceeded'}}
    OperatorDraining:
      description: |
        The operator is being wound down: its API keys still work and open rounds finish, but no new
        session starts. Final for this request.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          examples:
            default:
              value: {error: {code: operator_draining, message: 'The operator is not accepting new sessions'}}
    ServerError:
      description: Internal error. The outcome of the request is indeterminate.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          examples:
            default:
              value: {error: {code: internal_error, message: 'Internal error'}}
    PlayerBadRequest:
      description: |
        The request is not one this contract describes: the body is not JSON, does not match the
        schema, or a required header is missing or malformed. Final: no funds move. Only a `401` on
        `win` or `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ValidationError'}
          examples:
            validationError:
              value: {error: {code: validation_error, message: 'token must be a string'}}
    BalanceBadRequest:
      description: |
        The request is not one this contract describes: the body is not JSON, does not match the
        schema, or a required header is missing or malformed. Final: no funds move. Only a `401` on
        `win` or `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ValidationError'}
          examples:
            validationError:
              value: {error: {code: validation_error, message: 'currencies must not be empty'}}
    BetBadRequest:
      description: |
        The request is not one this contract describes: the body is not JSON, does not match the
        schema, or a required header is missing or malformed. Final: no funds move. Only a `401` on
        `win` or `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ValidationError'}
          examples:
            validationError:
              value: {error: {code: validation_error, message: 'amount must be a string in minor units'}}
    WinBadRequest:
      description: |
        The request is not one this contract describes: the body is not JSON, does not match the
        schema, or a required header is missing or malformed. Final: no funds move. Only a `401` on
        `win` or `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ValidationError'}
          examples:
            validationError:
              value: {error: {code: validation_error, message: 'bet_transaction_uuid must be a UUID'}}
    RollbackBadRequest:
      description: |
        The request is not one this contract describes: the body is not JSON, does not match the
        schema, or a required header is missing or malformed. Final: no funds move. Only a `401` on
        `win` or `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ValidationError'}
          examples:
            validationError:
              value: {error: {code: validation_error, message: 'rollback_transaction_uuid is required'}}
    PlayerUnauthorized:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/PlayerUnauthorizedError'}
          examples:
            unauthorized:
              value: {error: {code: unauthorized, message: 'Signature does not match'}}
            tokenInvalid:
              value: {error: {code: token_invalid, message: 'Launch token expired'}}
    PlayerForbidden:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/PlayerForbiddenError'}
          examples:
            blocked:
              value: {error: {code: user_blocked, message: 'Player is self-excluded'}}
    BalanceUnauthorized:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/UnauthorizedError'}
          examples:
            unauthorized:
              value: {error: {code: unauthorized, message: 'Signature does not match'}}
    BalanceNotFound:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/UserNotFoundError'}
          examples:
            userNotFound:
              value: {error: {code: user_not_found, message: 'Unknown user'}}
    BetUnauthorized:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/UnauthorizedError'}
          examples:
            unauthorized:
              value: {error: {code: unauthorized, message: 'Signature does not match'}}
    BetPaymentRequired:
      description: |
        Insufficient funds. The bet is rejected and no rollback is sent. Final: no funds move.
        Only a `401` on `win` or `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/InsufficientFundsError'}
          examples:
            insufficientFunds:
              value: {error: {code: insufficient_funds, message: 'Balance 4.00 below stake 10.00'}}
    BetForbidden:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/BetForbiddenError'}
          examples:
            blocked:
              value: {error: {code: user_blocked, message: 'Player is self-excluded'}}
            betNotAllowed:
              value: {error: {code: bet_not_allowed, message: 'Daily loss limit reached'}}
    BetNotFound:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/UserNotFoundError'}
          examples:
            userNotFound:
              value: {error: {code: user_not_found, message: 'Unknown user'}}
    BetConflict:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/BetConflictError'}
          examples:
            conflict:
              value: {error: {code: idempotency_conflict, message: 'Body differs from stored request'}}
            rolledBack:
              value: {error: {code: transaction_rolled_back, message: 'Transaction already rolled back'}}
    BetUnprocessable:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/CurrencyMismatchError'}
          examples:
            currencyMismatch:
              value: {error: {code: currency_mismatch, message: 'EUR is configured with 2 decimals'}}
    WinUnauthorized:
      description: |
        Error returned by the operator. No funds move. Unlike other `4xx`, Spartacus retries a
        `401` on `win` like a `5xx` for the 24-hour window, each attempt with a fresh `t` and
        signature, and raises an alert.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/UnauthorizedError'}
          examples:
            unauthorized:
              value: {error: {code: unauthorized, message: 'Signature does not match'}}
    WinNotFound:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/WinNotFoundError'}
          examples:
            userNotFound:
              value: {error: {code: user_not_found, message: 'Unknown user'}}
            transactionNotFound:
              value: {error: {code: transaction_not_found, message: 'Unknown bet_transaction_uuid'}}
    WinConflict:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/WinConflictError'}
          examples:
            conflict:
              value: {error: {code: idempotency_conflict, message: 'Body differs from stored request'}}
            rolledBack:
              value: {error: {code: transaction_rolled_back, message: 'Bet was rolled back'}}
            settled:
              value: {error: {code: transaction_settled, message: 'Bet already has a win'}}
    WinUnprocessable:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/CurrencyMismatchError'}
          examples:
            currencyMismatch:
              value: {error: {code: currency_mismatch, message: 'EUR is configured with 2 decimals'}}
    RollbackUnauthorized:
      description: |
        Error returned by the operator. No funds move. Unlike other `4xx`, Spartacus retries a
        `401` on `rollback` like a `5xx` for the 24-hour window, each attempt with a fresh `t`
        and signature, and raises an alert.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/UnauthorizedError'}
          examples:
            unauthorized:
              value: {error: {code: unauthorized, message: 'Signature does not match'}}
    RollbackConflict:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/RollbackConflictError'}
          examples:
            conflict:
              value: {error: {code: idempotency_conflict, message: 'Body differs from stored request'}}
            settled:
              value: {error: {code: transaction_settled, message: 'Bet already has a win'}}
    RollbackUnprocessable:
      description: |
        Error returned by the operator. Final: no funds move. Only a `401` on `win` or
        `rollback` is retried.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/CurrencyMismatchError'}
          examples:
            currencyMismatch:
              value: {error: {code: currency_mismatch, message: 'EUR is configured with 2 decimals'}}
    WalletServerError:
      description: |
        Error returned by the operator. Any `5xx` is indeterminate, whatever its body, and
        Spartacus retries `win` and `rollback`.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          examples:
            internal:
              value: {error: {code: internal_error, message: 'Internal error'}}

  schemas:

    Uuid:
      type: string
      format: uuid
      description: UUIDv7 identifier. Opaque.
      examples: ['0199c0f4-aaaa-7bbb-8ccc-ddddeeeeffff']

    MinorUnits:
      type: string
      pattern: '^(0|[1-9][0-9]{0,37})$'
      description: Non-negative integer amount in minor units, as a decimal string.
      examples: ['1000', '0', '10000000000000000']

    SignedMinorUnits:
      type: string
      pattern: '^-?(0|[1-9][0-9]{0,37})$'
      description: Signed integer amount in minor units, as a decimal string.
      examples: ['-38700', '147400']

    Multiplier:
      type: string
      pattern: '^(0|[1-9][0-9]*)\.[0-9]{2,4}$'
      description: |
        Multiplier as a decimal string. The number of digits after the point is the game's
        `multiplier_decimals` (2 to 4); format by it, never by assuming two digits.
      examples: ['2.37', '1.00', '10000.00']

    CurrencyCode:
      type: string
      pattern: '^[A-Z0-9]{2,10}$'
      description: |
        ISO 4217 code for fiat currencies, or a registry code for crypto (`BTC`, `ETH`, `USDT`)
        and social currencies (`GC`, `SC`).
      examples: ['EUR', 'USDT', 'SC']

    Decimals:
      type: integer
      minimum: 0
      maximum: 18
      description: Minor-unit exponent for the currency.
      examples: [2, 8, 18]

    Balances:
      type: object
      description: Balance per currency, in minor units.
      additionalProperties: {$ref: '#/components/schemas/MinorUnits'}
      examples:
        - {GC: '250000', SC: '1200'}

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: |
                Codes returned by the operator: `validation_error`, `unauthorized`, `insufficient_funds`,
                `user_blocked`, `bet_not_allowed`, `token_invalid`, `user_not_found`,
                `transaction_not_found`, `transaction_rolled_back`, `transaction_settled`,
                `idempotency_conflict`, `currency_mismatch`. The Errors section lists the
                condition and status of each.

                Codes returned by Spartacus: `validation_error`, `unauthorized`, `not_found`,
                `rate_limited`, `internal_error`, `operator_draining`.

                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
              enum:
                - insufficient_funds
                - user_blocked
                - bet_not_allowed
                - token_invalid
                - user_not_found
                - transaction_not_found
                - transaction_rolled_back
                - transaction_settled
                - idempotency_conflict
                - currency_mismatch
                - validation_error
                - unauthorized
                - not_found
                - rate_limited
                - internal_error
                - operator_draining
            message:
              type: string
              maxLength: 512
              description: Human-readable description. Not intended for display to players.

    ErrorMessage:
      type: string
      maxLength: 512
      description: Human-readable description. Not intended for display to players.

    ValidationError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [validation_error]
              description: |
                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
            message: {$ref: '#/components/schemas/ErrorMessage'}

    UnauthorizedError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [unauthorized]
              description: |
                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
            message: {$ref: '#/components/schemas/ErrorMessage'}

    PlayerUnauthorizedError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [unauthorized, token_invalid]
              description: |
                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
            message: {$ref: '#/components/schemas/ErrorMessage'}

    PlayerForbiddenError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [user_blocked]
              description: |
                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
            message: {$ref: '#/components/schemas/ErrorMessage'}

    UserNotFoundError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [user_not_found]
              description: |
                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
            message: {$ref: '#/components/schemas/ErrorMessage'}

    InsufficientFundsError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [insufficient_funds]
              description: |
                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
            message: {$ref: '#/components/schemas/ErrorMessage'}

    BetForbiddenError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [user_blocked, bet_not_allowed]
              description: |
                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
            message: {$ref: '#/components/schemas/ErrorMessage'}

    BetConflictError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [idempotency_conflict, transaction_rolled_back]
              description: |
                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
            message: {$ref: '#/components/schemas/ErrorMessage'}

    CurrencyMismatchError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [currency_mismatch]
              description: |
                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
            message: {$ref: '#/components/schemas/ErrorMessage'}

    WinNotFoundError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [user_not_found, transaction_not_found]
              description: |
                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
            message: {$ref: '#/components/schemas/ErrorMessage'}

    WinConflictError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [idempotency_conflict, transaction_rolled_back, transaction_settled]
              description: |
                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
            message: {$ref: '#/components/schemas/ErrorMessage'}

    RollbackConflictError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [idempotency_conflict, transaction_settled]
              description: |
                Open: new values may be added; treat an unknown value as an error of that HTTP
                status.
            message: {$ref: '#/components/schemas/ErrorMessage'}

    GameList:
      type: object
      required: [games]
      properties:
        games:
          type: array
          items: {$ref: '#/components/schemas/Game'}

    Game:
      type: object
      required: [code, name, category, currencies, demo, max_bets_per_round, max_multiplier, multiplier_decimals]
      properties:
        code:
          type: string
          maxLength: 64
          description: Game identifier used in `POST /v1/sessions`.
          examples: ['crash']
        name:
          type: string
          maxLength: 128
          description: Display name.
        provider:
          type: string
          maxLength: 64
          description: Game provider.
        released:
          type: string
          format: date
          description: Release date.
        status:
          type: string
          enum: [live, beta, sunset]
          description: |
            Availability status. Open: new values may be added; treat an unknown value as `sunset`.
        category:
          type: string
          maxLength: 32
          description: Primary category.
          examples: ['crash']
        subcategory:
          type: string
          maxLength: 32
          description: Secondary category.
        tags:
          type: array
          description: Lobby filter tags.
          items: {type: string, maxLength: 32}
        features:
          type: array
          description: Supported gameplay features.
          items:
            type: string
            examples: ['auto_cashout', 'leaderboard', 'live_players', 'provably_fair', 'solo_mode']
        platforms:
          type: array
          description: Supported device classes.
          items:
            type: string
            enum: [mobile, desktop]
            description: |
              Open: new values may be added; treat an unknown value as a device class the game does not support.
        orientation:
          type: array
          description: Supported screen orientations.
          items:
            type: string
            enum: [portrait, landscape]
            description: |
              Open: new values may be added; treat an unknown value as an orientation the game does not support.
        rtp:
          type: integer
          minimum: 0
          maximum: 10000
          description: Return to player in basis points. `9700` is 97.00%.
        volatility:
          type: string
          enum: [low, medium, high]
          description: |
            Volatility classification. Open: new values may be added; treat an unknown value as `high`.
        max_multiplier:
          allOf: [{$ref: '#/components/schemas/Multiplier'}]
          description: |
            Highest multiplier the game can reach: the game's cap. For crash it is `CAP`,
            `10000.00`; a round that reaches it ends `capped` and open bets settle at it.
        multiplier_decimals:
          type: integer
          minimum: 2
          maximum: 4
          description: |
            Digits after the decimal point in every `Multiplier` of this game. Crash is `2`.
            Format multipliers by this value, never by assuming two digits.
        round_seconds:
          type: integer
          description: Typical round duration in seconds. Indicative.
        demo:
          type: boolean
          description: Whether demo play is available. Demo sessions do not call wallet webhooks.
        max_bets_per_round:
          type: integer
          minimum: 1
          description: |
            Maximum independent bets one player may place in a round. Read this value rather than
            assuming one bet per player per round.
        languages:
          type: array
          description: Supported BCP 47 language tags.
          items: {type: string, maxLength: 35}
        default_language:
          type: string
          maxLength: 35
          description: Language used when `lang` is omitted at session creation.
        jurisdictions:
          type: array
          description: Jurisdictions in which the game is certified.
          items: {type: string, maxLength: 6}
        currencies:
          type: object
          description: Currencies enabled for this operator, keyed by currency code.
          additionalProperties: {$ref: '#/components/schemas/GameCurrency'}
        themes:
          type: array
          description: Available themes.
          items: {$ref: '#/components/schemas/Theme'}
        assets: {$ref: '#/components/schemas/GameAssets'}
        translations:
          type: string
          description: URL template for translation bundles. `{lang}` falls back to `en`.

    GameCurrency:
      type: object
      required: [decimals, min_bet, max_bet, max_win]
      properties:
        decimals: {$ref: '#/components/schemas/Decimals'}
        min_bet:
          allOf: [{$ref: '#/components/schemas/MinorUnits'}]
          description: Minimum stake.
        max_bet:
          allOf: [{$ref: '#/components/schemas/MinorUnits'}]
          description: Maximum stake.
        max_win:
          allOf: [{$ref: '#/components/schemas/MinorUnits'}]
          description: |
            Maximum payout per bet. A bet is settled automatically, with `settle_reason:
            max_win`, when the round reaches `M_cap` = `max_win / stake` floored to the game's
            `multiplier_decimals` (floored to 2 decimals for crash). In crash, the effective forced
            settle is at `min(M_cap, CAP)`: when `M_cap` is at or above `CAP`, the bet settles at
            `CAP` with `settle_reason: flight_cap`. In a stepped game (mines), the bet settles at
            the last step at or below `M_cap`, with `settle_reason: max_win`. When `M_cap` is at or
            above the game's cap, it settles at the last step at or below the cap, with
            `settle_reason: multiplier_cap`.

    Theme:
      type: object
      required: [id, name, default]
      properties:
        id:
          type: string
          maxLength: 64
          description: Theme identifier.
        name:
          type: string
          maxLength: 128
          description: Display name.
        default:
          type: boolean
          description: Whether this theme is used when `theme` is omitted at session creation.

    GameAssets:
      type: object
      description: |
        Public CDN URLs. `{dpr}` accepts `1`, `2` or `3`. `{lang}` falls back to `en`.
      properties:
        tile_square:
          type: string
          description: 1:1 lobby tile.
        tile_portrait:
          type: string
          description: 3:4 lobby tile.
        tile_landscape:
          type: string
          description: 16:9 lobby tile.
        tile_wide:
          type: string
          description: 21:9 lobby tile.
        logo:
          type: string
          description: Game logo.
        icon:
          type: string
          description: Monochrome icon.
        background:
          type: string
          description: Background image.
        preview_video:
          type: string
          description: Short preview video.
        screenshots:
          type: array
          description: Gameplay screenshots.
          items: {type: string}
        localized:
          type: string
          description: URL template for localised tiles.

    SkinList:
      type: object
      required: [skins]
      properties:
        skins:
          type: array
          items: {$ref: '#/components/schemas/Skin'}

    Skin:
      type: object
      required: [id, name, game_code, rarity, default]
      properties:
        id:
          type: string
          maxLength: 64
          description: Skin identifier.
        name:
          type: string
          maxLength: 128
          description: Display name.
        game_code:
          type: string
          maxLength: 64
          description: Game this skin belongs to.
        theme:
          type: string
          maxLength: 64
          description: Theme this skin belongs to.
        rarity:
          type: string
          enum: [common, rare, epic, legendary]
          description: |
            Rarity tier. Open: new values may be added; treat an unknown value as `common`.
        default:
          type: boolean
          description: Whether all players have this skin without an unlock.
        unlock_hint:
          type: string
          maxLength: 128
          description: Display text shown on a locked skin. Does not grant access.
        assets:
          type: object
          properties:
            thumbnail:
              type: string
              description: Thumbnail image.
            preview:
              type: string
              description: Full preview image.

    UnlockSkinRequest:
      type: object
      properties:
        reason:
          type: string
          maxLength: 64
          description: Operator-defined label, stored and returned.
          examples: ['level_up', 'promo', 'purchase']
        expires_at:
          type: [string, 'null']
          format: date-time
          description: When the skin re-locks. Omit for a permanent unlock.

    SkinUnlock:
      type: object
      required: [user, skin_id, unlocked]
      properties:
        user:
          type: string
          maxLength: 128
          description: Operator player identifier.
        skin_id:
          type: string
          maxLength: 64
          description: Skin identifier.
        unlocked:
          type: boolean
          description: Whether the player currently has the skin.
        unlocked_at:
          type: string
          format: date-time
          description: When the skin was granted.
        expires_at:
          type: [string, 'null']
          format: date-time
          description: When the skin re-locks.

    Player:
      type: object
      required: [user, skins, stats]
      properties:
        user:
          type: string
          maxLength: 128
          description: Operator player identifier.
        skins:
          type: object
          properties:
            unlocked:
              type: array
              description: Skins the player has access to.
              items: {type: string, maxLength: 64}
            selected:
              type: string
              maxLength: 64
              description: Skin currently selected by the player.
        stats:
          type: object
          description: Aggregate statistics keyed by currency code.
          additionalProperties: {$ref: '#/components/schemas/PlayerStats'}

    PlayerStats:
      type: object
      required: [rounds_played, total_bet, total_win, net]
      properties:
        rounds_played:
          type: integer
          minimum: 0
          description: Rounds containing at least one settled bet.
        total_bet:
          allOf: [{$ref: '#/components/schemas/MinorUnits'}]
          description: Total staked.
        total_win:
          allOf: [{$ref: '#/components/schemas/MinorUnits'}]
          description: Total paid out.
        net:
          allOf: [{$ref: '#/components/schemas/SignedMinorUnits'}]
          description: '`total_win` minus `total_bet`. Negative when the player is down.'
        biggest_win:
          allOf: [{$ref: '#/components/schemas/MinorUnits'}]
          description: Largest single payout.
        best_multiplier:
          type: string
          pattern: '^(0|[1-9][0-9]*)\.[0-9]{2}$'
          description: |
            Highest multiplier cashed out at, always with exactly 2 decimals, floored. Statistics
            may span several games, which have no single `multiplier_decimals`.
        first_played_at:
          type: string
          format: date-time
          description: First settled bet.
        last_played_at:
          type: string
          format: date-time
          description: Most recent settled bet.

    PlayerRoundPage:
      type: object
      required: [rounds]
      properties:
        rounds:
          type: array
          items: {$ref: '#/components/schemas/PlayerRound'}
        next_cursor:
          type: string
          maxLength: 512
          description: Cursor for the next page. Absent on the last page.

    PlayerRound:
      type: object
      required: [round_id, settled_at, currency, bet, win, status, settle_reason]
      properties:
        round_id: {$ref: '#/components/schemas/Uuid'}
        game_code:
          type: string
          maxLength: 64
          description: Game the round belongs to.
        settled_at:
          type: string
          format: date-time
          description: When the bet settled.
        currency: {$ref: '#/components/schemas/CurrencyCode'}
        bet:
          allOf: [{$ref: '#/components/schemas/MinorUnits'}]
          description: Stake.
        win:
          allOf: [{$ref: '#/components/schemas/MinorUnits'}]
          description: Payout. `"0"` for a losing bet.
        multiplier:
          oneOf:
            - {$ref: '#/components/schemas/Multiplier'}
            - {type: 'null'}
          description: Multiplier cashed out at. `null` for a losing bet.
        crash_multiplier:
          allOf: [{$ref: '#/components/schemas/Multiplier'}]
          description: Multiplier at which the round ended; absent for games without a crash point.
        status:
          type: string
          enum: [won, lost, refunded]
          description: |
            Outcome of the bet. Open: new values may be added; treat an unknown value as indeterminate: reconcile with `GET /v1/rounds/{round_id}`.
        settle_reason: {$ref: '#/components/schemas/SettleReason'}

    CreateSessionRequest:
      type: object
      required: [token, game_code, currencies, mode]
      properties:
        token:
          type: string
          minLength: 8
          maxLength: 512
          description: |
            Operator-generated launch token identifying the player. Returned unchanged to the
            `player` webhook and never parsed by Spartacus. Printable ASCII. Resolved once, within
            60 seconds. Must be unguessable and must not encode personal data.
          examples: ['3f9a1c07d2b84e5f9c1a4408bd7e2f61']
        game_code:
          type: string
          maxLength: 64
          description: Game to launch.
          examples: ['crash']
        currencies:
          type: array
          minItems: 1
          uniqueItems: true
          description: |
            Currencies available to the player. More than one enables an in-game currency
            switcher.
          items: {$ref: '#/components/schemas/CurrencyCode'}
        mode:
          type: string
          enum: [real, demo]
          description: |
            `demo` sessions do not call wallet webhooks. Open: new values may be added; treat an unknown value as an error (`400 validation_error`).
        lang:
          type: string
          maxLength: 35
          description: BCP 47 language tag. Defaults to the game's `default_language`.
        theme:
          type: string
          maxLength: 64
          description: Theme identifier. Defaults to the game's default theme.
        lobby_url:
          type: string
          format: uri
          description: Destination for the in-game home control.
        deposit_url:
          type: string
          format: uri
          description: Destination shown to a player with insufficient funds.

    CreateSessionResponse:
      type: object
      required: [session_id, url, expires_at]
      properties:
        session_id: {$ref: '#/components/schemas/Uuid'}
        url:
          type: string
          format: uri
          description: Launch URL. Single use, valid for 60 seconds.
        expires_at:
          type: string
          format: date-time
          description: When the launch URL expires.

    Round:
      type: object
      required: [round_id, game_code, status, bets]
      properties:
        round_id: {$ref: '#/components/schemas/Uuid'}
        game_code:
          type: string
          maxLength: 64
          description: Game the round belongs to.
        status:
          type: string
          enum: [running, crashed, capped, cancelled, open, settled]
          description: |
            Round state. `capped`: the round reached the game's cap (`CAP` for crash) and
            `crash_multiplier` is that cap. A crash round is `running`, then `crashed`, `capped` or
            `cancelled`; a one-bet round (mines) is `open` while its bet is live, then `settled` or
            `cancelled`. Open: new values may be added; treat an unknown value as `running` (not final).
        crash_multiplier:
          allOf: [{$ref: '#/components/schemas/Multiplier'}]
          description: Multiplier at which the round ended; absent for games without a crash point.
        currency: {$ref: '#/components/schemas/CurrencyCode'}
        started_at:
          type: string
          format: date-time
          description: When the round started.
        ended_at:
          type: string
          format: date-time
          description: When the round ended.
        fairness: {$ref: '#/components/schemas/Fairness'}
        bets:
          type: array
          description: Bets placed by this operator's players. One entry per bet.
          items: {$ref: '#/components/schemas/RoundBet'}

    Fairness:
      type: object
      description: |
        Verification data for the round. The outcome is derived from a server seed committed
        before betting opened and a public drand beacon published after betting closed.
      properties:
        server_seed:
          type: string
          description: Server seed, revealed after the round ends, or at seed rotation for solo games.
        commit:
          type: string
          pattern: '^[0-9a-f]{64}$'
          description: SHA-256 of the server seed, published before betting opened.
        client_seed:
          type: string
          pattern: '^[A-Za-z0-9]{1,64}$'
          description: The player's client seed (solo games).
        nonce:
          type: string
          pattern: '^(0|[1-9][0-9]{0,19})$'
          description: The round's nonce under its seed pair, a decimal string from 0 to 2^64 − 1 (solo games).
        bet_close:
          type: string
          format: date-time
          description: When betting closed and the beacon round was fixed (solo games).
        placement:
          type: array
          maxItems: 48
          items: {type: integer, minimum: 0, maximum: 48}
          description: |
            The placement derived from the beacon, ascending (solo games). Omitted, never `null`, for a
            round settled before any beacon was used.
        drand_randomness:
          type: string
          pattern: '^[0-9a-f]{64}$'
          description: The drand beacon's randomness for `drand_round`.
        rotation:
          type: object
          required: [rotation_seq, revealed_at, last_nonce]
          description: The seed rotation that revealed `server_seed` (solo games).
          properties:
            rotation_seq:
              type: integer
              minimum: 1
              description: The rotation's sequence number for this player's seed pairs.
            revealed_at:
              type: string
              format: date-time
              description: When the server seed was revealed.
            last_nonce:
              type: [string, 'null']
              pattern: '^(0|[1-9][0-9]{0,19})$'
              description: The last nonce used under the revealed pair; `null` when none was used.
        drand_round:
          type: integer
          description: drand beacon round mixed into the outcome.
        verify_url:
          type: string
          format: uri
          description: Public verifier for this round.

    RoundBet:
      type: object
      required: [user, transaction_uuid, bet, win, status, settle_reason]
      properties:
        user:
          type: string
          maxLength: 128
          description: Operator player identifier.
        transaction_uuid:
          allOf: [{$ref: '#/components/schemas/Uuid'}]
          description: '`transaction_uuid` of the `bet` webhook that placed this bet.'
        bet:
          allOf: [{$ref: '#/components/schemas/MinorUnits'}]
          description: Stake.
        win:
          allOf: [{$ref: '#/components/schemas/MinorUnits'}]
          description: Payout. `"0"` for a losing bet.
        multiplier:
          oneOf:
            - {$ref: '#/components/schemas/Multiplier'}
            - {type: 'null'}
          description: Multiplier cashed out at. `null` for a losing bet.
        status:
          type: string
          enum: [won, lost, refunded]
          description: |
            Outcome of the bet. Open: new values may be added; treat an unknown value as indeterminate: reconcile with `GET /v1/rounds/{round_id}`.
        settle_reason: {$ref: '#/components/schemas/SettleReason'}
        rank:
          oneOf:
            - {type: integer, minimum: 1}
            - {type: 'null'}
          description: |
            Position within the round by multiplier, `1` being highest. `null` for a bet that did
            not cash out.

    SettleReason:
      type: string
      enum: [cash_out, auto_cash_out, max_win, flight_cap, board_cleared, multiplier_cap, session_end, lost, refunded]
      description: |
        How the bet was settled. `cash_out`: manual cash-out; `auto_cash_out`: the player's auto
        cash-out target; `max_win`: forced settle at `M_cap` (see `GameCurrency.max_win`);
        `flight_cap`: open at the game's cap and settled there (crash); `board_cleared`: every safe
        tile was revealed (mines); `multiplier_cap`: settled at the last step at or below the
        game's cap, because the next step would pass it (a stepped game such as mines; see
        `GameCurrency.max_win`); `session_end`: the player's session ended, or the round's shown
        idle timer ran out, while the round was open, and the bet settled at its current
        multiplier, which can be below `1.00` (mines); `lost`: the round ended before any
        cash-out; `refunded`: the stake was returned by `rollback`. `won` pairs with `cash_out`,
        `auto_cash_out`, `max_win`, `flight_cap`, `board_cleared`, `multiplier_cap` or
        `session_end` (`session_end` even when `amount` is below the stake); `lost` with `lost`;
        `refunded` with `refunded`. In crash, when several triggers apply at one multiplier, the
        reason is the first of `auto_cash_out`, then `flight_cap` when `M_cap` is at or above
        `CAP`, else `max_win`. In a stepped game (mines), the reason is `board_cleared` when every
        safe tile was revealed; otherwise a bet stopped by a cap settles as `multiplier_cap` when
        `M_cap` is at or above the game's cap, a tie included, and as `max_win` when it is below. Open: new values may be added; treat an unknown value as indeterminate: reconcile with
        `GET /v1/rounds/{round_id}`.

    WinSettleReason:
      type: string
      enum: [cash_out, auto_cash_out, max_win, flight_cap, board_cleared, multiplier_cap, session_end, lost]
      description: |
        How the bet was settled; the values of `SettleReason` except `refunded`, which a `win` never
        carries. `lost` comes with `amount: "0"`. Open: new values may be added; treat an unknown value as `cash_out`: credit `amount` as sent.

    PlayerRequest:
      type: object
      required: [token, game_code]
      properties:
        token:
          type: string
          maxLength: 512
          description: Launch token from `POST /v1/sessions`, unchanged.
        game_code:
          type: string
          maxLength: 64
          description: Game being launched.
        ip:
          type: string
          maxLength: 45
          description: Player IP address as observed by Spartacus.

    PlayerResponse:
      type: object
      required: [user]
      properties:
        user:
          type: string
          maxLength: 128
          description: Operator player identifier. Must not contain personal data.
        country:
          type: string
          pattern: '^[A-Z]{2}$'
          description: ISO 3166-1 alpha-2 country code.
        lang:
          type: string
          maxLength: 35
          description: BCP 47 language tag. Overrides the value sent at session creation.
        nickname:
          type: string
          maxLength: 32
          description: Pseudonymous display name shown to other players.

    BalanceRequest:
      type: object
      required: [user, currencies]
      properties:
        user:
          type: string
          maxLength: 128
          description: Operator player identifier.
        currencies:
          type: array
          minItems: 1
          description: Currencies to return balances for.
          items: {$ref: '#/components/schemas/CurrencyCode'}
        game_code:
          type: string
          maxLength: 64
          description: Game in play.

    BalanceResponse:
      type: object
      required: [balances]
      properties:
        balances: {$ref: '#/components/schemas/Balances'}

    MoneyContext:
      type: object
      description: Fields common to all wallet webhooks that move funds.
      required: [user, currency, decimals, transaction_uuid, amount, created_at]
      properties:
        user:
          type: string
          maxLength: 128
          description: Operator player identifier.
        currency: {$ref: '#/components/schemas/CurrencyCode'}
        decimals: {$ref: '#/components/schemas/Decimals'}
        game_code:
          type: string
          maxLength: 64
          description: Game in play.
        round_id: {$ref: '#/components/schemas/Uuid'}
        transaction_uuid:
          allOf: [{$ref: '#/components/schemas/Uuid'}]
          description: Idempotency key for this transaction. Retain for at least 120 days.
        amount:
          allOf: [{$ref: '#/components/schemas/MinorUnits'}]
          description: Amount to move, in minor units.
        created_at:
          type: string
          format: date-time
          description: When Spartacus recorded the transaction. Identical across retries.

    BetRequest:
      allOf:
        - {$ref: '#/components/schemas/MoneyContext'}
        - type: object
          required: [round_id, game_code]

    WinRequest:
      allOf:
        - {$ref: '#/components/schemas/MoneyContext'}
        - type: object
          required: [round_id, game_code, bet_transaction_uuid, round_closed, settle_reason]
          properties:
            bet_transaction_uuid:
              allOf: [{$ref: '#/components/schemas/Uuid'}]
              description: '`transaction_uuid` of the `bet` being settled.'
            multiplier:
              allOf: [{$ref: '#/components/schemas/Multiplier'}]
              description: Multiplier cashed out at. Absent for a losing bet.
            crash_multiplier:
              allOf: [{$ref: '#/components/schemas/Multiplier'}]
              description: Multiplier at which the round ended.
            settle_reason: {$ref: '#/components/schemas/WinSettleReason'}
            round_closed:
              type: boolean
              description: Whether this is the final settlement for this player in this round.

    RollbackRequest:
      allOf:
        - {$ref: '#/components/schemas/MoneyContext'}
        - type: object
          required: [rollback_transaction_uuid, reason]
          properties:
            rollback_transaction_uuid:
              allOf: [{$ref: '#/components/schemas/Uuid'}]
              description: '`transaction_uuid` of the `bet` being reversed.'
            reason:
              type: string
              enum: [bet_timeout, bet_late, round_cancelled, player_left]
              description: |
                Why the bet produced no outcome. `player_left`: the player left before the round started; the stake is returned.
                Open: new values may be added; treat an unknown value as `bet_timeout`: reverse the stake as usual.

    WalletResponse:
      type: object
      required: [balances, transaction_id]
      properties:
        balances:
          allOf: [{$ref: '#/components/schemas/Balances'}]
          description: Balances after the transaction. Must include the currency that moved.
        transaction_id:
          type: string
          maxLength: 128
          description: Operator transaction reference, stored by Spartacus for reconciliation.
        processed_at:
          type: string
          format: date-time
          description: When the operator applied the transaction.

    RollbackResponse:
      allOf:
        - {$ref: '#/components/schemas/WalletResponse'}
        - type: object
          required: [status]
          properties:
            status:
              type: string
              enum: [rolled_back, not_found]
              description: |
                `rolled_back` if the target transaction was found and reversed. `not_found` if it
                was unknown and has been recorded for rejection if it arrives later — unless the
                target is a transaction the operator already holds, which is never marked. Open: new values may be added; treat an unknown value as
                indeterminate: reconcile before acting on it.
