> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rivermarkets.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Orderbooks

> Subscribe to live orderbook updates for one or more markets over a single WebSocket connection.

## 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

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](/api-reference/authentication) for the full recipe.

<Tip>
  The official SDKs sign the handshake for you.
</Tip>

```text Signed handshake theme={null}
wss://api.rivermarkets.com/v1/ws/orderbooks?key_id=<uuid>&ts=<unix>&sig=<b64-sig>
```

For first-party web clients you can authenticate with a Supabase JWT instead:
`?access_token=<jwt>`. 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

```json theme={null}
{ "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

```json theme={null}
{ "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

```json theme={null}
{
  "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

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):

```python theme={null}
import asyncio
from rivermarkets import AsyncRiverMarkets

async def run(river_ids: list[int]):
    client = AsyncRiverMarkets(key_id="<uuid>", private_key="<base64-priv>")
    async with client.ws.orderbooks() as stream:
        await stream.subscribe(river_ids)
        async for msg in stream:
            if msg.type in ("snapshot", "update"):
                print(msg.river_id, msg.data.best_bid_price, msg.data.best_ask_price)

asyncio.run(run([12345]))
```

Or signing the handshake manually:

```python theme={null}
import asyncio, base64, json, time
from urllib.parse import urlencode
import websockets
from nacl.signing import SigningKey

KEY_ID = "<uuid>"
signing_key = SigningKey(base64.b64decode("<base64-priv>"))


def sign_ws(path: str) -> str:
    ts = str(int(time.time()))
    canonical = "\n".join(["WS", path, "", ts]).encode()
    sig = base64.b64encode(signing_key.sign(canonical).signature).decode()
    return f"wss://api.rivermarkets.com{path}?{urlencode({'key_id': KEY_ID, 'ts': ts, 'sig': sig})}"


async def run(river_ids: list[int]):
    async with websockets.connect(sign_ws("/v1/ws/orderbooks")) 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([12345]))
```

## Close codes

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