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.
Endpoint
wss://api.rivermarkets.com/v1/ws/orders?subaccount_id=<uuid>
Each connection is scoped to exactly one subaccount. To watch orders across multiple subaccounts, open one connection per subaccount. The server pushes a snapshot of currently-open orders on connect, then a frame every time one of that subaccount’s orders changes user-visible status.
Query parameters
| Parameter | Required | Default | Description |
|---|
subaccount_id | yes | — | UUID of the subaccount whose orders you want to stream. Must belong to the authenticated user. |
extra_information | no | false | When true, every snapshot row and order frame additionally carries parent_iceberg_order_id, expiry_ts_utc, and complex_order_ids (TP/SL bucket). Useful for rendering full order rows without a REST round-trip. |
key_id / ts / sig | yes (programmatic) | — | Ed25519 signed-handshake params. See Authentication. |
access_token | yes (browser) | — | Supabase JWT for first-party web clients. |
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.
The official SDKs sign the handshake for you. The raw recipe below is for
integrations without an SDK.
wss://api.rivermarkets.com/v1/ws/orders?subaccount_id=<uuid>&key_id=<uuid>&ts=<unix>&sig=<b64-sig>
The authenticated user must own subaccount_id — otherwise the connection is closed with code 4401. A missing or malformed subaccount_id is closed with 4400.
Protocol
All frames are JSON. Server frames are bytes (orjson-serialized UTF-8). Client frames are accepted but ignored — there is no subscribe/unsubscribe action; the subaccount is fixed at handshake time.
Server → client
{ "type": "connected" }
{ "type": "snapshot", "orders": [ /* open orders for this subaccount */ ] }
{ "type": "order", "id": "...", "status": "pending", /* + full order fields */ }
{ "type": "order", "id": "...", "status": "resting", /* + full order fields */ }
{ "type": "order", "id": "...", "status": "pending_amend", /* + full order fields */ }
{ "type": "order", "id": "...", "status": "partially_filled", /* + full order fields */ }
{ "type": "order", "id": "...", "status": "executed", /* + full order fields */ }
{ "type": "order", "id": "...", "status": "cancelled", /* + full order fields */ }
{ "type": "order", "id": "...", "status": "rejected", /* + full order fields */ }
{ "type": "error", "code": "...", "message": "..." }
{ "type": "reconnect" }
connected is sent immediately after the handshake completes.
snapshot follows once, listing orders currently in pending, resting, or partially_filled for this subaccount. The client always sees state, then deltas — never the other way around.
order frames deliver every user-visible status transition. The internal processing status is never forwarded.
reconnect is sent during graceful pod shutdown. Reconnect with a small jitter.
Statuses
| Status | Meaning |
|---|
pending | Accepted by the API; not yet on the exchange book. |
resting | Live on the exchange book. |
pending_amend | A PATCH /v1/orders/{id} edit request is in flight on the exchange. Transitions back to resting (or a terminal state) once the exchange responds. |
partially_filled | Some quantity has traded; the rest is still resting. |
executed | Fully filled. Terminal. |
cancelled | Cancelled before full execution. Terminal. |
rejected | Rejected by the exchange or risk checks. Terminal. |
Iceberg child tranches arrive as ordinary order frames — clients can’t tell them apart from user-placed simple orders.
Order payload
Default fields (always sent):
{
"id": "8b1f1a2e-…",
"river_id": 12345,
"generic_asset_id": null,
"order_type": "LIMIT",
"time_in_force": "GTC",
"status": "partially_filled",
"qty": 10,
"price": 0.42,
"buy_flag": true,
"traded_qty": 4,
"average_price": 0.42,
"fees_paid": 0.02,
"created_at": "2026-04-29T13:14:15.123Z",
"updated_at": "2026-04-29T13:14:18.456Z",
"reject_reason": null
}
When connected with ?extra_information=true, every row additionally carries:
{
"parent_iceberg_order_id": null,
"expiry_ts_utc": null,
"complex_order_ids": { "tp": [], "sl": [] }
}
average_price is null until the first fill. traded_qty and fees_paid default to 0. complex_order_ids lists attached take-profit / stop-loss conditional-order ids by side; both buckets are empty when nothing is attached. order_type and time_in_force are the canonical enum names (LIMIT, MARKET, GTC, IOC, FOK, …).
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.
Minimal client
Using the SDK (handles signing automatically):
import asyncio
from rivermarkets import AsyncRiverMarkets
async def run(subaccount_id: str):
client = AsyncRiverMarkets(key_id="<uuid>", private_key="<base64-priv>")
async with client.ws.orders(subaccount_id=subaccount_id) as stream:
async for msg in stream:
if msg.type == "order":
print(msg.id, msg.status, msg.traded_qty)
elif msg.type == "snapshot":
print("open orders:", len(msg.orders))
asyncio.run(run("<subaccount-uuid>"))
Or signing the handshake manually:
import asyncio, base64, json, time
from urllib.parse import urlencode, quote
import websockets
from nacl.signing import SigningKey
KEY_ID = "<uuid>"
signing_key = SigningKey(base64.b64decode("<base64-priv>"))
def sign_ws(path: str, extra_params: dict) -> str:
sorted_q = urlencode(sorted(extra_params.items()), quote_via=quote)
ts = str(int(time.time()))
canonical = "\n".join(["WS", path, sorted_q, ts]).encode()
sig = base64.b64encode(signing_key.sign(canonical).signature).decode()
return f"wss://api.rivermarkets.com{path}?{urlencode({**extra_params, 'key_id': KEY_ID, 'ts': ts, 'sig': sig})}"
async def run(subaccount_id: str):
async with websockets.connect(sign_ws("/v1/ws/orders", {"subaccount_id": subaccount_id})) as ws:
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "order":
print(msg["id"], msg["status"], msg.get("traded_qty"))
asyncio.run(run("<subaccount-uuid>"))
Errors
Recoverable problems arrive as an error frame followed by a close. code is stable and safe to branch on; message is human-readable.
code | When |
|---|
unauthorized | API key / JWT was missing or invalid. |
missing_subaccount_id | ?subaccount_id= was not supplied on the handshake. |
invalid_subaccount_id | subaccount_id is not a valid UUID. |
subaccount_forbidden | The subaccount does not belong to the authenticated user. |
snapshot_failed | Initial open-orders snapshot could not be loaded; the connection stays open and live updates resume. |
Close codes
| Code | Meaning |
|---|
1000 | Normal closure |
1011 | Server-side overflow (client couldn’t keep up with the send rate) |
4400 | Bad request (missing/invalid subaccount_id) |
4401 | Missing or invalid authentication, or subaccount does not belong to the user |
4503 | Server draining for deploy — reconnect with jitter |