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

# Tradeprints

> Subscribe to the live trade tape (every print) for one or more markets over a single WebSocket connection.

## Endpoint

```
wss://api.rivermarkets.com/v1/ws/tradeprints
```

A single WebSocket connection can subscribe to any number of markets. The server pushes one frame per trade print, in real time. There is no snapshot on subscribe — see [Backfill](#backfill).

## 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/tradeprints?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`.

## Backfill

The WebSocket stream is **live-only** — it does not emit historical trades on subscribe. To populate a recent trades view on page load, call:

```
GET https://api.rivermarkets.com/v1/tradeprints?river_ids=12345&river_ids=67890&limit=50
```

This REST endpoint accepts one or more `river_ids` (repeat the parameter), queries each source exchange sequentially, and returns the most recent trades for every market in descending time order. The response is `{ "results": [...] }` with one block per requested `river_id` in input order. Per-market failures show up as `not_found` rows with a `message`. Fire it in parallel with the WebSocket `subscribe` action and deduplicate overlap by `exchange_trade_id`.

## 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": "trade", "river_id": 12345, "data": { /* tradeprint */ } }
{ "type": "error", "code": "bad_json" | "bad_action" | "bad_river_id" | "forbidden", "message": "..." }
{ "type": "reconnect" }
{ "type": "connected" }
```

* `trade` is sent once per print. Append-only — frames are never coalesced.
* `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.

### Tradeprint payload

```json theme={null}
{
  "exchange_timestamp": "2026-04-19T15:22:01.123+00:00",
  "price": 0.42,
  "qty": 150,
  "aggressor_buy_flag": true,
  "exchange_trade_id": "kalshi-abc-123"
}
```

| Field                | Type                      | Notes                                                                                       |
| -------------------- | ------------------------- | ------------------------------------------------------------------------------------------- |
| `exchange_timestamp` | string (ISO-8601) \| null | Exchange-reported trade time.                                                               |
| `price`              | number                    | Trade price, normalised to `0–1` in YES terms.                                              |
| `qty`                | number                    | Trade size in contracts.                                                                    |
| `aggressor_buy_flag` | boolean \| null           | `true` if the taker bought (hit an ask). `false` if the taker sold. `null` if unknown.      |
| `exchange_trade_id`  | string \| null            | Exchange-assigned trade id. Use it to deduplicate REST backfill against the live WS stream. |

## Rate limits and pacing

Trades are append-only events — they are **not** rate-capped or coalesced. Every print on the upstream exchange is forwarded. If a slow client cannot drain fast enough, the server closes the connection with code `1011`; reconnect and re-backfill.

## Minimal client

Using the SDK (handles REST + WS signing automatically):

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

async def run(river_id: int):
    client = AsyncRiverMarkets(key_id="<uuid>", private_key="<base64-priv>")

    # 1. Backfill recent trades.
    backfill = await client.tradeprints.list_tradeprints(river_ids=[river_id], limit=50)
    trades = backfill.results[0].trades
    seen = {t.exchange_trade_id for t in trades if t.exchange_trade_id}
    for t in trades:
        print("backfill", t.exchange_timestamp, t.price, t.qty)

    # 2. Subscribe to the live stream.
    async with client.ws.tradeprints() as stream:
        await stream.subscribe([river_id])
        async for msg in stream:
            if msg.type != "trade":
                continue
            tid = msg.data.exchange_trade_id
            if tid and tid in seen:
                continue
            if tid:
                seen.add(tid)
            print("live", msg.data.exchange_timestamp, msg.data.price, msg.data.qty)

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                |
