# GENERATED from exchange specs/asyncapi.yaml by scripts/sync-specs.py.
# Do not edit here — change the exchange spec and re-run.
asyncapi: 2.6.0

# ---------------------------------------------------------------------------
# Public machine-readable contract. Regenerated by scripts/sync-specs.py;
# do not hand-edit. See https://docs.omnibook.xyz/ for guides.
# ---------------------------------------------------------------------------

info:
  title: Exchange API — WebSocket lane
  version: "1.0.0"
  description: |
    The single multiplexed WebSocket the exchange exposes. One authenticated
    connection to `/v1/ws` carries every channel; a client `subscribe`s to
    channels and receives server-assigned `sid`s, then data frames flow on
    those sids.

    ## Channels a client can subscribe to
    | Channel | Scope | Content |
    |---|---|---|
    | `orderbook_delta` | read | L2 snapshot-then-deltas per market |
    | `trades` | read | public tape entries as they print |
    | `ticker` | read | conflated display feed — **deferred**; subscribing currently returns `wscode 4` |
    | `user` | read | the private per-user channel: orders, fills, balance, funding lifecycle |
    | `oracle` | none | global live reference price (median of the CEX perp streams), pushed tick-by-tick |
    | `rounds` | none | global round lifecycle: create / open (strike) / freeze (settle_price) / settle (winner) |

    Per-market public channels (`orderbook_delta`, `trades`) require
    `market_ids`; the `user` channel does not and instead supports resume via
    `last_channel_seq`. `ticker` is **deferred** (subscribe ⇒ `wscode 4`).
    `oracle` and `rounds` are **global, unauthenticated**
    public channels (the live price and round history are both public over REST):
    they carry no `market_ids`, need no `read` scope, and have no sync burst — a
    client reads the current baseline once from REST (`GET /v1/oracle/price`,
    `GET /v1/rounds?limit=1`) at mount, then live frames flow.

    ## Handshake
    The HTTP upgrade to `/v1/ws` carries the same signing headers as REST —
    `DX-ACCESS-KEY`, `DX-ACCESS-TIMESTAMP`, `DX-ACCESS-SIGNATURE` — with the
    signature computed over the canonical string `timestamp + "\n" + "GET" +
    "\n" + "/v1/ws" + "\n"` (empty body, no query). A bad signature fails the
    upgrade with HTTP 401. A `trade`-only key connects but receives `wscode 7`
    on any subscribe EXCEPT the global public `oracle` / `rounds` channels, which
    need no `read` scope (a mixed request still needs `read` for its other
    channels).

    ## Envelope seq vs channel_seq
    Public data frames carry a per-subscription `seq` (a JSON number, contiguous
    from 1 on each sid, counting snapshots). The `user` channel instead carries
    a per-user monotonic `channel_seq` (decimal string) plus the producing
    record's stream `seq`; there is no per-sid counter on the user channel. One
    gap-detector per channel kind.

    ## Delivery & recovery
    - Public channels have no replay: a gap in the envelope `seq` means loss →
      resubscribe for a fresh snapshot. Overflow closes the connection with
      **code 4001**.
    - The private `user` channel is never conflated or thinned. Overflow closes
      with **code 4000** and a `last_channel_seq` resume hint. On (re)subscribe,
      the server either replays from `last_channel_seq + 1` (if still in the
      ~10k-event / ~10-minute ring) or sends a `snapshot` then goes live.
    - The server pings every 10 s; two missed pongs close the connection.

  x-close-codes:
    "4000": Private-channel overflow; carries a last_channel_seq resume hint.
    "4001": Public-channel overflow; client resubscribes fresh.

externalDocs:
  description: Normative WebSocket contract (client wire + session state machine).
  url: https://docs.omnibook.xyz/

servers:
  production:
    url: wss://api.omnibook.xyz/v1/ws
    protocol: wss
    description: >-
      Production WebSocket. Same signing headers as REST.
      See Authentication.
    security:
      - dxAccessSignature: []

channels:
  /v1/ws:
    description: >
      The one multiplexed WebSocket. Clients `publish` command envelopes
      (subscribe / unsubscribe / list_subscriptions); the server `subscribe`-
      side emits command replies and channel data frames.
    bindings:
      ws:
        method: GET
        headers:
          type: object
          properties:
            DX-ACCESS-KEY: { type: string, description: API key id (decimal). }
            DX-ACCESS-TIMESTAMP: { type: string, description: Unix ms (decimal). }
            DX-ACCESS-SIGNATURE:
              type: string
              description: base64 HMAC-SHA256 over "timestamp\nGET\n/v1/ws\n".
    publish:
      operationId: sendCommand
      summary: Client → server. Send a command envelope over the connection.
      description: One of subscribe / unsubscribe / update_subscription / list_subscriptions.
      message:
        oneOf:
          - { $ref: "#/components/messages/SubscribeCommand" }
          - { $ref: "#/components/messages/UnsubscribeCommand" }
          - { $ref: "#/components/messages/UpdateSubscriptionCommand" }
          - { $ref: "#/components/messages/ListSubscriptionsCommand" }
    subscribe:
      operationId: receiveFrame
      summary: Server → client. Receive command replies and channel data frames.
      description: >
        Command replies (subscribed / unsubscribed / subscriptions / error) and
        data frames for every subscribed channel.
      message:
        oneOf:
          - { $ref: "#/components/messages/SubscribedReply" }
          - { $ref: "#/components/messages/UnsubscribedReply" }
          - { $ref: "#/components/messages/UpdatedReply" }
          - { $ref: "#/components/messages/SubscriptionsReply" }
          - { $ref: "#/components/messages/ErrorReply" }
          - { $ref: "#/components/messages/OrderbookSnapshotData" }
          - { $ref: "#/components/messages/OrderbookDeltaData" }
          - { $ref: "#/components/messages/TradesData" }
          - { $ref: "#/components/messages/OracleData" }
          - { $ref: "#/components/messages/RoundsData" }
          - { $ref: "#/components/messages/UserEventData" }
          - { $ref: "#/components/messages/UserSnapshotData" }

components:

  securitySchemes:
    dxAccessSignature:
      type: httpApiKey
      in: header
      name: DX-ACCESS-SIGNATURE
      description: >
        base64 HMAC-SHA256 signature (with the companion DX-ACCESS-KEY and
        DX-ACCESS-TIMESTAMP headers) over "timestamp\nGET\n/v1/ws\n". All three
        headers are required on the upgrade.

  messages:
    # ---- client → server (publish) -------------------------------------
    SubscribeCommand:
      name: subscribe
      title: Subscribe to one or more channels
      summary: Client subscribes; server assigns a sid per channel and emits the sync burst.
      payload: { $ref: "#/components/schemas/SubscribeEnvelope" }
    UnsubscribeCommand:
      name: unsubscribe
      title: Unsubscribe by sid
      payload: { $ref: "#/components/schemas/UnsubscribeEnvelope" }
    UpdateSubscriptionCommand:
      name: update_subscription
      title: Mutate a live public subscription in place (Kalshi parity)
      summary: >
        get_snapshot (re-emit snapshots without changing the set) / add_markets /
        delete_markets on an existing public sid — no unsubscribe + resubscribe,
        so the sid and its per-sid seq are preserved.
      payload: { $ref: "#/components/schemas/UpdateSubscriptionEnvelope" }
    ListSubscriptionsCommand:
      name: list_subscriptions
      title: List this connection's active subscriptions
      payload: { $ref: "#/components/schemas/ListSubscriptionsEnvelope" }

    # ---- server → client: command replies (subscribe) ------------------
    SubscribedReply:
      name: subscribed
      title: Acknowledgement of one subscribed channel
      payload: { $ref: "#/components/schemas/SubscribedReply" }
    UnsubscribedReply:
      name: unsubscribed
      title: Acknowledgement of one unsubscribed sid
      payload: { $ref: "#/components/schemas/UnsubscribedReply" }
    UpdatedReply:
      name: updated
      title: Acknowledgement of an update_subscription
      summary: Carries the sub's resulting market set; followed by any snapshot burst.
      payload: { $ref: "#/components/schemas/UpdatedReply" }
    SubscriptionsReply:
      name: subscriptions
      title: Reply to list_subscriptions
      payload: { $ref: "#/components/schemas/SubscriptionsReply" }
    ErrorReply:
      name: error
      title: Command error (wscode)
      payload: { $ref: "#/components/schemas/ErrorReply" }

    # ---- server → client: public channel data (subscribe) --------------
    OrderbookSnapshotData:
      name: orderbook_snapshot
      title: First frame of an orderbook_delta subscription (per market)
      payload: { $ref: "#/components/schemas/PublicEnvelope_OrderbookSnapshot" }
    OrderbookDeltaData:
      name: orderbook_delta
      title: Incremental L2 level update
      payload: { $ref: "#/components/schemas/PublicEnvelope_OrderbookDelta" }
    TradesData:
      name: trades
      title: Public tape entry
      payload: { $ref: "#/components/schemas/PublicEnvelope_Trade" }
    OracleData:
      name: oracle
      title: Live reference-price tick (global oracle channel)
      payload: { $ref: "#/components/schemas/PublicEnvelope_Oracle" }
    RoundsData:
      name: rounds
      title: Round lifecycle event (global rounds channel)
      payload: { $ref: "#/components/schemas/PublicEnvelope_Rounds" }

    # ---- server → client: private user channel data (subscribe) --------
    UserEventData:
      name: user_event
      title: One private user-channel event
      payload: { $ref: "#/components/schemas/PrivateEnvelope_Event" }
    UserSnapshotData:
      name: user_snapshot
      title: Private-channel resume snapshot (balance + positions + orders)
      payload: { $ref: "#/components/schemas/PrivateEnvelope_Snapshot" }

  schemas:
    # ---- primitives ----------------------------------------------------
    U64String:
      type: string
      description: u64-domain value as a decimal string.
      pattern: "^[0-9]+$"
    Tick:
      type: integer
      minimum: 1
      maximum: 99
      description: Price tick in cents, in the outcome's own space.
    ChannelName:
      type: string
      enum: [orderbook_delta, trades, ticker, "user", oracle, rounds]
    BidsSide:
      type: string
      description: The bids-only, both-sides convention.
      enum: ["yes", "no"]

    # ---- client command envelopes --------------------------------------
    SubscribeEnvelope:
      type: object
      additionalProperties: false
      properties:
        id: { type: integer, description: Client-unique; echoed in every reply. }
        cmd: { type: string, const: subscribe }
        params:
          type: object
          additionalProperties: false
          properties:
            channels:
              type: array
              minItems: 1
              items: { $ref: "#/components/schemas/ChannelName" }
            market_ids:
              type: array
              description: Required iff any public channel is requested; unique ids, each < market count.
              items: { type: integer }
            last_channel_seq:
              allOf: [{ $ref: "#/components/schemas/U64String" }]
              description: "`user` channel only — resume point; omit for a fresh snapshot."
          required: [channels]
      required: [id, cmd, params]
    UnsubscribeEnvelope:
      type: object
      additionalProperties: false
      properties:
        id: { type: integer }
        cmd: { type: string, const: unsubscribe }
        params:
          type: object
          additionalProperties: false
          properties:
            sids:
              type: array
              minItems: 1
              items: { type: integer }
          required: [sids]
      required: [id, cmd, params]
    UpdateSubscriptionEnvelope:
      type: object
      additionalProperties: false
      description: >
        Mutate a live public subscription in place. `sid` must name
        an existing subscription on this connection; `market_ids` is the non-empty
        set the action applies to. All-or-nothing: on the first validation failure
        nothing is committed. `get_snapshot` is orderbook_delta-only and leaves the
        set unchanged; `add_markets` / `delete_markets` require a per-market channel
        (orderbook_delta or trades).
      properties:
        id: { type: integer }
        cmd: { type: string, const: update_subscription }
        params:
          type: object
          additionalProperties: false
          properties:
            sid: { type: integer, description: An existing subscription id owned by this connection. }
            action:
              type: string
              enum: [get_snapshot, add_markets, delete_markets]
            market_ids:
              type: array
              minItems: 1
              description: >
                Unique ids, each < market count. get_snapshot/delete_markets ids
                must already be in the sub; add_markets ids must not be, and the
                resulting set must fit max_markets_per_sub. delete may not empty
                the set.
              items: { type: integer }
          required: [sid, action, market_ids]
      required: [id, cmd, params]
    ListSubscriptionsEnvelope:
      type: object
      additionalProperties: false
      properties:
        id: { type: integer }
        cmd: { type: string, const: list_subscriptions }
        params: { type: object }
      required: [id, cmd]

    # ---- server command replies ----------------------------------------
    SubscribedReply:
      type: object
      properties:
        id: { type: integer }
        type: { type: string, const: subscribed }
        sid: { type: integer, description: Server-assigned subscription id (first = 1). }
        channel: { $ref: "#/components/schemas/ChannelName" }
      required: [id, type, sid, channel]
    UnsubscribedReply:
      type: object
      properties:
        id: { type: integer }
        type: { type: string, const: unsubscribed }
        sid: { type: integer }
      required: [id, type, sid]
    UpdatedReply:
      type: object
      description: >
        Acknowledges an update_subscription. `market_ids` is the
        subscription's resulting sorted set (unchanged for get_snapshot). A
        snapshot burst follows for get_snapshot (requested markets) and
        add_markets on orderbook_delta (newly added markets); the per-sid `seq`
        stays contiguous across it.
      properties:
        id: { type: integer }
        type: { type: string, const: updated }
        sid: { type: integer }
        action: { type: string, enum: [get_snapshot, add_markets, delete_markets] }
        market_ids: { type: array, items: { type: integer } }
      required: [id, type, sid, action, market_ids]
    SubscriptionsReply:
      type: object
      properties:
        id: { type: integer }
        type: { type: string, const: subscriptions }
        subs:
          type: array
          items:
            type: object
            properties:
              sid: { type: integer }
              channel: { $ref: "#/components/schemas/ChannelName" }
              market_ids:
                type: array
                description: Omitted for the `user` channel.
                items: { type: integer }
            required: [sid, channel]
      required: [id, type, subs]
    ErrorReply:
      type: object
      description: A command error.
      properties:
        id: { type: integer, description: 0 when no id was recoverable. }
        type: { type: string, const: error }
        msg:
          type: object
          properties:
            wscode:
              type: integer
              description: |
                1 unable to process · 2 params required · 3 unknown command ·
                4 unknown channel · 5 unknown sid · 6 already subscribed ·
                7 auth/scope insufficient · 8 invalid parameter ·
                9 market required · 10 rate limited
              enum: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
            text:
              type: string
              description: Fixed string paired with the wscode.
          required: [wscode, text]
      required: [id, type, msg]

    # ---- public data envelope ------------------------------------------
    # Public frames: { sid, seq (number, per-sid, from 1), type, msg }.
    PublicEnvelope_OrderbookSnapshot:
      type: object
      properties:
        sid: { type: integer }
        seq: { type: integer, description: Per-subscription, contiguous from 1 (JSON number). }
        type: { type: string, const: orderbook_snapshot }
        msg: { $ref: "#/components/schemas/OrderbookSnapshotMsg" }
      required: [sid, seq, type, msg]
    PublicEnvelope_OrderbookDelta:
      type: object
      properties:
        sid: { type: integer }
        seq: { type: integer }
        type: { type: string, const: orderbook_delta }
        msg: { $ref: "#/components/schemas/OrderbookDeltaMsg" }
      required: [sid, seq, type, msg]
    PublicEnvelope_Trade:
      type: object
      properties:
        sid: { type: integer }
        seq: { type: integer }
        type: { type: string, const: trades }
        msg: { $ref: "#/components/schemas/TradeMsg" }
      required: [sid, seq, type, msg]
    PublicEnvelope_Oracle:
      type: object
      properties:
        sid: { type: integer }
        seq: { type: integer }
        type: { type: string, const: oracle }
        msg: { $ref: "#/components/schemas/OracleMsg" }
      required: [sid, seq, type, msg]
    PublicEnvelope_Rounds:
      type: object
      properties:
        sid: { type: integer }
        seq: { type: integer }
        type: { type: string, const: rounds }
        msg: { $ref: "#/components/schemas/RoundsMsg" }
      required: [sid, seq, type, msg]

    OrderbookLevel:
      type: array
      description: "[tick, aggregate_qty]. qty is a u64 aggregate → decimal string."
      prefixItems:
        - { $ref: "#/components/schemas/Tick" }
        - { $ref: "#/components/schemas/U64String" }
      minItems: 2
      maxItems: 2
    OrderbookSnapshotMsg:
      type: object
      description: Full book, bids-only both sides, best-first.
      properties:
        market_id: { type: integer }
        "yes": { type: array, items: { $ref: "#/components/schemas/OrderbookLevel" } }
        "no": { type: array, items: { $ref: "#/components/schemas/OrderbookLevel" } }
        as_of_seq: { $ref: "#/components/schemas/U64String" }
      required: [market_id, "yes", "no", as_of_seq]
    OrderbookDeltaMsg:
      type: object
      description: One L2 level's new aggregate.
      properties:
        market_id: { type: integer }
        side: { $ref: "#/components/schemas/BidsSide" }
        tick: { $ref: "#/components/schemas/Tick" }
        qty: { allOf: [{ $ref: "#/components/schemas/U64String" }], description: new aggregate at the level (u64 → string) }
        seq_ref: { allOf: [{ $ref: "#/components/schemas/U64String" }], description: producing record's stream seq }
      required: [market_id, side, tick, qty, seq_ref]
    TradeMsg:
      type: object
      description: Public tape entry.
      properties:
        seq: { $ref: "#/components/schemas/U64String" }
        ts: { $ref: "#/components/schemas/U64String" }
        market_id: { type: integer }
        tick_yes: { $ref: "#/components/schemas/Tick" }
        qty: { type: integer, description: per-maker fill quantity (u32 → JSON number) }
        path: { type: string, enum: [direct, mint, merge] }
        taker_side: { type: string, enum: [buy, sell] }
        taker_outcome: { type: string, enum: ["yes", "no"] }
      required: [seq, ts, market_id, tick_yes, qty, path, taker_side, taker_outcome]
    OracleMsg:
      type: object
      description: >
        Live reference-price tick (live-market feature). Mirrors
        `GET /v1/oracle/price` (gateway.rs::render_oracle_price) where fields
        overlap. `price` is the median @1e8 (u64 → decimal string); `seq_ref` is
        the producing ORACLE_PRICE record's stream seq.
      properties:
        price: { allOf: [{ $ref: "#/components/schemas/U64String" }], description: median reference price @1e8 (u64 → string) }
        fresh_source_count: { type: integer, description: fresh perp streams behind this tick (u8 → JSON number) }
        seq_ref: { allOf: [{ $ref: "#/components/schemas/U64String" }], description: producing record's stream seq }
      required: [price, fresh_source_count, seq_ref]
    RoundsMsg:
      type: object
      description: >
        One round-lifecycle event (live-market feature). Field names mirror
        `GET /v1/rounds` (gateway.rs::render_round_row) where they overlap, so
        REST and WS agree. Only the fields relevant to `action` are present:
        `strike` on `open`, `settle_price` + `fresh_source_count` on `freeze`,
        `winner` + `yes_payout` on `settle`; `create` carries the base fields
        only. `strike`/`settle_price`/`seq_ref`/`ts` are u64 decimal strings;
        `market_id`/`fresh_source_count`/`yes_payout` are bare numbers.
      properties:
        action: { type: string, enum: [create, open, freeze, settle] }
        market_id: { type: integer, description: the round's venue market id (the 64,65,66… series) }
        seq_ref: { allOf: [{ $ref: "#/components/schemas/U64String" }], description: producing record's stream seq }
        ts: { allOf: [{ $ref: "#/components/schemas/U64String" }], description: producing record's header ts (ns) }
        strike: { allOf: [{ $ref: "#/components/schemas/U64String" }], description: "open only — the open price @1e8 (last-good oracle)" }
        settle_price: { allOf: [{ $ref: "#/components/schemas/U64String" }], description: "freeze only — the close price @1e8 (true TWAP of sequenced oracle ticks over the last twap_window_secs for the round's category)" }
        fresh_source_count: { type: integer, description: "freeze only — fresh perp streams at freeze (u8 → JSON number)" }
        winner: { type: string, enum: ["yes", "no"], description: "settle only — the terminal outcome" }
        yes_payout: { type: integer, description: "settle only — the YES split in cents (u8 → JSON number)" }
      required: [action, market_id, seq_ref, ts]

    # ---- private (user) data envelope ----------------------------------
    # Private frames: { sid, channel_seq (string), seq (stream, string), ts, type, msg }.
    PrivateEnvelope_Event:
      type: object
      description: One user-channel event.
      properties:
        sid: { type: integer }
        channel_seq: { allOf: [{ $ref: "#/components/schemas/U64String" }], description: per-user monotonic counter }
        seq: { allOf: [{ $ref: "#/components/schemas/U64String" }], description: producing record's stream seq }
        ts: { $ref: "#/components/schemas/U64String" }
        type:
          type: string
          description: The state-machine transitions plus the gate's ledger entries.
          enum:
            - accepted
            - rejected
            - fill
            - canceled
            - decreased
            - reserved
            - released
            - fill_ledger
            - redeemed
            - settled
            - deposited
            - withdrawal_reserved
            - withdrawn
            - withdrawal_failed
        msg:
          type: object
          description: >
            The JSON view of the sequenced event; shape varies by `type`.
      required: [sid, channel_seq, seq, ts, type, msg]
    PrivateEnvelope_Snapshot:
      type: object
      description: Resume snapshot sent on `user` subscribe when replay is unavailable.
      properties:
        sid: { type: integer }
        type: { type: string, const: snapshot }
        channel_seq: { $ref: "#/components/schemas/U64String" }
        as_of_seq: { $ref: "#/components/schemas/U64String" }
        msg:
          type: object
          properties:
            balance:
              type: object
              properties:
                available: { $ref: "#/components/schemas/U64String" }
                reserved: { $ref: "#/components/schemas/U64String" }
              required: [available, reserved]
            positions:
              type: array
              items:
                type: object
                properties:
                  market_id: { type: integer }
                  outcome: { type: string, enum: ["yes", "no"] }
                  free: { $ref: "#/components/schemas/U64String" }
                  reserved_for_sale: { $ref: "#/components/schemas/U64String" }
                required: [market_id, outcome, free, reserved_for_sale]
            orders:
              type: array
              description: >
                Working-set order objects: ids, market,
                side/outcome/tick, status, quantities. The static request echo
                and created_seq/created_ts/last_update_seq are REST-joinable at
                the same as_of_seq via GET /v1/portfolio/orders.
              items: { type: object }
          required: [balance, positions, orders]
      required: [sid, type, channel_seq, as_of_seq, msg]
