openapi: 3.0.3

info:
  title: AltCoinTrader API
  version: 1.0.0
  description: |
    ## Overview

    Welcome to the AltCoinTrader API.

    The AltCoinTrader API allows developers to access market data, manage orders,
    view account information, retrieve wallet balances, generate deposit addresses,
    connect to real-time WebSocket streams, and manage savings accounts.

    The REST API is designed for request/response workflows such as placing orders,
    cancelling orders, checking balances, and retrieving order history.

    The WebSocket API is designed for real-time updates such as ticker changes,
    order book updates, public trades, private order updates, and balance changes.

    ## Getting started

    1. Create an API key from your AltCoinTrader account.
    2. Store the API key and API secret securely.
    3. Use public endpoints for market data without authentication.
    4. Sign private requests with your API key, timestamp, and HMAC signature.
    5. Use WebSocket streams for real-time ticker, order book, trades, order, and balance updates.

    ## Base URL

    Production REST requests are sent to:

    ```text
    https://api.altcointrader.co.za
    ```

    WebSocket connections use the same API host with the `/ws` or `/ws/private` paths.

    ## Authentication

    Public market-data endpoints do not require authentication.

    Private endpoints require the following headers on every request:

    | Header | Description |
    |---|---|
    | `X-API-KEY` | Your API key. |
    | `X-TIMESTAMP` | Current Unix time in seconds. Must be within ±30 seconds of server time. |
    | `X-SIGNATURE` | HMAC-SHA256 signature generated with your API secret. |

    ### Signing format

    Build the signature payload as:

    ```text
    timestamp + "\n" + METHOD + "\n" + path + "\n" + body
    ```

    Then sign it using:

    ```text
    HMAC-SHA256(secret, payload)
    ```

    Encode the final signature as lowercase hex.

    For `GET` and `DELETE` requests, use an empty string for the body.

    Example payload for `GET /balances`:

    ```text
    1710000000
    GET
    /balances

    ```

    Example payload for `POST /orders`:

    ```text
    1710000000
    POST
    /orders
    {"market":"BTCZAR","side":"buy","price":"1020000","quantity":"0.01"}
    ```

    ## API key permissions

    Each API key is created with a set of permissions that control which endpoints it
    may call. Calling an endpoint without the required permission returns `403 FORBIDDEN`.

    | Permission | Endpoints |
    |---|---|
    | `read` | Account details, balances |
    | `trade` | Place, view, and cancel orders; easy buy/sell |
    | `deposit` | Deposit addresses, deposit and withdrawal history, address book, fiat accounts |
    | `withdraw` | Create withdrawals (`POST /wallet/withdraw`) |
    | `savings` | All savings endpoints |

    ### Withdrawal address whitelist

    When creating an API key you may specify a list of allowed withdrawal destinations.
    If a whitelist is configured, any withdrawal submitted with that key must target one
    of the whitelisted addresses (crypto) or recipient IDs (fiat). Attempts to withdraw
    to an unlisted destination return `403 FORBIDDEN`.

    If no whitelist is configured the key may withdraw to any destination, subject to
    normal account limits and verification requirements.

    ## Rate limits

    API requests are rate limited to protect platform stability.

    Limits may vary by endpoint, account type, and API key permissions. If a request
    exceeds the allowed rate, the API returns `429 TOO_MANY_REQUESTS`.

    Clients should handle `429` responses with exponential backoff and should avoid
    retrying failed requests in a tight loop.

    ## Pagination

    List endpoints use one of two pagination styles.

    **Cursor-based** (wallet, address book, fiat accounts):

    | Parameter | Description |
    |---|---|
    | `limit` | Number of records to return (default `50`, max `200`). |
    | `cursor` | Opaque cursor returned in the previous response `meta.next_cursor`. |

    **Page-token-based** (savings history):

    | Parameter | Description |
    |---|---|
    | `limit` | Number of records to return (default `50`, max `200`). |
    | `page_token` | Token returned in the previous response `next_page_token`. |

    **Page-based** (orders, trades):

    | Parameter | Description |
    |---|---|
    | `limit` | Number of records to return. |
    | `page` | Page number to return. |

    Unless otherwise documented, `limit` defaults to `50`.

    ## Error format

    Error responses use a consistent JSON structure:

    ```json
    {
      "code": "ERROR_CODE",
      "message": "Human readable message"
    }
    ```

    Common authentication error codes:

    | Code | Meaning |
    |---|---|
    | `UNAUTHORIZED` | Missing API key or timestamp header. |
    | `API_KEYS.INVALID_KEY` | API key was not recognised. |
    | `API_KEYS.INVALID_SIGNATURE` | Signature does not match. |
    | `API_KEYS.TIMESTAMP_SKEW` | Timestamp is outside the ±30 second window. |
    | `INVALID_SIGNATURE` | Signature header is missing or not hex-encoded. |

servers:
  - url: https://api.altcointrader.co.za
    description: Production API

tags:
  - name: Market Data
    description: Public endpoints for markets, currencies, ticker data, order books, and trades.
  - name: Account
    description: Private endpoints for account information and wallet balances.
  - name: Trading
    description: Private endpoints for placing, viewing, and cancelling orders.
  - name: Wallet
    description: Private endpoints for deposit addresses, withdrawals, deposit/withdrawal history, address book, and fiat bank accounts.
  - name: Savings
    description: Private endpoints for savings account balances, contributions, and interest claims.
  - name: WebSocket
    description: Real-time public and private streaming APIs.

security:
  - ApiKeyAuth: []
    TimestampAuth: []
    SignatureAuth: []

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY

    TimestampAuth:
      type: apiKey
      in: header
      name: X-TIMESTAMP
      description: Unix timestamp in seconds. Must be within ±30 seconds of server time.

    SignatureAuth:
      type: apiKey
      in: header
      name: X-SIGNATURE
      description: HMAC-SHA256 signature, hex-encoded.

  parameters:
    MarketPath:
      name: market
      in: path
      required: true
      description: Market symbol.
      schema:
        type: string
      example: BTCZAR

    OrderIdPath:
      name: order_id
      in: path
      required: true
      description: Order identifier.
      schema:
        type: string
      example: order-001

    CurrencyPath:
      name: currency
      in: path
      required: true
      description: Currency code.
      schema:
        type: string
      example: BTC

    AddressBookIdPath:
      name: id
      in: path
      required: true
      description: Crypto withdrawal address identifier.
      schema:
        type: string
      example: addr-001

    FiatAccountIdPath:
      name: id
      in: path
      required: true
      description: Bank account identifier.
      schema:
        type: string
      example: bank-001

    NetworkQuery:
      name: network
      in: query
      required: true
      description: Network code, for example `BTC`, `ETH`, `SOL`, `TRX`, or `XRP`.
      schema:
        type: string
      example: BTC

    MarketQuery:
      name: market
      in: query
      required: false
      description: Filter by market symbol.
      schema:
        type: string
      example: BTCZAR

    CurrencyQuery:
      name: currency
      in: query
      required: false
      description: Filter by currency code.
      schema:
        type: string
      example: BTC

    LimitQuery:
      name: limit
      in: query
      required: false
      description: Number of records to return.
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50

    TradesLimitQuery:
      name: limit
      in: query
      required: false
      description: Number of trades to return.
      schema:
        type: integer
        minimum: 1
        maximum: 500
        default: 50

    OrderBookLimitQuery:
      name: limit
      in: query
      required: false
      description: Number of price levels to return per side.
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50

    PageQuery:
      name: page
      in: query
      required: false
      description: Page number to return.
      schema:
        type: integer
        minimum: 1
        default: 1

    CursorQuery:
      name: cursor
      in: query
      required: false
      description: Opaque pagination cursor returned in a previous response `meta.next_cursor`.
      schema:
        type: string

    PageTokenQuery:
      name: page_token
      in: query
      required: false
      description: Page token returned in a previous response `next_page_token`.
      schema:
        type: string

    StartTimeQuery:
      name: start_time
      in: query
      required: false
      description: Unix timestamp lower bound.
      schema:
        type: integer
        format: int64
      example: 1710000000

    EndTimeQuery:
      name: end_time
      in: query
      required: false
      description: Unix timestamp upper bound.
      schema:
        type: integer
        format: int64
      example: 1710003600

    OrderStatusQuery:
      name: status
      in: query
      required: false
      description: Filter by order status.
      schema:
        type: string
        enum:
          - open
          - partially_filled
          - filled
          - cancelled
          - rejected

  responses:
    BadRequest:
      description: Invalid request parameters.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    Unauthorized:
      description: Missing or invalid authentication headers.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    Forbidden:
      description: API key does not have permission for this action.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    NotFound:
      description: Requested resource was not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    Conflict:
      description: Request conflicts with an existing resource.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    TooManyRequests:
      description: Rate limit exceeded.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    ServiceUnavailable:
      description: Service temporarily unavailable.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    NotImplemented:
      description: Endpoint is not yet implemented.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    UnprocessableEntity:
      description: Request was valid but could not be processed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: INVALID_PARAMETER
        message:
          type: string
          example: market is required

    CursorMeta:
      type: object
      properties:
        next_cursor:
          type: string
          nullable: true
          description: Cursor to pass as `cursor` in the next request. Null when there are no more pages.
          example: "eyJpZCI6IjEyMyJ9"
        has_more:
          type: boolean
          description: Whether more records exist beyond this page.
          example: true
        total:
          type: integer
          format: int64
          description: Total number of records matching the query.
          example: 142

    Market:
      type: object
      properties:
        symbol:
          type: string
          example: BTCZAR
        base:
          type: string
          example: BTC
        quote:
          type: string
          example: ZAR
        status:
          type: string
          enum:
            - active
            - inactive
          example: active
        min_order_value:
          type: string
          example: "0"
        price_precision:
          type: integer
          example: 2
        quantity_precision:
          type: integer
          example: 8

    NetworkFeeConfig:
      type: object
      description: Fee structure for a deposit or withdrawal on a specific network.
      properties:
        flat:
          type: string
          nullable: true
          description: Fixed fee amount as a decimal string. Null when not applicable.
          example: "0.0001"
        percentage:
          type: string
          nullable: true
          description: Percentage fee (e.g. `"0.5"` means 0.5%). Null when not applicable.
          example: null
        min_fee:
          type: string
          nullable: true
          description: Minimum fee amount.
          example: "0.0001"
        max_fee:
          type: string
          nullable: true
          description: Maximum fee amount. Null means no cap.
          example: null
        fee_currency:
          type: string
          nullable: true
          description: Currency the fee is denominated in. Null when same as the transfer currency.
          example: null

    NetworkSummary:
      type: object
      description: A network on which a currency can be deposited or withdrawn.
      properties:
        code:
          type: string
          description: Network code used in deposit address and withdrawal requests.
          example: BTC
        name:
          type: string
          example: Bitcoin
        is_active:
          type: boolean
          example: true
        deposit_enabled:
          type: boolean
          example: true
        withdrawal_enabled:
          type: boolean
          example: true
        memo_required:
          type: boolean
          description: Whether a memo / destination tag must be provided for deposits.
          example: false
        min_amount:
          type: string
          nullable: true
          description: Minimum transfer amount as a decimal string.
          example: "0.0001"
        max_amount:
          type: string
          nullable: true
          description: Maximum transfer amount as a decimal string. Null means no limit.
          example: null
        confirmations:
          type: integer
          description: Number of on-chain confirmations required before a deposit is credited.
          example: 3
        est_confirmation_time:
          type: string
          nullable: true
          description: Human-readable estimated confirmation time.
          example: "~30 minutes"
        address_regex:
          type: string
          nullable: true
          description: Regular expression for validating destination addresses on this network.
          example: "^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$|^bc1[a-z0-9]{6,87}$"
        deposit_fee:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/NetworkFeeConfig"
        withdrawal_fee:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/NetworkFeeConfig"
        icon_uri:
          type: string
          nullable: true
          example: null

    Currency:
      type: object
      required:
        - code
        - name
        - capabilities
        - networks
      properties:
        code:
          type: string
          description: Ticker symbol.
          example: BTC
        name:
          type: string
          description: Human-readable currency name.
          example: Bitcoin
        capabilities:
          type: object
          required:
            - trade
            - deposit
            - withdraw
            - easy_buy
            - easy_sell
            - easy_save
          properties:
            trade:
              type: boolean
              example: true
            deposit:
              type: boolean
              example: true
            withdraw:
              type: boolean
              example: true
            easy_buy:
              type: boolean
              example: true
            easy_sell:
              type: boolean
              example: true
            easy_save:
              type: boolean
              example: true
        easy_buy_sell_quote_currency:
          type: string
          description: >
            Quote currency used for easy buy/sell. Present only when
            `easy_buy` or `easy_sell` is true. Omitted otherwise.
          enum:
            - ZAR
            - USDT
          example: ZAR
        networks:
          type: array
          description: Networks on which this currency can be deposited or withdrawn.
          items:
            $ref: "#/components/schemas/NetworkSummary"

    Ticker:
      type: object
      properties:
        symbol:
          type: string
          example: BTCZAR
        last:
          type: string
          example: "1020000.00"
        open:
          type: string
          example: "1000000.00"
        high:
          type: string
          example: "1050000.00"
        low:
          type: string
          example: "990000.00"
        change:
          type: string
          example: "20000.00"
        change_pct:
          type: string
          example: "2.00"
        volume:
          type: string
          example: "3.50000000"
        quote_volume:
          type: string
          example: "0"
        timestamp:
          type: integer
          format: int64
          description: Unix timestamp of last trade.
          example: 1710000000

    OrderBookLevel:
      type: array
      description: Price and quantity pair.
      items:
        type: string
      minItems: 2
      maxItems: 2
      example:
        - "1020000.00"
        - "0.50000000"

    OrderBook:
      type: object
      properties:
        symbol:
          type: string
          example: BTCZAR
        bids:
          type: array
          description: Price levels, best bid first.
          items:
            $ref: "#/components/schemas/OrderBookLevel"
        asks:
          type: array
          description: Price levels, best ask first.
          items:
            $ref: "#/components/schemas/OrderBookLevel"
        timestamp:
          type: integer
          format: int64
          example: 1710000000

    Trade:
      type: object
      properties:
        trade_id:
          type: string
          example: abc123
        market:
          type: string
          example: BTCZAR
        price:
          type: string
          example: "1020000.00"
        quantity:
          type: string
          example: "0.10000000"
        side:
          type: string
          enum:
            - buy
            - sell
          example: buy
        timestamp:
          type: integer
          format: int64
          example: 1710000000

    Account:
      type: object
      properties:
        email:
          type: string
          format: email
          example: trader@example.com
        tier:
          type: string
          example: verified
        kyc_level:
          type: integer
          example: 2
        maker_fee:
          type: string
          example: "0.001"
        taker_fee:
          type: string
          example: "0.002"
        two_fa_enabled:
          type: boolean
          example: true
        withdrawal_limit_24h:
          type: string
          example: "0"

    Balance:
      type: object
      properties:
        currency:
          type: string
          example: BTC
        available:
          type: string
          example: "0.45000000"
        reserved:
          type: string
          example: "0.12000000"
        total:
          type: string
          example: "0.57000000"

    Order:
      type: object
      properties:
        order_id:
          type: string
          example: order-001
        client_order_id:
          type: string
          example: my-order-1
        market:
          type: string
          example: BTCZAR
        side:
          type: string
          enum:
            - buy
            - sell
          example: buy
        type:
          type: string
          enum:
            - limit
            - market
            - stop_limit
            - stop_market
          example: limit
        price:
          type: string
          nullable: true
          description: Null for market orders.
          example: "1020000"
        quantity:
          type: string
          example: "10000000"
        filled:
          type: string
          example: "0"
        remaining:
          type: string
          example: "10000000"
        status:
          type: string
          enum:
            - open
            - partially_filled
            - filled
            - cancelled
            - rejected
          example: open
        time_in_force:
          type: string
          enum:
            - GTC
            - IOC
            - FOK
          example: GTC
        created_at:
          type: integer
          format: int64
          example: 1710000000
        updated_at:
          type: integer
          format: int64
          example: 1710000001
        cancelled_at:
          type: integer
          format: int64
          nullable: true
          description: Present when the order has been cancelled.
          example: 1710000100

    PlaceLimitOrderRequest:
      type: object
      required:
        - market
        - side
        - price
        - quantity
      properties:
        market:
          type: string
          example: BTCZAR
        side:
          type: string
          enum:
            - buy
            - sell
          example: buy
        price:
          type: string
          description: Limit price as a decimal string.
          example: "1020000"
        quantity:
          type: string
          description: Order quantity as a decimal string.
          example: "0.01"
        time_in_force:
          type: string
          enum:
            - GTC
            - IOC
            - FOK
          default: GTC
        client_order_id:
          type: string
          description: Optional idempotency key. Auto-generated if omitted.
          example: my-order-1

    PlaceMarketOrderRequest:
      type: object
      required:
        - market
        - side
      properties:
        market:
          type: string
          example: BTCZAR
        side:
          type: string
          enum:
            - buy
            - sell
          example: buy
        quantity:
          type: string
          description: >
            Sell orders only. Base asset quantity to sell.
            Must not be sent on a buy order.
          example: "0.01"
        amount:
          type: string
          description: >
            Buy orders only. Maximum quote asset to spend. The base quantity is
            derived automatically from the live orderbook. Must not be sent on a
            sell order. Minimum 5 quote units.
          example: "10000"
        client_order_id:
          type: string
          description: Optional idempotency key.
          example: my-order-2

    DepositAddress:
      type: object
      required:
        - currency
        - network
        - address
        - memo
        - created_at
      properties:
        currency:
          type: string
          example: BTC
        network:
          type: string
          example: BTC
        address:
          type: string
          example: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
        memo:
          type: string
          nullable: true
          description: Memo or destination tag. Null when not applicable.
          example: null
        created_at:
          type: integer
          format: int64
          description: Unix timestamp when the address was created.
          example: 1712000000

    WithdrawRequest:
      type: object
      required:
        - currency
        - amount
        - channel_type
      description: |
        Submit a withdrawal request.

        Set `channel_type` to `"crypto"` for on-chain withdrawals or `"fiat"` for bank transfers.

        **Crypto** - provide `address` or `address_book_id` (one required).

        **Fiat** - provide `recipient_id` (a saved bank account ID) or inline bank details
        (`account_number` required). Set `instant_pay: true` to route via Instant EFT.
      properties:
        currency:
          type: string
          description: Currency code.
          example: BTC
        amount:
          type: string
          description: Withdrawal amount as a decimal string.
          example: "0.01"
        channel_type:
          type: string
          enum:
            - crypto
            - fiat
          description: "`crypto` for on-chain; `fiat` for bank transfer."
          example: crypto
        idempotency_key:
          type: string
          description: Optional idempotency key. Duplicate requests with the same key return the original result.
          example: "550e8400-e29b-41d4-a716-446655440000"
        address:
          type: string
          description: Crypto only. Destination address. Required when `address_book_id` is not provided.
          example: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
        address_book_id:
          type: string
          description: Crypto only. Saved address ID. Can be used instead of `address`.
          example: "addr-001"
        network:
          type: string
          description: Crypto only. Network code.
          example: BTC
        address_tag:
          type: string
          description: >
            Crypto only. Destination tag or memo for memo-based networks (e.g. XRP destination
            tag, XLM memo, EOS memo). Use this field in preference to `memo`.
          example: "12345"
        memo:
          type: string
          description: >
            Crypto only. Alias for `address_tag` - accepted for backward compatibility.
            If both are provided, `address_tag` takes precedence.
          example: "12345"
        recipient_id:
          type: string
          description: Fiat only. Saved bank account ID. Can be used instead of inline bank details.
          example: "bank-001"
        instant_pay:
          type: boolean
          description: Fiat only. Routes the withdrawal via Instant EFT.
          default: false
          example: false
        account_holder_name:
          type: string
          description: Fiat only. Account holder full name.
          example: John Smith
        bank_name:
          type: string
          description: >
            Fiat inline only. Required when `recipient_id` is not provided.
            Must be a full bank name from `GET /wallet/banks`
            (e.g. `"FNB South Africa"`).
          example: "FNB South Africa"
        bank_code:
          type: string
          description: Fiat only. Bank clearing code (optional).
          example: "250655"
        branch_code:
          type: string
          description: Fiat only.
          example: "250655"
        account_number:
          type: string
          description: Fiat only. Required when `recipient_id` is not provided.
          example: "62000000000"
        account_type:
          type: string
          enum:
            - Savings
            - Current
            - Cheque
          description: Fiat inline only. Bank account type.
          example: Cheque
        bank_reference:
          type: string
          description: >
            Fiat only. Reference that appears on the recipient's bank statement.
            Defaults to `"AltCoin Trader"` if not provided.
          default: AltCoin Trader
          example: INV-2024-001

    Withdrawal:
      type: object
      description: A created or historical withdrawal record.
      properties:
        id:
          type: string
          example: wd-001
        currency:
          type: string
          example: BTC
        amount:
          type: string
          example: "0.01"
        fee_amount:
          type: string
          example: "0.0001"
        amount_after_fees:
          type: string
          example: "0.0099"
        status:
          type: string
          description: Lowercase status value from the funding service.
          example: withdrawal_status_processing
        channel_type:
          type: string
          enum:
            - crypto
            - fiat
          example: crypto
        network_code:
          type: string
          nullable: true
          example: BTC
        address:
          type: string
          nullable: true
          description: Present for crypto withdrawals.
          example: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
        address_tag:
          type: string
          nullable: true
          description: Memo or destination tag. Present for crypto when applicable.
          example: null
        tx_hash:
          type: string
          nullable: true
          description: On-chain transaction hash. Present once broadcast.
          example: null
        requested_at:
          type: integer
          format: int64
          nullable: true
          example: 1712000000
        completed_at:
          type: integer
          format: int64
          nullable: true
          example: null
        created_at:
          type: integer
          format: int64
          example: 1712000000

    WithdrawalListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Withdrawal"
        meta:
          $ref: "#/components/schemas/CursorMeta"

    DepositRecord:
      type: object
      properties:
        id:
          type: string
          example: dp-001
        currency:
          type: string
          example: BTC
        amount:
          type: string
          example: "0.01"
        fee_amount:
          type: string
          example: "0.00000000"
        amount_after_fees:
          type: string
          example: "0.01"
        status:
          type: string
          description: Lowercase status value from the funding service.
          example: deposit_status_completed
        channel_type:
          type: string
          enum:
            - crypto
            - fiat
          example: crypto
        network_code:
          type: string
          nullable: true
          example: BTC
        address:
          type: string
          nullable: true
          description: Destination address for crypto deposits.
          example: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
        address_tag:
          type: string
          nullable: true
          example: null
        tx_hash:
          type: string
          nullable: true
          example: "a1b2c3d4e5f6..."
        detected_at:
          type: integer
          format: int64
          nullable: true
          example: 1712000000
        completed_at:
          type: integer
          format: int64
          nullable: true
          example: 1712000060
        created_at:
          type: integer
          format: int64
          example: 1712000000

    DepositListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/DepositRecord"
        meta:
          $ref: "#/components/schemas/CursorMeta"

    CryptoWithdrawalAddress:
      type: object
      description: A saved crypto withdrawal address from the address book.
      properties:
        id:
          type: string
          description: Unique address identifier. Pass as `address_book_id` when submitting a withdrawal.
          example: addr-001
        currency:
          type: string
          description: Asset ticker.
          example: BTC
        network:
          type: string
          example: BTC
        address:
          type: string
          example: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
        memo:
          type: string
          nullable: true
          description: Memo or destination tag. Null when not applicable.
          example: null
        label:
          type: string
          nullable: true
          description: Human-readable label for this address.
          example: My cold wallet
        status:
          type: string
          description: Address status.
          enum:
            - crypto_address_status_active
            - crypto_address_status_inactive
            - crypto_address_status_disabled
          example: crypto_address_status_active
        created_at:
          type: integer
          format: int64
          example: 1712000000
        updated_at:
          type: integer
          format: int64
          example: 1712000000

    AddressBookListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/CryptoWithdrawalAddress"
        meta:
          type: object
          properties:
            next_cursor:
              type: string
              nullable: true
              example: null
            total:
              type: integer
              format: int64
              example: 3

    BankAccount:
      type: object
      description: A saved bank account (fiat withdrawal destination).
      properties:
        id:
          type: string
          description: Unique bank account identifier. Pass as `address_book_id` when submitting a fiat withdrawal.
          example: bank-001
        currency:
          type: string
          example: ZAR
        label:
          type: string
          nullable: true
          description: Human-readable label for this account.
          example: My FNB account
        account_holder_name:
          type: string
          example: John Smith
        bank_name:
          type: string
          nullable: true
          example: FNB
        bank_code:
          type: string
          nullable: true
          example: "250655"
        branch_code:
          type: string
          nullable: true
          example: "250655"
        account_number:
          type: string
          nullable: true
          example: "62000000000"
        account_type:
          type: string
          nullable: true
          example: cheque
        iban:
          type: string
          nullable: true
          example: null
        swift_code:
          type: string
          nullable: true
          example: null
        routing_number:
          type: string
          nullable: true
          example: null
        bank_country_code:
          type: string
          nullable: true
          example: ZA
        status:
          type: string
          enum:
            - fiat_account_status_active
            - fiat_account_status_inactive
            - fiat_account_status_disabled
          example: fiat_account_status_active
        created_at:
          type: integer
          format: int64
          example: 1712000000
        updated_at:
          type: integer
          format: int64
          example: 1712000000

    FiatAccountListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/BankAccount"
        meta:
          type: object
          properties:
            next_cursor:
              type: string
              nullable: true
              example: null
            total:
              type: integer
              format: int64
              example: 2

    FiatBank:
      type: object
      description: An accepted fiat bank (source of valid `bank_name` values for withdrawals).
      properties:
        id:
          type: integer
          description: Internal bank identifier.
          example: 1
        name:
          type: string
          description: Full bank name. Submit this exact string as `bank_name` in a withdrawal.
          example: "FNB South Africa"
        abbreviation:
          type: string
          nullable: true
          description: Short abbreviation.
          example: "FNB"
        is_active:
          type: boolean
          example: true
        icon_uri:
          type: string
          nullable: true
          example: "https://cdn.example.com/banks/fnb.png"
        created_at:
          type: integer
          format: int64
          example: 1712000000

    FiatBankListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/FiatBank"

    SavingsBalance:
      type: object
      properties:
        savings_account_id:
          type: string
          example: sav-001
        currency:
          type: string
          example: USDT
        capital_contributions:
          type: string
          description: Total capital deposited, as a decimal string scaled to 8 decimal places.
          example: "1000.00000000"
        claimed_interest:
          type: string
          description: Total interest already claimed.
          example: "12.50000000"
        unclaimed_interest:
          type: string
          description: Interest accrued but not yet claimed.
          example: "3.20000000"
        calculated_at:
          type: integer
          format: int64
          nullable: true
          example: 1712000000

    SavingsSummaryResponse:
      type: object
      properties:
        wallet_id:
          type: string
          example: wallet-001
        balances:
          type: array
          items:
            $ref: "#/components/schemas/SavingsBalance"
        calculated_at:
          type: integer
          format: int64
          nullable: true
          example: 1712000000

    SavingsContribution:
      type: object
      properties:
        id:
          type: string
          example: contrib-001
        savings_account_id:
          type: string
          example: sav-001
        currency:
          type: string
          example: USDT
        amount:
          type: string
          description: Contributed amount as a decimal string scaled to 8 decimal places.
          example: "500.00000000"
        contributed_at:
          type: integer
          format: int64
          nullable: true
          example: 1712000000

    SavingsContributionListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/SavingsContribution"
        next_page_token:
          type: string
          nullable: true
          description: Token to pass as `page_token` for the next page. Null when no more pages.
          example: null

    SavingsInterestClaim:
      type: object
      properties:
        id:
          type: string
          example: claim-001
        savings_account_id:
          type: string
          example: sav-001
        currency:
          type: string
          example: USDT
        amount:
          type: string
          description: Interest claimed as a decimal string scaled to 8 decimal places.
          example: "3.20000000"
        accrual_start_at:
          type: integer
          format: int64
          nullable: true
          example: 1710000000
        accrual_end_at:
          type: integer
          format: int64
          nullable: true
          example: 1712000000
        claimed_at:
          type: integer
          format: int64
          nullable: true
          example: 1712000100

    SavingsInterestClaimListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/SavingsInterestClaim"
        next_page_token:
          type: string
          nullable: true
          description: Token to pass as `page_token` for the next page. Null when no more pages.
          example: null

    ContributeCapitalRequest:
      type: object
      required:
        - currency
        - amount
      description: |
        Deposit capital into the savings account for the given currency.

        The savings account is resolved automatically from the wallet ID associated
        with the API key - no account ID is required.
      properties:
        currency:
          type: string
          description: Currency to contribute. Must match an existing savings account on this wallet.
          example: USDT
        amount:
          type: string
          description: Amount to contribute as a decimal string. Up to 8 decimal places.
          example: "500.00"
        idempotency_key:
          type: string
          description: Optional idempotency key. Duplicate requests with the same key return the original result.
          example: "550e8400-e29b-41d4-a716-446655440000"
        memo:
          type: string
          description: Optional memo attached to this contribution.
          example: ""

    ContributeCapitalResponse:
      type: object
      properties:
        contribution:
          $ref: "#/components/schemas/SavingsContribution"
        balance:
          $ref: "#/components/schemas/SavingsBalance"

    WithdrawCapitalRequest:
      type: object
      required:
        - currency
        - amount
      description: |
        Withdraw (reclaim) capital from the savings account for the given currency.

        The savings account is resolved automatically from the wallet associated with
        the API key - no account ID is required. Capital is returned to the authenticated
        wallet. Any accrued interest is automatically claimed at the same time.
      properties:
        currency:
          type: string
          description: Currency of the savings account to withdraw from.
          example: USDT
        amount:
          type: string
          description: Amount to withdraw as a decimal string (up to 8 decimal places).
          example: "250.00"
        idempotency_key:
          type: string
          description: Optional idempotency key. Duplicate requests with the same key return the original result.
          example: "550e8400-e29b-41d4-a716-446655440002"
        memo:
          type: string
          description: Optional memo attached to this withdrawal.
          example: ""

    SavingsCapitalWithdrawal:
      type: object
      properties:
        id:
          type: string
          example: wd-001
        savings_account_id:
          type: string
          example: sav-001
        currency:
          type: string
          example: USDT
        amount:
          type: string
          description: Withdrawn amount as a decimal string scaled to 8 decimal places.
          example: "250.00000000"
        withdrawn_at:
          type: integer
          format: int64
          example: 1712000000

    WithdrawCapitalResponse:
      type: object
      properties:
        withdrawal:
          $ref: "#/components/schemas/SavingsCapitalWithdrawal"
        interest_claim:
          allOf:
            - $ref: "#/components/schemas/SavingsInterestClaim"
          nullable: true
          description: Auto-claimed interest at time of withdrawal, if any accrued.
        balance:
          $ref: "#/components/schemas/SavingsBalance"

    ClaimInterestRequest:
      type: object
      required:
        - currency
      description: |
        Claim all accrued interest for the given currency's savings account.

        The savings account is resolved automatically from the wallet ID associated
        with the API key - no account ID is required. Interest is credited to the
        authenticated wallet.
      properties:
        currency:
          type: string
          description: Currency whose accrued interest should be claimed.
          example: USDT
        idempotency_key:
          type: string
          description: Optional idempotency key. Duplicate requests with the same key return the original result.
          example: "550e8400-e29b-41d4-a716-446655440001"
        memo:
          type: string
          description: Optional memo attached to this claim.
          example: ""

    ClaimInterestResponse:
      type: object
      properties:
        claim:
          $ref: "#/components/schemas/SavingsInterestClaim"
        balance:
          $ref: "#/components/schemas/SavingsBalance"

    UserTrade:
      type: object
      properties:
        trade_id:
          type: string
          example: trd-001
        order_id:
          type: string
          example: order-001
        client_order_id:
          type: string
          description: Present only when the original order was submitted with a client order ID.
          example: 7ec370f1-c77d-4276-b492-0d8a4ecb8ffd
        market:
          type: string
          example: BTCZAR
        side:
          type: string
          enum:
            - buy
            - sell
          example: buy
        price:
          type: string
          example: "1020000.00"
        quantity:
          type: string
          example: "0.10000000"
        total_value:
          type: string
          example: "102000.00"
        fee:
          type: string
          example: "204.00"
        timestamp:
          type: integer
          format: int64
          example: 1710000000

    EasyBuyRequest:
      type: object
      required:
        - side
        - ticker
        - amount
      properties:
        side:
          type: string
          enum:
            - buy
            - sell
          example: buy
        ticker:
          type: string
          description: Coin symbol.
          example: BTC
        amount:
          type: string
          description: >
            For buy orders: the quote currency amount to spend.
            For sell orders: the coin quantity to sell.
          example: "1000.00"
        client_order_id:
          type: string
          description: Optional idempotency key.
          example: optional-uuid

    EasyBuyResult:
      type: object
      required:
        - ok
        - result
      properties:
        ok:
          type: boolean
          example: true
        result:
          type: object
          required:
            - status
            - filled_qty
            - filled_quote_qty
            - avg_price
            - fee_pct
            - fee_amount
            - net_amount
          properties:
            status:
              type: string
              example: FILLED
            filled_qty:
              type: string
              example: "0.00123456"
            filled_quote_qty:
              type: string
              example: "1000.00"
            avg_price:
              type: string
              example: "810000.00"
            fee_pct:
              type: string
              example: "0.001"
            fee_amount:
              type: string
              example: "1.00"
            net_amount:
              type: string
              example: "999.00"

paths:
  /markets:
    get:
      tags:
        - Market Data
      operationId: listMarkets
      summary: List trading markets
      description: Returns all trading markets available on AltCoinTrader.
      security: []
      responses:
        "200":
          description: Trading markets returned successfully.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Market"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /currencies:
    get:
      tags:
        - Market Data
      operationId: listCurrencies
      summary: List supported currencies
      description: Returns all currencies supported by the platform.
      security: []
      responses:
        "200":
          description: Supported currencies returned successfully.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Currency"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /ticker/{market}:
    get:
      tags:
        - Market Data
      operationId: getTicker
      summary: Get 24-hour ticker
      description: Returns 24-hour ticker statistics for a market.
      security: []
      parameters:
        - $ref: "#/components/parameters/MarketPath"
      responses:
        "200":
          description: Ticker returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ticker"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /orderbook/{market}:
    get:
      tags:
        - Market Data
      operationId: getOrderBook
      summary: Get order book
      description: Returns the current order book snapshot for a market.
      security: []
      parameters:
        - $ref: "#/components/parameters/MarketPath"
        - $ref: "#/components/parameters/OrderBookLimitQuery"
      responses:
        "200":
          description: Order book returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderBook"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /trades/{market}:
    get:
      tags:
        - Market Data
      operationId: listPublicTrades
      summary: List recent trades
      description: Returns recent public trades for a market.
      security: []
      parameters:
        - $ref: "#/components/parameters/MarketPath"
        - $ref: "#/components/parameters/TradesLimitQuery"
      responses:
        "200":
          description: Recent trades returned successfully.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Trade"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /account:
    get:
      tags:
        - Account
      operationId: getAccount
      summary: Get account details
      description: Returns details for the authenticated account.
      responses:
        "200":
          description: Account details returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Account"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /balances:
    get:
      tags:
        - Account
      operationId: listBalances
      summary: List account balances
      description: Returns per-currency available, reserved, and total balances.
      responses:
        "200":
          description: Balances returned successfully.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Balance"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /orders:
    post:
      tags:
        - Trading
      operationId: placeLimitOrder
      summary: Place limit order
      description: Creates a new limit order.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PlaceLimitOrderRequest"
      responses:
        "201":
          description: Order created successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /orders/market:
    post:
      tags:
        - Trading
      operationId: placeMarketOrder
      summary: Place market order
      description: |
        Creates a new market order.

        **Sell orders** - send `quantity` (base asset amount to sell). Do not send `amount`.

        **Buy orders** - send `amount` (maximum quote asset to spend). The base quantity is
        derived automatically from the live orderbook, using the same logic as the easy buy
        feature. Do not send `quantity`. Minimum spend is 5 quote units.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PlaceMarketOrderRequest"
      responses:
        "201":
          description: Order created successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /orders/open:
    get:
      tags:
        - Trading
      operationId: listOpenOrders
      summary: List open orders
      description: Returns open and partially filled orders for the authenticated account.
      parameters:
        - $ref: "#/components/parameters/MarketQuery"
      responses:
        "200":
          description: Open orders returned successfully.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Order"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /orders/history:
    get:
      tags:
        - Trading
      operationId: listOrderHistory
      summary: List order history
      description: Returns completed, cancelled, rejected, and historical orders.
      parameters:
        - $ref: "#/components/parameters/MarketQuery"
        - $ref: "#/components/parameters/OrderStatusQuery"
        - $ref: "#/components/parameters/LimitQuery"
        - $ref: "#/components/parameters/PageQuery"
        - $ref: "#/components/parameters/StartTimeQuery"
        - $ref: "#/components/parameters/EndTimeQuery"
      responses:
        "200":
          description: Order history returned successfully.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Order"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /trades:
    get:
      tags:
        - Trading
      operationId: listMyTrades
      summary: List trade history
      description: Returns the authenticated user's trade history.
      parameters:
        - $ref: "#/components/parameters/MarketQuery"
        - $ref: "#/components/parameters/LimitQuery"
        - $ref: "#/components/parameters/PageQuery"
        - $ref: "#/components/parameters/StartTimeQuery"
        - $ref: "#/components/parameters/EndTimeQuery"
      responses:
        "200":
          description: Trade history returned successfully.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/UserTrade"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /orders/{order_id}:
    get:
      tags:
        - Trading
      operationId: getOrder
      summary: Get order
      description: Returns a single order by ID.
      parameters:
        - $ref: "#/components/parameters/OrderIdPath"
      responses:
        "200":
          description: Order returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

    delete:
      tags:
        - Trading
      operationId: cancelOrder
      summary: Cancel order
      description: Cancels an open order.
      parameters:
        - $ref: "#/components/parameters/OrderIdPath"
      responses:
        "200":
          description: Order cancelled successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /orders/{order_id}/trades:
    get:
      tags:
        - Trading
      operationId: listOrderTrades
      summary: List trades for an order
      description: Returns all trades that were filled as part of a specific order.
      parameters:
        - $ref: "#/components/parameters/OrderIdPath"
      responses:
        "200":
          description: Order trades returned successfully.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/UserTrade"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /easybuy:
    post:
      tags:
        - Trading
      operationId: placeEasyBuy
      summary: Place easy buy/sell order
      description: |
        Executes an easy buy or sell against the live orderbook.

        Requires the **trade** permission on the API key.

        **Buy** - set `side` to `buy` and provide `amount` as the quote currency
        to spend (e.g. ZAR or USDT). The coin quantity is derived automatically.

        **Sell** - set `side` to `sell` and provide `amount` as the coin quantity
        to sell. The quote proceeds are calculated automatically.

        `client_order_id` acts as an idempotency key: submitting the same value
        twice returns the original result without placing a second order.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EasyBuyRequest"
      responses:
        "200":
          description: Easy buy/sell order filled successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EasyBuyResult"
        "400":
          description: Missing or invalid request fields.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                code: INVALID_PARAMETER
                message: amount is required
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: API key does not have the trade permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                code: FORBIDDEN
                message: API key does not have trade permission
        "422":
          description: Order rejected by the easy buy/sell service (e.g. insufficient balance).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                code: EASY_BUY_SELL_FAILED
                message: Insufficient balance
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          description: Easy buy/sell service not available.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                code: INTERNAL_ERROR
                message: Easy buy/sell service is temporarily unavailable

  /wallet/deposit/{currency}:
    get:
      tags:
        - Wallet
      operationId: getDepositAddress
      summary: Get deposit address
      description: Returns the deposit address for a currency and network.
      parameters:
        - $ref: "#/components/parameters/CurrencyPath"
        - $ref: "#/components/parameters/NetworkQuery"
      responses:
        "200":
          description: Deposit address returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DepositAddress"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /wallet/withdraw:
    post:
      tags:
        - Wallet
      operationId: submitWithdrawal
      summary: Submit withdrawal
      description: |
        Creates a withdrawal in **processing** state.

        Set `channel_type` to `"crypto"` for on-chain withdrawals or `"fiat"` for bank
        transfers. See the request body schema for full field details per channel type.

        Requires the **withdraw** permission on the API key.

        If the API key has a withdrawal address whitelist, the destination (`address`,
        `address_book_id`, or `recipient_id`) must be in the whitelist or the request
        is rejected with `403 FORBIDDEN`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WithdrawRequest"
            examples:
              crypto:
                summary: Crypto withdrawal (BTC)
                value:
                  currency: BTC
                  amount: "0.01"
                  channel_type: crypto
                  address: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
                  network: BTC
                  idempotency_key: "550e8400-e29b-41d4-a716-446655440000"
              crypto_memo:
                summary: Crypto withdrawal with memo (XRP)
                value:
                  currency: XRP
                  amount: "100"
                  channel_type: crypto
                  address: rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh
                  address_tag: "12345"
                  network: XRP
                  idempotency_key: "550e8400-e29b-41d4-a716-446655440000"
              fiat_bank_details:
                summary: Fiat EFT with inline bank details
                value:
                  currency: ZAR
                  amount: "1000.00"
                  channel_type: fiat
                  account_holder_name: John Smith
                  bank_name: "FNB South Africa"
                  branch_code: "250655"
                  account_number: "62000000000"
                  account_type: Cheque
                  bank_reference: INV-2024-001
                  idempotency_key: "550e8400-e29b-41d4-a716-446655440001"
              fiat_recipient:
                summary: Fiat EFT via saved recipient
                value:
                  currency: ZAR
                  amount: "1000.00"
                  channel_type: fiat
                  recipient_id: bank-001
                  bank_reference: INV-2024-001
                  idempotency_key: "550e8400-e29b-41d4-a716-446655440002"
              fiat_instant:
                summary: Fiat Instant EFT via saved recipient
                value:
                  currency: ZAR
                  amount: "1000.00"
                  channel_type: fiat
                  instant_pay: true
                  recipient_id: bank-001
                  bank_reference: INV-2024-001
                  idempotency_key: "550e8400-e29b-41d4-a716-446655440003"
      responses:
        "200":
          description: Withdrawal created successfully in processing state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Withdrawal"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /wallet/deposits:
    get:
      tags:
        - Wallet
      operationId: listDeposits
      summary: List deposits
      description: |
        Returns deposit history for the authenticated account using cursor-based pagination.

        Requires the **deposit** permission on the API key.
      parameters:
        - $ref: "#/components/parameters/CurrencyQuery"
        - $ref: "#/components/parameters/LimitQuery"
        - $ref: "#/components/parameters/CursorQuery"
      responses:
        "200":
          description: Deposit history returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DepositListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /wallet/withdrawals:
    get:
      tags:
        - Wallet
      operationId: listWithdrawals
      summary: List withdrawals
      description: |
        Returns withdrawal history for the authenticated account using cursor-based pagination.

        Requires the **deposit** permission on the API key.
      parameters:
        - $ref: "#/components/parameters/CurrencyQuery"
        - $ref: "#/components/parameters/LimitQuery"
        - $ref: "#/components/parameters/CursorQuery"
      responses:
        "200":
          description: Withdrawal history returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WithdrawalListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /wallet/address-book:
    get:
      tags:
        - Wallet
      operationId: listAddressBook
      summary: List crypto withdrawal addresses
      description: |
        Returns saved crypto withdrawal addresses for the authenticated account.

        The `id` of each entry can be passed as `address_book_id` when submitting
        a crypto withdrawal.

        Requires the **deposit** permission on the API key.
      parameters:
        - $ref: "#/components/parameters/CurrencyQuery"
        - $ref: "#/components/parameters/LimitQuery"
        - $ref: "#/components/parameters/CursorQuery"
      responses:
        "200":
          description: Address book returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AddressBookListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /wallet/address-book/{id}:
    get:
      tags:
        - Wallet
      operationId: getAddressBookEntry
      summary: Get crypto withdrawal address
      description: |
        Returns a single saved crypto withdrawal address by ID.

        Requires the **deposit** permission on the API key.
      parameters:
        - $ref: "#/components/parameters/AddressBookIdPath"
      responses:
        "200":
          description: Address book entry returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CryptoWithdrawalAddress"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /wallet/fiat-accounts:
    get:
      tags:
        - Wallet
      operationId: listFiatAccounts
      summary: List fiat bank accounts
      description: |
        Returns saved bank account destinations for the authenticated account.

        The `id` of each entry can be passed as `recipient_id` when submitting
        a fiat withdrawal.

        Requires the **deposit** permission on the API key.
      parameters:
        - $ref: "#/components/parameters/LimitQuery"
        - $ref: "#/components/parameters/CursorQuery"
      responses:
        "200":
          description: Fiat bank accounts returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FiatAccountListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /wallet/fiat-accounts/{id}:
    get:
      tags:
        - Wallet
      operationId: getFiatAccount
      summary: Get fiat bank account
      description: |
        Returns a single saved bank account by ID.

        Requires the **deposit** permission on the API key.
      parameters:
        - $ref: "#/components/parameters/FiatAccountIdPath"
      responses:
        "200":
          description: Bank account returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BankAccount"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /wallet/banks:
    get:
      tags:
        - Wallet
      operationId: listBanks
      summary: List accepted banks
      description: |
        Returns the list of accepted fiat bank names.

        Use the `name` field of each entry as the `bank_name` value when submitting
        an inline fiat withdrawal (without `recipient_id`).

        Requires the **deposit** permission on the API key.
      responses:
        "200":
          description: Bank list returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FiatBankListResponse"
              example:
                data:
                  - id: 1
                    name: "FNB South Africa"
                    abbreviation: "FNB"
                    is_active: true
                    icon_uri: null
                    created_at: 1712000000
                  - id: 2
                    name: "ABSA South Africa"
                    abbreviation: "ABSA"
                    is_active: true
                    icon_uri: null
                    created_at: 1712000000
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /savings/summary:
    get:
      tags:
        - Savings
      operationId: getSavingsSummary
      summary: Get savings summary
      description: |
        Returns balances for all savings accounts associated with the authenticated wallet.

        Each balance entry includes the savings account ID, currency, total capital
        contributed, claimed interest, and unclaimed interest.

        Requires the **savings** permission on the API key.
      responses:
        "200":
          description: Savings summary returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SavingsSummaryResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /savings/contributions:
    get:
      tags:
        - Savings
      operationId: listSavingsContributions
      summary: List savings contributions
      description: |
        Returns capital contribution history for the authenticated wallet using
        page-token-based pagination.

        Requires the **savings** permission on the API key.
      parameters:
        - $ref: "#/components/parameters/LimitQuery"
        - $ref: "#/components/parameters/PageTokenQuery"
      responses:
        "200":
          description: Savings contributions returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SavingsContributionListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /savings/interest:
    get:
      tags:
        - Savings
      operationId: listSavingsInterestClaims
      summary: List interest claims
      description: |
        Returns interest claim history for the authenticated wallet using
        page-token-based pagination.

        Requires the **savings** permission on the API key.
      parameters:
        - $ref: "#/components/parameters/LimitQuery"
        - $ref: "#/components/parameters/PageTokenQuery"
      responses:
        "200":
          description: Interest claims returned successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SavingsInterestClaimListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /savings/contribute:
    post:
      tags:
        - Savings
      operationId: contributeCapital
      summary: Contribute capital
      description: |
        Deposits capital into the savings account for the given currency.

        The savings account is resolved automatically from the wallet associated with
        the API key - no account ID is required in the request.

        Requires the **savings** permission on the API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContributeCapitalRequest"
            example:
              currency: USDT
              amount: "500.00"
              idempotency_key: "550e8400-e29b-41d4-a716-446655440000"
      responses:
        "200":
          description: Capital contributed successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContributeCapitalResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /savings/capital/withdraw:
    post:
      tags:
        - Savings
      operationId: withdrawCapital
      summary: Withdraw capital
      description: |
        Withdraws (reclaims) contributed capital from the savings account for the given
        currency. Capital is credited directly to the authenticated wallet.

        Any accrued interest is automatically claimed at the same time and returned
        in the response as `interest_claim` (omitted if no interest had accrued).

        The savings account is resolved automatically - no account ID is required.

        Requires the **savings** permission on the API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WithdrawCapitalRequest"
            example:
              currency: USDT
              amount: "250.00"
              idempotency_key: "550e8400-e29b-41d4-a716-446655440002"
      responses:
        "200":
          description: Capital withdrawn successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WithdrawCapitalResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /savings/interest/claim:
    post:
      tags:
        - Savings
      operationId: claimInterest
      summary: Claim interest
      description: |
        Claims all accrued interest for the given currency's savings account.

        The savings account is resolved automatically from the wallet associated with
        the API key - no account ID is required. Interest is credited directly to
        the authenticated wallet.

        Requires the **savings** permission on the API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ClaimInterestRequest"
            example:
              currency: USDT
              idempotency_key: "550e8400-e29b-41d4-a716-446655440001"
      responses:
        "200":
          description: Interest claimed successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClaimInterestResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /ws:
    get:
      tags:
        - WebSocket
      operationId: connectPublicWebSocket
      summary: Public WebSocket
      security: []
      description: |
        Upgrade to a WebSocket connection for real-time public market data.

        Public WebSocket streams do not require authentication.

        ## Subscribe

        ```json
        {"action":"subscribe","channel":"ticker","market":"BTCZAR"}
        {"action":"subscribe","channel":"orderbook","market":"BTCZAR","limit":10}
        {"action":"subscribe","channel":"trades","market":"BTCZAR"}
        ```

        ## Unsubscribe

        ```json
        {"action":"unsubscribe","channel":"ticker","market":"BTCZAR"}
        ```

        ## Client message fields

        | Field | Type | Description |
        |---|---|---|
        | `action` | string | `subscribe` or `unsubscribe`. |
        | `channel` | string | `ticker`, `orderbook`, or `trades`. |
        | `market` | string | Market symbol, for example `BTCZAR`. |
        | `limit` | integer | Order book depth. Applies to `orderbook` only. Default `50`, maximum `200`. |

        ## Server frames

        ```json
        {"channel":"subscribed","market":"BTCZAR","subscription":"ticker"}
        {"channel":"ticker","market":"BTCZAR","data":{}}
        {"channel":"orderbook","market":"BTCZAR","data":{}}
        {"channel":"trades","market":"BTCZAR","data":[]}
        {"channel":"error","message":"..."}
        ```

        ## Poll intervals

        | Channel | Interval |
        |---|---|
        | `ticker` | 1 second |
        | `orderbook` | 500 milliseconds |
        | `trades` | 1 second |

        Frames are only pushed when data changes. The server sends a WebSocket
        ping every 45 seconds. Clients must respond with a pong within 60 seconds
        or the connection is closed.
      responses:
        "101":
          description: Switching Protocols - WebSocket upgrade successful.
        "503":
          description: Market data service unavailable.

  /ws/private:
    get:
      tags:
        - WebSocket
      operationId: connectPrivateWebSocket
      summary: Private WebSocket
      description: |
        Upgrade to an authenticated WebSocket connection for private account streams.

        Send `X-API-KEY`, `X-TIMESTAMP`, and `X-SIGNATURE` headers on the HTTP
        upgrade request. Sign the upgrade request with an empty body string.

        ## Subscribe

        ```json
        {"action":"subscribe","channel":"orders"}
        {"action":"subscribe","channel":"fills"}
        {"action":"subscribe","channel":"balances"}
        ```

        ## Unsubscribe

        ```json
        {"action":"unsubscribe","channel":"orders"}
        {"action":"unsubscribe","channel":"fills"}
        {"action":"unsubscribe","channel":"balances"}
        ```

        Private channels are account-wide and do not use a `market` parameter.

        ## Server frames

        **orders**
        ```json
        {"channel":"subscribed","subscription":"orders"}
        {"channel":"orders","data":{}}
        {"channel":"error","message":"..."}
        ```

        **fills** - private trade fills for the authenticated account:
        ```json
        {"channel":"subscribed","subscription":"fills"}
        {
          "channel": "fills",
          "data": {
            "trade_id": "33b18c87-1088-4dc7-8bf2-42910a96be76",
            "order_id": "46402c10-64f3-5dcc-8702-9d21ed6aa7bc",
            "client_order_id": "7ec370f1-c77d-4276-b492-0d8a4ecb8ffd",
            "market": "BTCZAR",
            "side": "buy",
            "price": "1050000.00000000",
            "fill_delta": "0.02500000",
            "filled": "0.07500000",
            "remaining": "0.12500000",
            "quantity": "0.02500000",
            "total_value": "26250.00000000",
            "fee": "157.50000000",
            "timestamp": 1784289007
          }
        }
        ```

        The `fills` channel emits new private trade fills while you are subscribed.
        Use `fill_delta` for the amount filled by this execution, and `filled` / `remaining` for the updated order totals.

        | Field | Description |
        |---|---|
        | `trade_id` | Unique trade public UUID for this fill. |
        | `order_id` | UUID of the authenticated user's affected order. |
        | `client_order_id` | Client-supplied order ID, present only when the original order included one. |
        | `market` | Trading pair symbol. |
        | `side` | User-side of the fill, `buy` or `sell`. |
        | `price` | Execution price for this fill. |
        | `fill_delta` | Amount filled by this execution. |
        | `quantity` | Same amount as `fill_delta`, included for consistency with trade history. |
        | `filled` | Total order quantity filled after this execution. |
        | `remaining` | Total order quantity remaining after this execution. |
        | `total_value` | Quote value of this fill. |
        | `fee` | Fee charged for this fill. |
        | `timestamp` | Unix seconds when the fill executed. |

        **balances** - full snapshot pushed every second:
        ```json
        {"channel":"subscribed","subscription":"balances"}
        {
          "channel": "balances",
          "data": [
            {"currency":"ZAR","available":"125000.00000000","reserved":"5000.00000000","total":"130000.00000000"},
            {"currency":"BTC","available":"0.45000000","reserved":"0.05000000","total":"0.50000000"},
            {"currency":"ETH","available":"3.20000000","reserved":"0.00000000","total":"3.20000000"}
          ]
        }
        ```

        `available` is free to use; `reserved` is locked in open orders; `total` is the sum of both.
        The `balances` channel mirrors `GET /balances` exactly.

        ## Poll intervals

        | Channel | Interval | Notes |
        |---|---|---|
        | `orders` | 1 second | Pushed when order status or filled quantity changes. |
        | `fills` | 1 second | Pushed once for each new private trade fill. |
        | `balances` | 1 second | Full balance snapshot on every tick. |
      responses:
        "101":
          description: Switching Protocols - WebSocket upgrade successful.
        "401":
          description: Unauthorized - missing or invalid API key.
        "503":
          description: Trading service unavailable.
