Skip to main content

Endpoint

wss://api.rivermarkets.com/v1/ws/orderbooks
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

Authenticate with an API key. Create one from your dashboard.
GET /v1/ws/orderbooks HTTP/1.1
Host: api.rivermarkets.com
x-api-key: <your-api-key>
Upgrade: websocket
Browsers can’t set custom headers on the WebSocket handshake; use the query-parameter form in the browser. Invalid or missing auth closes the connection with close code 4401.

Protocol

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

Client → server

{ "action": "subscribe",   "river_ids": [12345, 67890] }
{ "action": "unsubscribe", "river_ids": [12345] }
  • subscribe / unsubscribe accept one or many river_ids. Duplicate subscribes on the same connection are no-ops.

Server → client

{ "type": "snapshot", "river_id": 12345, "data": { /* orderbook */ } }
{ "type": "update",   "river_id": 12345, "data": { /* orderbook */ } }
{ "type": "pending",  "river_id": 12345, "message": "subscription initiated" }
{ "type": "error",    "code": "bad_json" | "bad_action" | "bad_river_id" | "forbidden", "message": "..." }
{ "type": "reconnect" }
  • 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

{
  "river_id": 12345,
  "bids": [{ "price": 0.42, "qty": 1500 }, ...],
  "asks": [{ "price": 0.43, "qty": 2200 }, ...],
  "best_bid_price": 0.42,
  "best_ask_price": 0.43,
  "exchange_timestamp": "2026-04-19T15:22:01.123456+00:00",
  "is_valid": true
}
is_valid=false indicates a transient crossed-book state at the exchange. The snapshot that follows will have fresh values.

Rate limits and pacing

Updates for any single (connection, river_id) are capped at 20 Hz. Faster upstream changes are coalesced, keeping only the latest pending state per market. Your client will never see stale data — only lower-frequency updates.

Minimal client

import asyncio
import json
import websockets

async def run(api_key: str, river_ids: list[int]):
    async with websockets.connect(
        "wss://api.rivermarkets.com/v1/ws/orderbooks",
        additional_headers={"x-api-key": api_key},
    ) as ws:
        await ws.send(json.dumps({"action": "subscribe", "river_ids": river_ids}))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("type") in ("snapshot", "update"):
                print(msg["river_id"], msg["data"]["best_bid_price"], msg["data"]["best_ask_price"])

asyncio.run(run("<your-api-key>", [12345]))

Close codes

CodeMeaning
1000Normal closure
1011Server-side overflow (client couldn’t keep up with the send rate)
4401Missing or invalid authentication
4503Server draining for deploy — reconnect with jitter