asyncapi: 2.6.0
info:
  title: Spot WebSocket API
  version: 3.2.0
  description: |
    Spot WebSocket streams for real-time market data and private user events.

    **OSS Endpoint (Orderbook):** `wss://ws.btse.com/ws/oss/spot`
    **Main Endpoint:** `wss://ws.btse.com/ws/spot`
    **Testnet OSS:** `wss://testws.btse.io/ws/oss/spot`
    **Testnet Main:** `wss://testws.btse.io/ws/spot`

    **Authentication:** Private topics require sending an `authKeyExpires` message
    with HMAC-SHA384 signature: `HMAC-SHA384(secret, "/ws/spot" + nonce)`

servers:
  oss-production:
    url: wss://ws.btse.com/ws/oss/spot
    protocol: wss
    description: Production — Order Stream Service (orderbook data)
  oss-testnet:
    url: wss://testws.btse.io/ws/oss/spot
    protocol: wss
    description: Testnet — Order Stream Service
  main-production:
    url: wss://ws.btse.com/ws/spot
    protocol: wss
    description: Production — Main WebSocket (trades, auth, notifications)
  main-testnet:
    url: wss://testws.btse.io/ws/spot
    protocol: wss
    description: Testnet — Main WebSocket

channels:
  # ─── Heartbeat ───────────────────────────────────────────────────────────

  ping:
    description: |
      Send a plain text `ping` to keep the connection alive. The server responds with `pong`.
    servers: [main-production, main-testnet, oss-production, oss-testnet]
    publish:
      operationId: sendPing
      summary: Ping
      message:
        payload:
          type: string
          const: ping
    subscribe:
      operationId: receivePong
      summary: Pong
      message:
        payload:
          type: string
          const: pong

  # ─── Subscription Management ─────────────────────────────────────────────

  subscription:
    description: Subscribe or unsubscribe from any topic.
    servers: [main-production, main-testnet, oss-production, oss-testnet]
    publish:
      operationId: subscribe
      summary: Subscribe / Unsubscribe
      message:
        payload:
          $ref: '#/components/schemas/SubscriptionRequest'
    subscribe:
      operationId: subscriptionAck
      summary: Subscription Acknowledgement
      message:
        payload:
          $ref: '#/components/schemas/SubscriptionResponse'

  # ─── Authentication ──────────────────────────────────────────────────────

  auth:
    description: |
      Authenticate the session to access private topics (Notifications, User Trade Fills).
      Signature: `HMAC-SHA384(secret, "/ws/spot" + nonce)`
    servers: [main-production, main-testnet]
    publish:
      operationId: authenticate
      summary: WebSocket Authentication
      message:
        payload:
          $ref: '#/components/schemas/AuthRequest'

  # ─── OSS: Order Book Streams ─────────────────────────────────────────────

  "snapshotL1:{symbol}":
    description: |
      Subscribe to the best bid / best ask (Level 1) for a market.
      **Topic format:** `snapshotL1:<symbol>` (e.g. `snapshotL1:BTC-USD`)
    parameters:
      symbol:
        description: Market symbol (e.g. BTC-USD)
        schema:
          type: string
    servers: [oss-production, oss-testnet]
    subscribe:
      operationId: receiveL1Snapshot
      summary: OSS L1 Snapshot
      message:
        payload:
          $ref: '#/components/schemas/L1SnapshotMessage'

  "update:{symbol}_{grouping}":
    description: |
      Subscribe to full orderbook updates with delta streaming.
      **Topic format:** `update:<symbol>_<grouping>` (e.g. `update:BTC-USD_0`)

      On successful subscribe, the server pushes one `snapshot` message (up to 50 levels)
      followed by `delta` updates. **The snapshot is delivered without grouping (raw book);
      only `delta` updates respect the subscribed `<grouping>` value.** If the market is
      idle, the snapshot still arrives but no deltas follow until book activity resumes.

      **Applying deltas:**
      - `[price, size]` tuple: if `size` is `"0"`, remove the price level; otherwise update it
      - `seqNum` must equal `prevSeqNum + 1`. On gap, unsubscribe and re-subscribe
      - On crossed orderbook (best bid >= best ask), unsubscribe and re-subscribe
    parameters:
      symbol:
        description: Market symbol
        schema:
          type: string
      grouping:
        description: |
          Price-tick grouping level. `0` is the finest grouping (raw book);
          higher values aggregate into wider price buckets.
        schema:
          type: integer
          enum: [0, 1, 2, 3, 4, 5, 6, 7, 8]
    servers: [oss-production, oss-testnet]
    subscribe:
      operationId: receiveOrderbookUpdate
      summary: Orderbook Incremental Updates
      message:
        payload:
          $ref: '#/components/schemas/OrderbookUpdateMessage'

  # ─── Main: Public Streams ────────────────────────────────────────────────

  "tradeHistoryApi:{symbol}":
    description: |
      Real-time public trade feed for a market.
      **Topic:** `tradeHistoryApi:<symbol>` — no authentication required.
    parameters:
      symbol:
        description: Market symbol
        schema:
          type: string
    servers: [main-production, main-testnet]
    subscribe:
      operationId: receivePublicTrades
      summary: Public Trade Fills
      message:
        payload:
          $ref: '#/components/schemas/PublicTradeMessage'

  # ─── Main: Private Streams ──────────────────────────────────────────────

  notificationApiV3:
    description: |
      Real-time order status updates. Requires WebSocket authentication.
      **Topic:** `notificationApiV3`

      Includes both base and quote currency size fields (`originalOrderBaseSize`,
      `originalOrderQuoteSize`, `currentOrderBaseSize`, `currentOrderQuoteSize`,
      `filledBaseSize`, `totalFilledBaseSize`, `remainingBaseSize`, `remainingQuoteSize`)
      so that quote-denominated market orders are represented faithfully.
    servers: [main-production, main-testnet]
    subscribe:
      operationId: receiveNotifications
      summary: Notifications
      message:
        payload:
          $ref: '#/components/schemas/NotificationMessage'

  fills:
    description: |
      Notification whenever an order is filled. Requires WebSocket authentication.
      **Topic:** `fills`
    servers: [main-production, main-testnet]
    subscribe:
      operationId: receiveUserFills
      summary: User Trade Fills
      message:
        payload:
          $ref: '#/components/schemas/UserFillMessage'

# ═════════════════════════════════════════════════════════════════════════
# Components
# ═════════════════════════════════════════════════════════════════════════

components:
  schemas:
    # ─── Subscription ──────────────────────────────────────────────────────

    SubscriptionRequest:
      type: object
      required: [op, args]
      properties:
        op:   { type: string, enum: [subscribe, unsubscribe], description: Operation }
        args: { type: array, items: { type: string }, description: Array of topic strings }
      example:
        op: subscribe
        args: ["tradeHistoryApi:BTC-USD"]

    SubscriptionResponse:
      type: object
      properties:
        event:   { type: string, description: Echo of the event type }
        channel: { type: array, items: { type: string }, description: Successfully subscribed topics }
      example:
        event: subscribe
        channel: ["tradeHistoryApi:BTC-USD"]

    # ─── Authentication ────────────────────────────────────────────────────

    AuthRequest:
      type: object
      required: [op, args]
      properties:
        op:   { type: string, const: authKeyExpires, description: "Operation — fixed value `authKeyExpires`" }
        args:
          type: array
          items: { type: string }
          minItems: 3
          maxItems: 3
          description: "[API_KEY, NONCE, HMAC-SHA384_SIGNATURE]"
      example:
        op: authKeyExpires
        args: ["<API_KEY>", "<NONCE>", "<SIGNATURE>"]

    # ─── OSS Orderbook ─────────────────────────────────────────────────────

    L1SnapshotMessage:
      type: object
      properties:
        topic: { type: string, description: WebSocket topic }
        data:
          type: object
          properties:
            bids:      { type: array, items: { type: array, items: { type: string } }, description: "Best bid [price, size]" }
            asks:      { type: array, items: { type: array, items: { type: string } }, description: "Best ask [price, size]" }
            symbol:    { type: string, description: Market symbol }
            type:      { type: string, const: snapshotL1 }
            timestamp: { type: integer, format: int64, description: Orderbook timestamp (ms) }
      example:
        topic: "snapshotL1:BTC-USD"
        data:
          bids: [["28016.7", "1.48063"]]
          asks: [["28033.6", "1.34133"]]
          type: snapshotL1
          symbol: BTC-USD
          timestamp: 1680750154232

    OrderbookUpdateMessage:
      type: object
      properties:
        topic: { type: string, description: WebSocket topic }
        data:
          type: object
          properties:
            bids:       { type: array, items: { type: array, items: { type: string } }, description: "Bid updates [price, size]" }
            asks:       { type: array, items: { type: array, items: { type: string } }, description: "Ask updates [price, size]" }
            seqNum:     { type: integer, description: Current sequence number }
            prevSeqNum: { type: integer, description: Previous sequence number }
            type:       { type: string, enum: [snapshot, delta], description: "snapshot (first) or delta (updates)" }
            timestamp:  { type: integer, format: int64, description: Orderbook timestamp }
            symbol:     { type: string, description: Market symbol }
      example:
        topic: "update:BTC-USD_0"
        data:
          bids: [["59252.5", "0.06865"]]
          asks: [["59292.0", "0.50000"]]
          seqNum: 628282
          prevSeqNum: 628281
          type: snapshot
          timestamp: 1565135165600
          symbol: BTC-USD

    # ─── Public Trades ─────────────────────────────────────────────────────

    PublicTradeMessage:
      type: object
      properties:
        topic: { type: string, description: WebSocket topic }
        data:
          type: array
          items:
            type: object
            properties:
              symbol:    { type: string, description: Market symbol }
              side:      { type: string, enum: [BUY, SELL], description: Trade side }
              size:      { type: number, description: Transacted size }
              price:     { type: number, description: Transacted price }
              tradeId:   { type: integer, format: int64, description: Trade sequence ID }
              timestamp: { type: integer, format: int64, description: Trade timestamp }
      example:
        topic: "tradeHistoryApi:BTC-USD"
        data:
          - symbol: BTC-USD
            side: SELL
            size: 0.007
            price: 5302.8
            tradeId: 118974855
            timestamp: 1584446020295

    # ─── Notifications ─────────────────────────────────────────────────────

    NotificationMessage:
      type: object
      properties:
        topic: { type: string, const: notificationApiV3 }
        data:
          type: object
          properties:
            symbol:                 { type: string,  description: Market symbol }
            orderID:                { type: string,  description: Internal order ID }
            side:                   { type: string,  description: Trade direction (BUY or SELL) }
            orderType:              { type: integer, description: "Order classification — `76`: Limit · `77`: Market · `80`: Peg/Algo" }
            txType:                 { type: integer, description: "Transaction type — `0`: LIMIT · `1`: STOP · `2`: TRIGGER · `3`: OCO" }
            price:                  { type: number,  description: Order price or executed transaction price }
            triggerPrice:           { type: number,  description: Activation price for conditional orders }
            pegPriceDeviation:      { type: number,  description: Deviation percentage (Algo orders only) }
            stealth:                { type: number,  description: Order visibility percentage on orderbook (Algo orders only) }
            status:                 { type: integer, description: "Current order state — `1` (MARKET_UNAVAILABLE) · `2` (ORDER_INSERTED) · `4` (ORDER_FULLY_TRANSACTED) · `5` (ORDER_PARTIALLY_TRANSACTED) · `6` (ORDER_CANCELLED) · `8` (INSUFFICIENT_BALANCE) · `9` (TRIGGER_INSERTED) · `10` (TRIGGER_ACTIVATED) · `12` (ERROR_UPDATE_RISK_LIMIT) · `15` (ORDER_REJECTED) · `27` (TRANSFER_SUCCESSFUL) · `28` (TRANSFER_UNSUCCESSFUL) · `41` (ERROR_INVALID_RISK_LIMIT) · `64` (STATUS_LIQUIDATION) · `101` (FUTURES_ORDER_PRICE_OUTSIDE_LIQUIDATION_PRICE) · `1003` (ORDER_LIQUIDATION) · `1004` (ORDER_ADL)" }
            timestamp:              { type: integer, format: int64, description: Order or transaction timestamp (ms) }
            avgFilledPrice:         { type: number,  description: Mean execution price across fills }
            clOrderID:              { type: string,  description: User-supplied order identifier }
            maker:                  { type: boolean, description: Whether this fill was on the maker side }
            postOnly:               { type: boolean, description: Resting order without aggressive matching }
            orderUserInitiated:     { type: boolean, description: Manual (true) vs. automated (false) order origin }
            originalOrderBaseSize:  { type: number,  description: Initial base currency quantity }
            originalOrderQuoteSize: { type: number,  description: Initial quote currency quantity }
            currentOrderBaseSize:   { type: number,  description: Latest base currency quantity }
            currentOrderQuoteSize:  { type: number,  description: Latest quote currency quantity }
            filledBaseSize:         { type: number,  description: Base currency amount executed on this event }
            totalFilledBaseSize:    { type: number,  description: Cumulative base currency fills }
            remainingBaseSize:      { type: number,  description: Unfilled base currency portion }
            remainingQuoteSize:     { type: number,  description: Unfilled quote currency portion }
            orderCurrency:          { type: string,  description: "Size denomination of the order (`base` or `quote`)" }
            time_in_force:          { type: string,  description: "Time in force: `GTC` · `IOC` · `FOK` · `HALFMIN` · `FIVEMIN` · `HOUR` · `TWELVEHOUR` · `DAY` · `WEEK` · `MONTH`" }
      example:
        topic: notificationApiV3
        data:
          symbol: BTC-USDT
          orderID: "789b05fa-dd43-43e8-8626-e229ae216ead"
          side: BUY
          orderType: 77
          txType: 0
          price: 111131
          triggerPrice: 0
          pegPriceDeviation: 1
          stealth: 1
          status: 5
          timestamp: 1752147703368
          avgFilledPrice: 111131
          clOrderID: "_W_bjvf1752147703280"
          maker: false
          postOnly: false
          orderUserInitiated: true
          originalOrderBaseSize: 0
          originalOrderQuoteSize: 1000
          currentOrderBaseSize: 0
          currentOrderQuoteSize: 1000
          filledBaseSize: 0.00899
          totalFilledBaseSize: 0.00899
          remainingBaseSize: 0
          remainingQuoteSize: 0.93231
          orderCurrency: quote
          time_in_force: GTC

    # ─── User Trade Fills ──────────────────────────────────────────────────

    UserFillMessage:
      type: object
      properties:
        topic: { type: string, const: fills }
        data:
          type: array
          items:
            type: object
            properties:
              symbol:      { type: string,  description: Market symbol }
              orderId:     { type: string,  description: Internal order ID }
              clOrderId:   { type: string,  description: Custom order ID }
              side:        { type: string,  description: BUY or SELL }
              price:       { type: string,  description: Fill price }
              size:        { type: string,  description: Fill size }
              feeAmount:   { type: string,  description: Fee amount charged }
              feeCurrency: { type: string,  description: "Fee currency (BUY: base, SELL: quote)" }
              base:        { type: string,  description: Base currency }
              quote:       { type: string,  description: Quote currency }
              maker:       { type: boolean, description: Whether this was a maker trade }
              timestamp:   { type: string,  description: Fill timestamp }
              tradeId:     { type: string,  description: Unique trade ID }
      example:
        topic: fills
        data:
          - orderId: "<order ID>"
            serialId: "<serial ID>"
            clOrderId: "<custom order ID>"
            type: "<order type>"
            symbol: BTC-USD
            side: BUY
            price: "34000.0"
            size: "1.0"
            feeAmount: "0.01"
            feeCurrency: USD
            base: BTC
            quote: USD
            maker: false
            timestamp: "1624985375123"
            tradeId: "<trade unique ID>"
