Skip to main content

Endpoint

A single WebSocket connection can subscribe to any number of 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.

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.
  • reconnect is sent during graceful pod shutdown. Reconnect with a small jitter.

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

Orderbook updates are not rate-capped — every upstream change is forwarded as it happens. If your client falls behind, pending updates are coalesced per market (only the latest state is kept), so you never see stale data. 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