Skip to main content
Connect to wss://api.liftx.io/v1/stream using a client that supports an HTTP Authorization: Bearer … upgrade header. read permission and current entitlement are required. Do not put a key in the URL or a WebSocket subprotocol. This is a state stream. Trading commands remain durable HTTP requests. The connection reuses Liftx publications and does not create a bot-specific exchange socket.

Authorization and connection scope

All channel controls belong to one exact exchange_link_id. A connection cannot switch to another link by sending another control. Use a separate authorized connection when another link is required, rather than multiplexing account authority implicitly. Instrument-restricted credentials cannot subscribe to link-wide balances, positions pages or private indexes. An exact-instrument ticker or authorized exact-position history remains subject to scope checks. Expiry has an exact connection deadline. Local revocation cancels registered streams, and an authority sweep detects out-of-process changes within its 30-second interval.

Client controls: JSON text frames

Each message has 1–32 channels and at most 16 KiB. Fields are exact, unique and case-sensitive; unknown fields invalidate control. Send only fields listed for the operation. Client controls must be text frames; binary, malformed or empty controls can terminate the stream generation. Position page offset is 0–10,000,000 and limit is 1–200. Each control can include at most one ticker channel and at most one position-history channel. History selection uses a monotonically increasing request ID within that connection; an identical current tuple can be repeated. Do not reuse an older request ID for a different history demand.

Server data frame header

Each binary WebSocket message contains one seven-byte header followed by its payload. WebSocket fragmentation is resolved by the WebSocket library before this parser runs. Require message_length == 7 + payload_length. Set a bounded receive limit appropriate to the documented pages; reject unknown versions and malformed/trailing payloads. Control ping/pong/close frames are WebSocket protocol operations, not this application envelope. Do not assume all payloads are JSON or that the same version applies to every channel.

Balances: channel 1, version 1

For an update frame, the payload is:
  1. count: uint32 little-endian.
  2. For each row, an asset string ending in NUL (0x00).
  3. A signed int64 little-endian free balance with fixed scale 12.
An empty batch contains four zero bytes. A separate response frame (message_type:3) with the single payload byte 0x01 marks readiness for the empty complete snapshot path. Completion metadata also arrives on channel 6. The connection’s exact link supplies scope; the balance row itself has no link field.
The existing balance stream codec is a compact presentation format. It truncates precision beyond 12 decimal places toward zero and is bounded by signed int64. Its current encoder can emit zero when conversion is unrepresentable. Therefore a zero stream value alone cannot establish an exact available balance. Use the fixed-point decimal HTTP /v1/balances snapshot, including snapshot_complete, for sizing and reconciliation. This codec limitation remains a release-qualification concern.

Positions: channel 2, version 1

Position publications contain revision, product/account identity, OPEN/close/SL/TP rows, canonical allocation/execution amounts, FSM projections and exact fixed-point fields. The full schema is included under Position and PositionOrder in the OpenAPI contract. Do not overwrite a complete position with a partial event as if it were a full model. Apply only understood event fields for the correct link/position/generation. If an event requires state that is absent or cannot be safely interpreted, mark the local view incomplete and reconcile the affected position. Coalesce that read; do not poll every position on every event. History notifications use event type position_pnl_history. Its data includes position_id, request_id, ready and reset. Verify the exact requested tuple before reading history. A reset invalidates the retained history baseline; a ready/reset notification is not the history samples themselves.

Tickers: channel 3, version 2

Every ticker payload begins with:
  • One-byte kind: 1 topic mapping or 2 price batch.
  • A uint16 little-endian row count.
A mapping frame has message type 3 and kind 1. Each row contains: An update frame has message type 1 and kind 2. Each row contains: Retain the mapping for the current connection/generation and resolve updates only against it. An unknown topic is not permission to infer a market from the numeric ID. Rebuild the mapping after reconnect. Exchange/source values describe the mapped publication; they do not override execution authority from your authenticated link/catalog.

Position and order indexes

Channel 4 snapshots use kind:"positions_index_snapshot"; deltas use "positions_index_delta". Channel 5 uses "orders_index_snapshot" and "orders_index_delta". Common fields are exchange_link_id, kind, session_gen (an exact decimal string) and rev. A snapshot adds complete and items; each item contains exchange_link_id and instrument_id. A delta has optional add/remove arrays with the same item shape. Positions index messages also include server_epoch, active_total and inactive_total. These count positions, while the items array is an instrument index, not one row per position. The orders index is likewise an instrument membership index, not a full executable-order inventory.
Require a complete snapshot before accepting deltas. Preserve full-width revisions and generation strings. On a changed generation/epoch, revision gap or an incomplete baseline, invalidate dependent state and request the appropriate index resync. An empty incomplete index does not mean no positions or orders exist.

Private control: channel 6, version 1

When snapshot_required is true, do not continue making trading decisions from an assumed complete cache. Rebuild the appropriate canonical snapshot. is_snapshot and revisions describe state publication; neither is a command receipt or fill confirmation.

Minimal decoder example

Download decode_stream.py for a bounded standard-library parser of the documented framing, balance and ticker wire formats. It does not open a connection, store credentials, reconstruct position FSMs or submit commands. JSON channels are returned as decoded values for a separate versioned application model. The decoder retains fractional JSON numbers as Python decimal.Decimal, integers as exact Python integers and fixed-point coefficient strings as strings. It does not convert money through binary floating-point or turn a large finite exponent into floating-point infinity. Application schema validation must still reject values outside each field’s documented bounds. A transport decoder alone is not a production SDK. A real client must implement schema validation, exact-link checks, snapshot/revision rules, bounded queues, credential lifecycle, websocket ping/pong and reconciliation before trading from observed state.

Reconnect and backpressure

Slow consumers are disconnected or instructed to rebuild snapshots under bounded backpressure. Reconnect with bounded backoff and jitter, discard obsolete session mappings, obtain fresh complete state and only then resume dependent decisions. A stream gap is not evidence that orders were canceled or that exposure is zero. Use a single consumer state owner per connection. Coalesce related state updates; avoid spawning one exchange socket, polling loop or unconstrained worker for every position or strategy.