openapi: 3.0.3
info:
  title: SoftSwiss Game Aggregator Wallet API
  description: |
    SoftSwiss-compatible Wallet API for Game Aggregator integration.

    Based on SoftSwiss Game Aggregator API v2 — Provider API.
    Only core Round and Player endpoints are included.

    ## Authentication

    All requests include an `X-REQUEST-SIGN` header containing the HMAC-SHA256
    signature of the raw request body, computed with the operator's AUTH_TOKEN.

    ## Idempotency

    Every transaction carries a unique `id_provider` (generated by the Provider).
    If the Game Aggregator receives a duplicate `id_provider`, it must return the
    original response without re-processing the transaction.
  version: v.2026.03.03

servers:
  - url: https://operator-wallet.example.com
    description: Operator (Game Aggregator) Wallet API endpoint

tags:
  - name: Round
    description: Game round operations (BetWin, Finish, Rollback)
  - name: Player
    description: Player operations (Balance)

paths:
  /betwin:
    post:
      operationId: BetWin
      tags: [Round]
      summary: BetWin
      description: |
        Debit/credit the player balance according to in-game activity.

        - Game Aggregator must process all transactions from the request atomically.
        - Transactions are processed in order.
        - Duplicate transaction IDs return the original response (idempotent).
        - Amounts equal to 0 are not accepted.
        - A transaction whose `id_provider` was already rolled back — including a
          rollback that arrived BEFORE it (out-of-order delivery; see the
          /rollback tombstone) — MUST be rejected, not applied: respond HTTP 400
          with `meta.api_code` = `409` ("action already rolled back") and leave
          the balance unchanged.
      parameters:
        - $ref: '#/components/parameters/RequestSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RoundBetWinRequest'
      responses:
        '200':
          description: Transactions processed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoundBetWinResponse'
        '400':
          description: >-
            Invalid request (insufficient funds, invalid argument, signature
            error, transaction already rolled back [api_code 409], etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /finish:
    post:
      operationId: Finish
      tags: [Round]
      summary: Finish
      description: |
        Finishes game round. Called only when the last BetWin did not set finished=true.
      parameters:
        - $ref: '#/components/parameters/RequestSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RoundFinishRequest'
      responses:
        '200':
          description: Round finish acknowledged
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoundFinishResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /rollback:
    post:
      operationId: Rollback
      tags: [Round]
      summary: Rollback
      description: |
        Revert changes activated by Provider due to error response or timeout.

        - Rollback requests are idempotent.
        - When the original transaction (`original_id_provider`) does not exist
          yet, a tombstone is created for it: a later /betwin transaction with
          that `id_provider` MUST then be rejected with `meta.api_code` = `409`.
          This handles out-of-order delivery — a rollback arriving before the
          bet/win it reverses — so the reversed transaction can never be applied.
        - Transactions are processed in the order they appear in the request.
      parameters:
        - $ref: '#/components/parameters/RequestSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RoundRollbackRequest'
      responses:
        '200':
          description: Rollback processed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoundRollbackResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /balance:
    post:
      operationId: Balance
      tags: [Player]
      summary: Balance
      description: Retrieve the player's balance for the specified currency.
      parameters:
        - $ref: '#/components/parameters/RequestSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlayerBalanceRequest'
      responses:
        '200':
          description: Balance retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlayerBalanceResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  parameters:
    RequestSignature:
      name: X-REQUEST-SIGN
      in: header
      required: true
      description: HMAC-SHA256 signature of request body (hex encoded)
      schema:
        type: string
        minLength: 1
        maxLength: 512

  schemas:
    # ==================== Error ====================

    ErrorResponseMeta:
      type: object
      required:
        - api_code
        - api_message
      properties:
        api_code:
          type: string
          description: >-
            API error code (see SoftSwiss API codes table). Beexar additionally
            defines `409` — "action already rolled back" — returned by /betwin to
            reject a transaction whose `id_provider` was (pre-)rolled back
            (tombstoned). See the /betwin and /rollback descriptions.
          example: "100"
        api_message:
          type: string
          description: API error message
          example: "Player has not enough funds to process an action."
        balance:
          type: string
          description: Player balance. Required when api_code is 100, 105, or 106.
          example: "0.01"

    ErrorResponse:
      type: object
      required:
        - code
        - meta
      properties:
        code:
          type: string
          description: |
            Twirp error code. SoftSwiss uses only two codes:
            "invalid_argument" (HTTP 400) and "internal" (HTTP 500).
          example: "invalid_argument"
        msg:
          type: string
          description: Human-readable error message
          example: "Player has not enough funds to process an action."
        meta:
          $ref: '#/components/schemas/ErrorResponseMeta'

    # ==================== Balance ====================

    PlayerBalanceRequest:
      type: object
      required:
        - account_id
        - currency
        - game_id
      properties:
        account_id:
          type: string
          minLength: 1
          description: Player account identifier on Game Aggregator's side
          example: "100"
        currency:
          type: string
          minLength: 3
          maxLength: 32
          pattern: '^[A-Z][A-Z0-9_]{2,31}$'
          description: Player account's currency
          example: "EUR"
        game_id:
          type: string
          minLength: 1
          description: Game identifier
          example: "PlatinumLightning"
        session_id:
          type: string
          description: Session identifier in UUID format
          example: "7b115688-3849-490a-95bd-7c281d81c64c"

    PlayerBalanceResponse:
      type: object
      required:
        - balance
      properties:
        balance:
          type: string
          description: Player account's balance
          example: "0.01"

    # ==================== BetWin ====================

    RoundBetWinRequest:
      type: object
      required:
        - account_id
        - currency
        - game_id
        - round_id
        - transactions
      properties:
        account_id:
          type: string
          minLength: 1
          description: Player account identifier on Game Aggregator's side
          example: "100"
        currency:
          type: string
          minLength: 3
          maxLength: 32
          pattern: '^[A-Z][A-Z0-9_]{2,31}$'
          description: Player account's currency
          example: "EUR"
        game_id:
          type: string
          minLength: 1
          description: Game identifier
          example: "PlatinumLightning"
        round_id:
          type: string
          minLength: 1
          description: Game round identifier on Provider's side
          example: "7b115688-3849-490a-95bd-7c281d81c64c"
        finished:
          type: boolean
          description: Whether the game round is finished
          example: true
        transactions:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/RoundBetWinRequestTransaction'
          description: A list of bet and win transactions
        session_id:
          type: string
          description: Session identifier in UUID format
          example: "7b115688-3849-490a-95bd-7c281d81c64c"

    RoundBetWinRequestTransaction:
      type: object
      required:
        - id_provider
        - type
        - amount
      properties:
        id_provider:
          type: string
          minLength: 1
          description: Unique transaction identifier on Provider's side
          example: "f0396ae4-3783-48d3-8e7d-20d90ba72d14"
        type:
          type: string
          enum: [bet, win]
          description: Transaction type
          example: "bet"
        amount:
          type: string
          description: Transaction amount (decimal string, must be > 0)
          example: "0.01"

    RoundBetWinResponse:
      type: object
      required:
        - round_id
        - transactions
        - balance
      properties:
        round_id:
          type: string
          minLength: 1
          description: Game round identifier on Game Aggregator's side
          example: "697962ac-fd0d-4dc2-b52c-e850e40825dc"
        transactions:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/RoundBetWinResponseTransaction'
          description: Results for each transaction (same order as request)
        balance:
          type: string
          description: Player account's balance after all transactions
          example: "0.01"

    RoundBetWinResponseTransaction:
      type: object
      required:
        - id_provider
        - id
        - bonus_amount
      properties:
        id_provider:
          type: string
          minLength: 1
          description: Transaction identifier from request
          example: "f0396ae4-3783-48d3-8e7d-20d90ba72d14"
        id:
          type: string
          minLength: 1
          description: Transaction identifier on Game Aggregator's side
          example: "7c096779-5a5c-4605-a976-3e19e6ee7fce"
        bonus_amount:
          type: string
          pattern: '^\d{1,18}(\.\d{1,12})?$'
          description: >-
            REQUIRED by SoftSwiss in every transaction: the player's bonus
            balance updated by this transaction (credited for win / debited for
            bet). Return "0.00" when no bonus balance is affected. Any value
            >= 0 and <= the transaction amount.
          example: "0.00"

    # ==================== Rollback ====================

    RoundRollbackRequest:
      type: object
      required:
        - account_id
        - currency
        - game_id
        - round_id_provider
        - finished
        - transactions
      properties:
        account_id:
          type: string
          minLength: 1
          description: Player account identifier on Game Aggregator's side
          example: "100"
        currency:
          type: string
          minLength: 3
          maxLength: 32
          pattern: '^[A-Z][A-Z0-9_]{2,31}$'
          description: Player account's currency
          example: "EUR"
        game_id:
          type: string
          minLength: 1
          description: Game identifier
          example: "PlatinumLightning"
        round_id_provider:
          type: string
          minLength: 1
          description: Game round identifier on Provider's side
          example: "bd3a5b23-37df-4344-bacb-de4b208e69c0"
        finished:
          type: boolean
          description: Whether the game round is finished
          example: true
        transactions:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/RoundRollbackRequestTransaction'
          description: A list of rollback transactions
        session_id:
          type: string
          description: Session identifier in UUID format
          example: "7b115688-3849-490a-95bd-7c281d81c64c"

    RoundRollbackRequestTransaction:
      type: object
      required:
        - id_provider
        - type
        - original_id_provider
      properties:
        id_provider:
          type: string
          minLength: 1
          description: Unique rollback transaction identifier on Provider's side
          example: "94e26949-a466-4773-9b51-2b2c8ac1a3d4"
        type:
          type: string
          enum: [rollback]
          description: Transaction type (always "rollback")
          example: "rollback"
        original_id_provider:
          type: string
          minLength: 1
          description: Transaction identifier on Provider's side to rollback
          example: "186d1e59-bc3d-4602-81ee-b96ace3f35bf"

    RoundRollbackResponse:
      type: object
      required:
        - balance
        - round_id
        - transactions
      properties:
        balance:
          type: string
          description: Player account's balance after rollback
          example: "0.01"
        round_id:
          type: string
          minLength: 1
          description: Game round identifier on Game Aggregator's side
          example: "5789d44e-76ca-4458-86e4-6861c73bff2a"
        transactions:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/RoundRollbackResponseTransaction'
          description: Results for each rollback transaction

    RoundRollbackResponseTransaction:
      type: object
      required:
        - id_provider
        - id
      properties:
        id_provider:
          type: string
          minLength: 1
          description: Rollback transaction identifier from request
          example: "94e26949-a466-4773-9b51-2b2c8ac1a3d4"
        id:
          type: string
          description: Transaction identifier on Game Aggregator's side (may be empty if original was not processed)
          example: "76033e09-6207-44a1-b40a-840107104a15"

    # ==================== Finish ====================

    RoundFinishRequest:
      type: object
      required:
        - account_id
        - currency
        - round_id
      properties:
        account_id:
          type: string
          minLength: 1
          description: Player account identifier on Game Aggregator's side
          example: "100"
        currency:
          type: string
          minLength: 3
          maxLength: 32
          pattern: '^[A-Z][A-Z0-9_]{2,31}$'
          description: Player account's currency
          example: "EUR"
        round_id:
          type: string
          minLength: 1
          description: Game round identifier on Provider's side
          example: "f81cc256-c714-4ecc-ba46-8db0337e6626"
        session_id:
          type: string
          description: Session identifier in UUID format
          example: "7b115688-3849-490a-95bd-7c281d81c64c"

    RoundFinishResponse:
      type: object
      required:
        - balance
      properties:
        balance:
          type: string
          description: Player account's balance
          example: "0.01"
