Predictefy Docs
Browse documentation

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

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
    }),
  );
});

Handle the frames

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.

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.

  • Streaming — the full protocol: auth, close codes, feed tickers
  • Identifiers — venue-native versus unified ids
  • Venue coverage — which venues have a real book at all