> ## Documentation index
> Fetch the complete documentation index at: https://docs.predictefy.com/llms.txt
> Use it to discover every available page before exploring further.

# Stream a live order book

> Subscribe over WebSocket, handle full-state frames, and survive the non-fatal errors.

REST gives you a book at a moment. The WebSocket gives you the book as it changes, and it does
so with one upstream venue subscription shared across every client watching that market — so
scaling consumers does not burn a venue's rate limits.

The protocol reference is [Streaming](/guides/streaming/). This page is the working loop.

## Connect and subscribe

Connect to `/v1/stream` on the WebSocket origin shown in your dashboard, authenticating with the
same `pk_live_…` key as REST.

```js
const ws = new WebSocket(`${WS_ORIGIN}/v1/stream`, {
  headers: { Authorization: `Bearer ${process.env.PREDICTEFY_API_KEY}` },
});

ws.on('open', () => {
  ws.send(
    JSON.stringify({
      op: 'subscribe',
      channel: 'orderbook',
      venue: 'polymarket',
      marketId: OUTCOME_ID, // venue-native — see below
    }),
  );
});
```

:::caution[`marketId` is the venue-native id, not the unified one]
It is passed straight through to the venue. For Polymarket it is the CLOB asset/token id, which
the unified API returns as the outcome's **`outcomeId`** — the same id `fetchOrderBook` takes.
Sending a unified `marketId` here produces a subscription the venue has nothing for: an empty
stream rather than an error. [Identifiers](/guides/market-ids/) covers the general rule.
:::

## Handle the frames

```js
ws.on('message', (raw) => {
  const frame = JSON.parse(raw);

  switch (frame.type) {
    case 'subscribed':
    case 'unsubscribed':
      return;

    // Both carry the FULL book, never deltas. Replace state; do not merge.
    case 'snapshot':
    case 'update':
      book.set(frame.marketId, frame.data);
      return;

    case 'trade':
      onTrade(frame.venue, frame.marketId, frame.data);
      return;

    case 'error':
      // Non-fatal. The socket stays open — do NOT reconnect on these.
      console.warn(`${frame.code}: ${frame.message}`, frame.venue, frame.marketId);
      return;
  }
});
```

Two properties decide the shape of this loop:

**`snapshot` and `update` both carry the full book.** `snapshot` is the first frame after
subscribing and after backpressure coalescing; `update` marks live ticks. Neither is a delta, so
replace your local state rather than merging into it. Merging works until the first coalesced
snapshot, then drifts.

**Protocol errors are non-fatal.** `BAD_MESSAGE`, `NOT_SUPPORTED`, `NOT_SUBSCRIBED` and
`SUBSCRIPTION_LIMIT` arrive as `type: "error"` frames and the socket **stays open**. Treating
them as disconnects produces a reconnect loop against a working connection.

Book frames carry `bids` and `asks` as `{ price, size }`, with prices as probabilities in
`[0, 1]`.

## What you cannot subscribe to

`NOT_SUPPORTED` is a real answer about the world, not a failure:

- Venues without a public stream answer it rather than serving a synthetic one.
- **Trades stream only where the venue has a native fills channel.** Order-book support does not
  imply trade support.
- `subscribeAll` needs a venue-wide firehose upstream; venues without one answer
  `NOT_SUPPORTED`.

Do not fall back to another venue when you get this. The honest answer is that this venue does
not offer that stream — see [Capability-honest data](/guides/honest-data/).

## Subscription limits

Active logical subscriptions are capped by plan: **2** on Free, **20** on Builder, **100** on
Pro, **500** on Scale. A lower service safety cap can also apply.

Exceeding either returns a non-fatal `SUBSCRIPTION_LIMIT` — the socket survives and the
subscription simply does not exist. Track your own count; a subscribe that "succeeded" because
the socket stayed open is not the same as a subscribe that took effect. Wait for the
`subscribed` ack.

## Falling back to REST

For a point-in-time book, or for any venue without a stream, `fetchOrderBook` takes an
`outcomeId` and a `limit` of up to 1000 depth levels. It also reads the archive: `at` for the
nearest stored snapshot to a time, `since`/`until` for a range, and `side` to return only bids
or only asks.

## Related

- [Streaming](/guides/streaming/) — the full protocol: auth, close codes, feed tickers
- [Identifiers](/guides/market-ids/) — venue-native versus unified ids
- [Venue coverage](/reference/venues/) — which venues have a real book at all
