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

# WebSocket API

> Stream live Hyperliquid and Lighter market data, or replay a stored window in order, over authenticated WebSocket connections.

Every channel, live or replayed, uses `wss://api.0xarchive.io/ws`, except the live [`mempool`](/websocket/mempool) channel, which is served only on `wss://stream.0xarchive.io/ws` with the same key and protocol. If one snapshot or a bounded window answers the question, the [REST API](/rest-api) is simpler to retry.

## Before you connect

* An API key, sent in the opening handshake as `Authorization: Bearer $OXARCHIVE_API_KEY`. The Free plan works, and replays the most recent 30 days.
* A server-side runtime. Browsers cannot set the `Authorization` header, so a browser app connects through your backend. Keep the key out of browser code, logs, and shared notebooks.
* For the example below, Node.js 18 or later and the `ws` package: `npm install ws`.

## Subscribe to one channel

Save this as `subscribe.mjs` and run `node subscribe.mjs`:

```javascript theme={"theme":"github-dark"}
import WebSocket from "ws";

const apiKey = process.env.OXARCHIVE_API_KEY;
if (!apiKey) throw new Error("Set OXARCHIVE_API_KEY");

const ws = new WebSocket("wss://api.0xarchive.io/ws", {
  headers: { Authorization: `Bearer ${apiKey}` }
});

ws.on("open", () => {
  ws.send(JSON.stringify({ op: "subscribe", channel: "orderbook", symbol: "BTC" }));
});

ws.on("message", (raw) => {
  const message = JSON.parse(raw);
  if (message.type === "subscribed") console.log("subscribed", message.channel, message.symbol);
  if (message.type === "data") console.log(message.symbol, message.data.levels[0][0], message.data.levels[1][0]);
  if (message.type === "error") console.error(message.error_code, message.message);
});

ws.on("close", (code, reason) => console.log("closed", code, String(reason)));
```

The server first confirms the subscription:

```json theme={"theme":"github-dark"}
{
  "type": "subscribed",
  "channel": "orderbook",
  "coin": "BTC",
  "symbol": "BTC"
}
```

Then each update arrives as a `data` message. `levels` holds bids, then asks, up to 20 per side (one each shown here):

```json theme={"theme":"github-dark"}
{
  "type": "data",
  "channel": "orderbook",
  "coin": "BTC",
  "symbol": "BTC",
  "data": {
    "coin": "BTC",
    "time": 1791287222643,
    "levels": [
      [{ "px": "86303.0", "sz": "17.36307", "n": 55 }],
      [{ "px": "86304.0", "sz": "0.99133", "n": 11 }]
    ]
  }
}
```

<Warning>
  An open socket only means the key was accepted; a wrong key fails the handshake with HTTP `401`. Wait for `subscribed` after `subscribe`, or `replay_started` after `replay`, and handle `error`: an unknown channel, for example, returns an `error` message with `error_code` `invalid_parameter`.
</Warning>

## Stream live or replay history

The same socket does both. Each command names its channel and symbol.

### Live streaming

The example above is a live subscription; `unsubscribe` takes the same channel and symbol. [Channels](/websocket/channels) lists every channel with its live and replay availability and what each message does to your local state, [Real-time streams](/websocket/real-time) covers ordering and local books, and [L4 order book](/websocket/l4-orderbook) covers stateful L4 books built from a snapshot and diffs. For Hyperliquid channels and depth choices, see [Hyperliquid WebSocket streaming](/hyperliquid-websocket-streaming-api).

### Historical replay

Send a `replay` command with a window in Unix milliseconds. Save this as `replay.mjs` and run `node replay.mjs`. It replays five minutes of BTC trades from an hour ago, inside the Free plan's 30 days, and takes about 30 seconds at `speed` 10:

```javascript theme={"theme":"github-dark"}
import WebSocket from "ws";

const apiKey = process.env.OXARCHIVE_API_KEY;
if (!apiKey) throw new Error("Set OXARCHIVE_API_KEY");

const ws = new WebSocket("wss://api.0xarchive.io/ws", {
  headers: { Authorization: `Bearer ${apiKey}` }
});

const end = Date.now() - 60 * 60 * 1000; // one hour ago
const start = end - 5 * 60 * 1000;       // five minutes earlier
let rows = 0;

ws.on("open", () => {
  ws.send(JSON.stringify({ op: "replay", channel: "trades", symbol: "BTC", start, end, speed: 10 }));
});

ws.on("message", (raw) => {
  const message = JSON.parse(raw);
  if (message.type === "replay_started") {
    console.log("replaying", message.channel, message.symbol, "at", `${message.speed}x`);
  } else if (message.type === "historical_data") {
    rows += 1;
    if (rows === 1) console.log("first fill", new Date(message.timestamp).toISOString(), message.data.price, message.data.size);
  } else if (message.type === "gap_detected") {
    console.warn("gap from", message.gap_start, "to", message.gap_end);
  } else if (message.type === "replay_completed") {
    console.log("replay completed:", rows, "fill rows");
    ws.close();
  } else if (message.type === "error") {
    // The server refused the command, for example a window outside your plan's history.
    console.error(message.error_code, message.message);
    ws.close();
  }
});

// A connection failure, such as a rejected key, arrives as the socket's own error event.
ws.on("error", (err) => console.error("connection failed:", err.message));
```

It prints the speed the replay runs at, the first fill, and the number of fill rows when the window ends. Two kinds of error reach the client. A failed connection, such as a rejected key, fires the socket's own `error` event with the HTTP status of the handshake. A refused command arrives as a message with `type` `error` and a stable `error_code`, and the socket stays open.

Standard replay sends `replay_started`, then `historical_data` messages in order, `gap_detected` where the window has an interruption, and `replay_completed`. A multi-channel replay sends `replay_snapshot` baselines before its timeline. L4 and full-depth replay sends `replay_started`, one `l4_snapshot`, ordered `l4_batch` messages, and `replay_completed`; it ignores `speed`, and `replay.seek` returns an error. [Replay](/websocket/replay) covers each delivery model and the pause, resume, seek, and stop controls. To prepare a backtest from REST windows or replay, see [Prepare data for a reproducible backtest](/guides/point-in-time-backtesting).

## Commands and events

Client commands use `op`: `subscribe`, `unsubscribe`, `replay`, `replay.pause`, `replay.resume`, `replay.seek`, `replay.stop`, and `ping`. Server messages use `type`, and the names differ: the command `replay.pause` is answered by the event `replay_paused`. The older `stream` and `stream.stop` commands return an `error`; use [Bulk Exports](/data-catalog) for large files. The [Protocol reference](/websocket/schema) has every command and event shape.

## Channels and limits

Which channels stream live, which replay, and how, is on [Channels](/websocket/channels), venue family by venue family. Subscription, connection, and replay-speed limits per plan are on [Limits](/websocket/limits).

## Measure your own latency

Current WebSocket health is an HTTP service check, not a measurement of your connection. These are separate numbers: HTTP service health, server-side REST handler timing, stored-data freshness, ingest lag, WebSocket upgrade/connect time, application ping/pong round-trip time, and client processing lag. In the data-quality response, `websocket.current_ms` is the latest stored order-book freshness value, not client WebSocket round-trip time.

To measure your own path, send `{"op":"ping"}` and time the `{"type":"pong"}` reply. The result describes your client and network.

## Next step

<Card title="Connect a production client" icon="plug" href="/websocket/connection">
  Authenticate, keep the socket alive, and reconnect and resubscribe cleanly.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.