Endpoint
Authentication
Browsers can’t set custom headers on the WebSocket handshake, so the signed-request flow moves to query params. The signature coversWS\nPATH\nSORTED_QUERY (excluding key_id/ts/sig)\nTIMESTAMP. See
Authentication for the full recipe.
Signed handshake
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/unsubscribeaccept one or manyriver_ids. Duplicate subscribes on the same connection are no-ops.max_freq(optional, Hz) capsupdateframes per market for theriver_idsin 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 itsriver_ids: re-subscribing an already-subscribedriver_idwith a differentmax_frequpdates the cap, and re-subscribing without one clears it. Omitted means uncapped. A non-positive or non-numeric value gets anerrorframe with codebad_max_freq.
Server → client
snapshotis sent once on successful subscribe when the orderbook is already cached. Subsequent changes are delivered asupdateframes with an identical payload shape.pendingis sent when the orderbook isn’t in cache yet. A snapshot arrives shortly, followed by updates.errorwith codesubscription_limitis sent when asubscribewould take the connection past 1,000 markets. The whole frame is rejected and none of itsriver_idsare 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 (Pythonwebsockets, 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. Passmax_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 byclient.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.

