NEW: Live arbitrage across 10+ prediction markets.Arbitrage →
← Index
APIAug 23, 20269 min read

How to Stream Live Prediction Market Prices With a WebSocket API

How to Stream Live Prediction Market Prices With a WebSocket API

The Short Answer

Open a WebSocket to /v1/stream, authenticate with an Authorization: Bearer header or an auth frame within ten seconds, then send {"op":"subscribe","channel":"orderbook","venue":"polymarket","marketId":"..."}. You get a snapshot frame first, then update frames on every tick. One connection covers 15+ venues. The free API key includes two concurrent streams, and streaming costs 2 credits per connection-minute.

Polling a prediction market order book is the wrong tool. By the time your next request lands the book has moved, and the faster you poll the sooner you hit a rate limit. Streaming inverts that: you connect once and the venue pushes changes to you.

The complication is that every venue speaks a different dialect. Kalshi wants a key just to connect, SX Bet and Myriad run Centrifugo, Polymarket uses its own channel names. This walks through streaming all of them through one socket, with the frame shapes, the close codes, and the one identifier mistake that breaks most first integrations.

Key Takeaways

  • One WebSocket at /v1/stream covers 15+ venues, so you write one client rather than five.
  • Authenticate with a Bearer header, or send an auth frame within ten seconds or the socket closes with code 4001.
  • snapshot is the full book and arrives first. update is a live tick. Treat them differently or your book will drift.
  • On Polymarket the marketId is the CLOB token id of the outcome, not the market id. This is the single most common integration error.
  • Protocol errors are non-fatal and leave the socket open. Close codes in the 4000s are fatal and each means something specific.

What You Need Before You Start

An API key, and that is genuinely it. The free key includes a monthly credit allowance, two concurrent WebSocket streams and one key. That is enough to build and test a real client.

Streaming is metered by time rather than by message: 2 credits per connection-minute, prepaid, with the first minute charged at connect and each later minute charged on the minute. A socket you forget to close is a socket you are paying for, which matters more than it sounds when you are developing and leaving processes running.

Connect and Authenticate

There are two ways to authenticate, and which you use depends on where the code runs.

import WebSocket from 'ws';

const ws = new WebSocket(
  'wss://<your-stream-origin>/v1/stream',
  { headers: { Authorization: `Bearer ${process.env.PREDICTEFY_API_KEY}` } }
);

ws.on('open', () => console.log('connected'));
What This Does

Server-side clients should use the Authorization header, which is the preferred method. The stream origin is shown in your developer dashboard rather than hardcoded here, because it can differ per environment.

Browsers cannot set headers on a WebSocket. From the browser you connect first and authenticate with a frame.

const ws = new WebSocket('wss://<your-stream-origin>/v1/stream');

ws.onopen = () => {
  ws.send(JSON.stringify({ op: 'auth', apiKey: 'pk_live_...' }));
};
What This Does

The auth frame has to arrive within ten seconds or the server closes the connection with code 4001. Note that this puts a live key in client-side code, so for anything public you want a thin proxy on your own server rather than shipping the key to the browser.

Subscribe to an Order Book

Subscriptions are single JSON frames. The three you will use most:

OperationWhat it gives you
subscribe with channel orderbookBids and asks for one market on one venue
subscribe with channel tradesExecutions as they happen on one market
subscribeAllEvery market on a venue for that channel
ws.send(JSON.stringify({
  op: 'subscribe',
  channel: 'orderbook',
  venue: 'polymarket',
  marketId: '<clob-token-id>'
}));
What This Does

Asks for one market's book on one venue. The server replies with a subscribed acknowledgement, then a snapshot, then update frames. Swap venue for kalshi, limitless, sxbet or any other supported venue and nothing else about your client changes. That is the point of a normalized stream.

Reading the Frames

Every message is JSON with a type. Handle it as a switch, not as a series of guesses.

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

  switch (msg.type) {
    case 'subscribed':
      break;

    case 'snapshot':
      book.set(msg.marketId, msg.data);
      break;

    case 'update':
      applyUpdate(book.get(msg.marketId), msg.data);
      break;

    case 'trade':
      onTrade(msg.venue, msg.marketId, msg.data);
      break;

    case 'error':
      console.warn(msg.code, msg.message);
      break;
  }
});
What This Does

The distinction that matters is snapshot against update. A snapshot is the complete book and arrives on first subscribe and again after the server coalesces under backpressure. An update is an incremental tick. If you apply a snapshot as though it were an update your book will silently drift, and you will not notice until a price looks impossible.

A book frame carries bids and asks as price and size pairs, plus a venue timestamp in data.timestamp and a server timestamp in ts. Use the venue timestamp when you care about what the market did, and the server timestamp when you care about what your client knew.

The Polymarket Identifier Trap

This is the error that costs people an afternoon, so it deserves its own section.

On Polymarket, the marketId you subscribe with is the CLOB asset or token id of the outcome, which the API exposes as outcomeId. It is not the market-level id, even though that is the field called marketId elsewhere in your code.

A Polymarket market with a Yes and a No outcome has two token ids. Subscribing with the market id gets you nothing useful, and the failure is quiet rather than loud. Pull the outcome ids from the market catalog and subscribe to the one you actually want to watch.

Errors and Close Codes

There are two failure classes and conflating them will make your reconnect logic wrong.

Protocol errors are non-fatal. BAD_MESSAGE, NOT_SUPPORTED, NOT_SUBSCRIBED and SUBSCRIPTION_LIMIT arrive as an error frame and the socket stays open. Log them and carry on. Do not reconnect.

Close codes are fatal and each tells you what to do next.

CodeMeaningWhat to do
4001UnauthorizedFix the key. Do not retry in a loop
4002Insufficient creditsStop and top up. Retrying burns nothing but time
4003Platform unavailableBack off and retry
4004Connection limitClose an existing socket first
4008Rate limitedToo many failed auth attempts. Back off hard

The one that catches people in development is 4004. The free key allows two concurrent streams, and a crashed process that did not close its socket cleanly still counts until the server notices.

Skipping the Protocol With the SDK

Everything above is the raw protocol, worth understanding because it is what your client is actually doing. For most work the SDK is less code and handles reconnects for you.

const close = client.watchFeedTicker(
  { feed: 'binance', symbol: 'BTC/USDT' },
  (ticker) => console.log(ticker.last, ticker.sourceMetadata?.transport)
);

// later
close();
What This Does

Subscribes to a reference price feed and returns a function that closes the subscription. Since streaming is billed per connection-minute, calling the returned closer when you are finished is the difference between a cheap integration and an expensive one.

There is an equivalent for cross-venue arbitrage, client.watchArbitrage(), backed by the subscribeArbitrage operation. It pushes a frame of rows whenever the shared arbitrage surface changes, which is a different job from watching one book and worth a separate look.

Why One Socket Instead of Five

The alternative to this is writing a client per venue. Kalshi needs a key just to connect, SX Bet and Myriad run Centrifugo, Polymarket has its own channel naming, and each has separate reconnect semantics and its own idea of what a book update looks like. That is five integrations to build and five to keep working when a venue changes something.

Predictefy normalizes those behind one connection across 15+ venues, so the frame you handle for Polymarket is the frame you handle for Kalshi. The API key is free to start and includes a monthly credit allowance and two concurrent streams, which is enough to build and test against before any of this costs anything.

Frequently Asked Questions

How do I stream live prediction market prices?

Open a WebSocket to the streaming endpoint, authenticate with a Bearer header or an auth frame, then send a subscribe operation naming the channel, venue and market. Predictefy's free API key covers 15+ venues through one connection, so you write a single client rather than one per venue.

What is the difference between a snapshot and an update frame?

A snapshot is the complete order book and arrives when you first subscribe, and again after the server coalesces frames under backpressure. An update is an incremental change. Applying a snapshot as though it were an update makes your local book drift silently, which is the most common source of wrong prices.

Why is my Polymarket WebSocket subscription returning nothing?

Almost certainly the wrong identifier. On Polymarket the marketId for a subscription is the CLOB asset or token id of the specific outcome, exposed as outcomeId, not the market-level id. Each market has one per outcome, so pull the right token id from the catalog first.

Do I need a paid plan to use the streaming API?

No. Predictefy's free API key includes a monthly credit allowance and two concurrent WebSocket streams, which is enough to build and test a real client. Streaming meters at 2 credits per connection-minute, prepaid, so closing sockets you are not using matters more than message volume.

Should I reconnect when I get an error frame?

No. Protocol errors such as BAD_MESSAGE, NOT_SUPPORTED and SUBSCRIPTION_LIMIT arrive as error frames and leave the socket open, so reconnecting is wasted work. Only a close code in the 4000s is fatal, and 4001 and 4002 should never be retried in a loop.

Conclusion

A working streaming client is smaller than most people expect: connect, authenticate, subscribe, then a switch on frame type. The parts that actually cause trouble are the ones that fail quietly, and there are three. Treating a snapshot as an update. Subscribing to Polymarket with the market id rather than the outcome token id. Reconnecting on errors that never closed the socket.

Get those three right and the rest is plumbing. One connection, 15+ venues, one frame shape to handle.

One housekeeping note: this is information, not financial advice. Endpoints, credit costs and venue coverage change, so confirm anything that matters against the current developer documentation before you rely on it.