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

# Tape - live WebSocket

> Channel-multiplexed live stream of options trades across all tape venues, with API-key auth, per-tier connection caps and slow-consumer protection.

`wss://api.backquant.com/v2/ws/options`

Single connection, multiple subscriptions. The same canonical trade payload from the REST tape, pushed sub-second after the upstream venue emits it.

## Authentication

Pass your API key on the WebSocket handshake - either of:

* **Query parameter** - `wss://api.backquant.com/v2/ws/options?api_key=bq_live_...` (easiest from browsers).
* **`Authorization: Bearer` header** - preferred from server-to-server clients.
* **`X-API-Key` header** - accepted as a fallback for clients that can set arbitrary handshake headers.

The handshake validates the key, subscription status and tier eligibility. Failures close the connection with a `1008` policy-violation code and a human-readable reason.

## Channel inventory

| Channel               | Payload                                                        | Frequency                       |
| --------------------- | -------------------------------------------------------------- | ------------------------------- |
| `tape.{coin}.{venue}` | One trade per frame                                            | Per trade (\~10-100/s in burst) |
| `tape.{coin}.agg`     | Same trade payload, aggregated across all venues for that coin | Per trade                       |
| `gex.levels.{SYMBOL}` | FLOW-signed GEX levels (all-expiry + 0DTE HVL, walls, spot)    | When levels update (\~30 s)     |
| `status.heartbeat`    | Server liveness + per-client queue stats                       | Every 15 s                      |

`{coin}` is typically `BTC` or `ETH` (SOL/HYPE appear when tape exists).
`{venue}` is one of `deribit`, `bybit`, `okx`, `binance`, `derive`,
`thalex`, `delta_india`, `delta` (case-insensitive on subscribe; the
server normalises to lowercase). `{SYMBOL}` is `BTCUSDT`, `ETHUSDT`,
`SOLUSDT`, or `HYPEUSDT`. See [Venue coverage](/concepts/venues) for
what each venue ships.

**Levels note (changed in 2.6.0):** `gex.levels.*` pushes **flow**-signed
levels - the same model and the same builder as REST
[`/v2/gex/levels`](/api/v2/gex/levels), so the two surfaces always agree.
Before 2.6.0 this channel pushed a **std** (textbook) snapshot while REST
defaulted to flow.

When the aggressor tape is unavailable for a symbol the payload falls back
to the worker's std snapshot and says so: `positioning: "std"` plus a
`fallback_reason`. Always read `positioning` before overlaying the numbers
on anything else. See [Positioning](/concepts/positioning).

On subscribing to `gex.levels.*` the server sends the **current levels
immediately**, flagged `"snapshot": true`, then pushes again only when the
levels move. Without that first frame you could not tell an idle book from a
subscription that never took.

Max **48 channels per connection**.

## Protocol

### Server → client envelope

Every server frame is a JSON object with an `event` field:

```json theme={null}
{ "event": "welcome",       "ts": "...", "subscription_tier": "enterprise",
                            "client_id": "abc...", "max_channels": 48,
                            "available_channels": [...] }

{ "event": "subscribed",    "ts": "...", "channels": [...], "added": [...] }
{ "event": "unsubscribed",  "ts": "...", "channels": [...], "removed": [...] }

{ "event": "trade",         "ts": "...", "channel": "tape.BTC.deribit",
                            "data": { ...trade payload... } }

{ "event": "levels",        "ts": "...", "channel": "gex.levels.BTCUSDT",
                            "data": { "symbol": "BTCUSDT", "spot_price": ...,
                                      "all_expiry": { "hvl": ..., "call_resistance": ...,
                                                      "put_support": ...,
                                                      "call_wall_2": null, "call_wall_3": null,
                                                      "put_wall_2": null, "put_wall_3": null },
                                      "odte": { "hvl": ..., "call_resistance": ...,
                                                "put_support": ... },
                                      "positioning": "flow", "fallback_reason": null,
                                      "ts": "...", "positioning_note": "..." } }

{ "event": "heartbeat",     "ts": "...", "channel": "status.heartbeat",
                            "queue": { "size": 7, "capacity": 1000 },
                            "channels": ["tape.BTC.agg", ...] }

{ "event": "pong",          "ts": "..." }
{ "event": "error",         "ts": "...", "detail": "...",
                            "invalid": [...], "available_channels": [...] }
{ "event": "slow_consumer", "ts": "...", "detail": "Queue full for >5s ..." }
```

### Client → server commands

```json theme={null}
{ "action": "subscribe",   "channels": ["tape.BTC.agg", "gex.levels.BTCUSDT"] }
{ "action": "unsubscribe", "channels": ["tape.BTC.deribit"] }
{ "action": "ping" }
```

Each command receives a corresponding ack frame.

### Trade payload

```json theme={null}
{
  "venue":          "deribit",
  "coin":           "BTC",
  "instrument":     "BTC-30JUN26-100000-C",
  "trade_id":      "12345",
  "direction":      "buy",
  "option_type":    "call",
  "strike":         100000.0,
  "expiry_date":    "2026-06-30",
  "amount":         1.5,
  "price":          0.0525,
  "mark_price":     0.0530,
  "index_price":    67234.10,
  "iv":             65.4,
  "premium_usd":    5293.60,
  "is_block_trade": false,
  "ts_ms":          1748480000000
}
```

| Field         | Meaning                                                                                        |
| ------------- | ---------------------------------------------------------------------------------------------- |
| `venue`       | `deribit` \| `bybit` \| `okx` \| `binance` \| `derive` \| `thalex` \| `delta_india` \| `delta` |
| `amount`      | **Coin-equivalent** size (normalized at ingest)                                                |
| `price`       | Venue-quoted price (units vary by venue)                                                       |
| `premium_usd` | Canonical USD premium - **rank and filter on this**                                            |

`amount` and `premium_usd` are comparable across venues. For Delta ingest
details see [Venue coverage → Delta units](/concepts/venues#delta-exchange-units).

## Connection lifecycle

1. **Connect** with API key → server validates → counts connection against your per-tier cap.
2. **Receive `welcome`** → sanity-check `subscription_tier` and `available_channels`.
3. **Send `subscribe`** → server replies with `subscribed` ack + immediately starts streaming matching trades.
4. **Stream** - trades arrive on their channel; `status.heartbeat` arrives every 15 s if subscribed.
5. **Heartbeat liveness** - the server expects at least one client frame (subscribe / unsubscribe / ping) within 60 s. Otherwise it closes with code `1001`.
6. **Slow consumer** - each client has a 1000-message send buffer. If full for > 5 s the server sends a `slow_consumer` event then closes with code `1013`. Reduce your subscription set or process frames faster.
7. **Disconnect** → connection counter decrements; reconnect any time.

## Per-tier connection caps

| Level      | Concurrent WS connections |
| ---------- | ------------------------- |
| Starter    | 1                         |
| Standard   | 2                         |
| Pro        | 5                         |
| Enterprise | up to 25 (custom)         |

Exceeding the cap on connect returns close code `1008` with a
connection-cap message. WS usage does **not** draw down monthly REST
quota - see [Rate limits](/concepts/rate-limits).

## Reconnect template

The server has no replay - `$` cursor is used on the underlying Redis stream, so a reconnect resumes with whatever's live, not whatever you missed. For zero-gap consumers, also poll [`/v2/tape?after=<last_ts>`](/api/v2/tape/tape) on reconnect to fetch trades that arrived during the gap.

```python theme={null}
import asyncio, json, os, random, websockets

URL = "wss://api.backquant.com/v2/ws/options?api_key=" + os.environ["BQ_API_KEY"]
CHANNELS = ["tape.BTC.agg", "status.heartbeat"]

async def run():
    attempt = 0
    while True:
        try:
            async with websockets.connect(URL, ping_interval=20) as ws:
                await ws.recv()  # welcome
                await ws.send(json.dumps({"action": "subscribe", "channels": CHANNELS}))
                attempt = 0
                async for raw in ws:
                    msg = json.loads(raw)
                    if msg.get("event") == "trade":
                        handle(msg["data"])
        except (websockets.ConnectionClosed, ConnectionError) as e:
            attempt += 1
            backoff = min(60, 2 ** attempt) * (1 + random.uniform(-0.2, 0.2))
            print(f"reconnect in {backoff:.1f}s ({e})")
            await asyncio.sleep(backoff)

def handle(trade):
    print(trade["venue"], trade["instrument"], trade["direction"], f"${trade['premium_usd']:,.0f}")

asyncio.run(run())
```

Full working client: [`scripts/v2_ws_demo/ws_tape_demo.py`](https://github.com/backquant/backquant-terminal/blob/main/scripts/v2_ws_demo/ws_tape_demo.py).

## See also

<CardGroup cols={2}>
  <Card title="REST tape with filters" href="/api/v2/tape/tape" icon="filter" />

  <Card title="Tape overview" href="/api/v2/tape/overview" icon="circle-info" />

  <Card title="GEX levels (REST, flow default)" href="/api/v2/gex/levels" icon="chart-simple" />

  <Card title="Authentication" href="/authentication" icon="key" />
</CardGroup>
