Skip to main content

Endpoint

A single WebSocket connection can subscribe to up to 1,000 markets. The server pushes a snapshot on subscribe and updates every time the orderbook changes.

Authentication

Browsers can’t set custom headers on the WebSocket handshake, so the signed-request flow moves to query params. The signature covers WS\nPATH\nSORTED_QUERY (excluding key_id/ts/sig)\nTIMESTAMP. See Authentication for the full recipe.
The official SDKs sign the handshake for you.
Signed handshake
For first-party web clients, authenticate with a Supabase JWT carried in the subprotocol list — browsers cannot set headers on a WS upgrade, and a token in the query string would be written into every intermediary’s request log:
The server selects the bare river.jwt marker, so the token never appears in a response either. Invalid or missing auth closes the connection with code 4401.

Protocol

All frames are JSON. Client frames are text; server frames are bytes (orjson-serialized UTF-8).

Client → server

  • subscribe / unsubscribe accept one or many river_ids. Duplicate subscribes on the same connection are no-ops.
  • max_freq (optional, Hz) caps update frames per market for the river_ids in that frame. While capped, updates are coalesced to the latest book state, so you always converge on the current orderbook; the first update after a quiet period is delivered immediately. Each subscribe frame declares the desired cap for its river_ids: re-subscribing an already-subscribed river_id with a different max_freq updates the cap, and re-subscribing without one clears it. Omitted means uncapped. A non-positive or non-numeric value gets an error frame with code bad_max_freq.

Server → client

  • snapshot is sent once on successful subscribe when the orderbook is already cached. Subsequent changes are delivered as update frames with an identical payload shape.
  • pending is sent when the orderbook isn’t in cache yet. A snapshot arrives shortly, followed by updates.
  • error with code subscription_limit is sent when a subscribe would take the connection past 1,000 markets. The whole frame is rejected and none of its river_ids are added — unsubscribe from markets you no longer need, or spread subscriptions across connections.

Keepalive

Liveness is handled at the WebSocket protocol layer (RFC 6455 PING/PONG control frames). Standard clients (Python websockets, browser WebSocket) reply to server PINGs automatically — no application-level heartbeat is needed.

Orderbook payload

is_valid=false indicates a transient crossed-book state at the exchange. The snapshot that follows will have fresh values.

Rate limits and pacing

By default orderbook updates are not rate-capped — every upstream change is forwarded as it happens. Pass max_freq on subscribe to cap update frames per market; this reduces bandwidth and client-side processing without risking stale data, because capped updates are coalesced to the latest book state. If your client falls behind, pending updates are likewise coalesced per market (only the latest state is kept). A client that cannot drain fast enough is disconnected with code 1011; reconnect and resubscribe to get a fresh snapshot.

Minimal client

Using the SDK (handles signing automatically):
msg.type and top-level frame fields (msg.river_id) are attributes; msg.data is a plain dict — index it. best_bid_price / best_ask_price are null on an empty side of the book.

Changing subscriptions mid-stream

The object returned by client.realtime.orderbooks(...) exposes subscribe / unsubscribe. Both accept one or many river_ids and can be called at any time — typically from a separate task while the async for loop drains messages. The SDK tracks the active set, so after a transparent reconnect the current set (including mid-stream changes) is re-subscribed automatically.
Or signing the handshake manually:

Close codes