Streaming

Keep one connection open and receive an event's prices whenever they change. Pro plans only.

Pro only

Streaming needs a Pro key. Other plans are refused with 403 Plan restriction. See Pricing.

What the stream is

wss://api.sockodds.com/v1/stream/odds is a WebSocket that checks prices for you every 15 seconds and sends them when they change. Each update is the same body the matching odds route returns, so code that reads /v1/odds reads the stream too.

It saves you polling, not time: prices are the same ones the REST routes serve, refreshed every 15 seconds. It is pre-match first and not a low-latency in-play feed.

Connect

Open the WebSocket with your key in the Authorization: Bearer header, as on every request. Choose exactly one of:

ParameterStreams
event_idOne event, like GET /v1/events/{event_id}/odds.
leagueUp to limit events in one league, like GET /v1/odds?league=.... limit is 10 by default and at most 20.

You can add markets, bookmakers, regions, alternates and group_by, which work exactly as they do on the odds routes. Snapshots are grouped by market unless you add group_by=bookmaker. Narrow filters keep each update small.

Browsers cannot set headers on a WebSocket, and your key must stay on your server anyway, so connect from your own server and pass updates on to your pages. Node 22 and later has a WebSocket client built in that accepts headers.

This program connects to the Rugby League World Cup opener, prints a line for each event in the first snapshot and closes:

const socket = new WebSocket("wss://api.sockodds.com/v1/stream/odds?event_id=ev_000ckk0nc1tg&markets=winner", {
  headers: { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` },
});
socket.addEventListener("message", (message) => {
  const frame = JSON.parse(message.data);
  if (frame.type === "heartbeat") return; // nothing changed since the last snapshot
  if (frame.type === "error") {
    console.log(`${frame.status} ${frame.problem.title}: ${frame.problem.detail}`);
    return; // the server closes the connection next
  }
  // A snapshot: one event for event_id, a list of events for league.
  const events = Array.isArray(frame.data) ? frame.data : [frame.data];
  for (const event of events) {
    const prices = event.markets.flatMap((m) => m.outcomes.flatMap((o) => o.prices));
    const bookmakers = new Set(prices.map((p) => p.bookmaker));
    console.log(`${event.name}: ${prices.length} prices from ${bookmakers.size} bookmakers`);
  }
  socket.close();
});

Output from a local copy of the API with invented test prices

Australia v New Zealand: 8 prices from 4 bookmakers

Frames

Every message is one JSON object with a type:

typeWhenBody
snapshotOn connect, then whenever the prices change.The odds route's body, data, meta and links, with type added.
heartbeatA refresh found nothing changed.at: when the check ran, in UTC.
errorA refresh was refused.status and problem, the same problem object a REST request gets. The connection closes straight after.

The first snapshot arrives as soon as you connect. After that, each refresh sends either a new snapshot or a heartbeat. A snapshot is the full current state, not a list of changes, so replace what you held rather than merging into it.

The stream is read only. Do not send messages: any message from your side closes the connection.

Limits

LimitValue
Refresh interval15 seconds
Open streams per key2
Longest connection3,600 seconds
Events per league snapshot20
Largest snapshot8,000,000 bytes
Largest unread backlog1,000,000 bytes

A connection that would go over the per-key limit is refused before it opens with 429 Stream limit reached. Bad parameters are refused with 400, and a plain HTTP request to the stream gets 426 Upgrade required. A WebSocket client usually reports these only as a failed connection, so check your parameters and plan against the REST routes first.

Billing

  • The first snapshot, and every new snapshot after it, counts towards your allowance exactly like the matching odds request.
  • A heartbeat is free: an unchanged refresh costs no objects.
  • Every refresh, heartbeat or not, counts towards your per-minute request limit, so each open stream uses one request every 15 seconds. See Limits.

Close codes

CodeMeaningWhat to do
1000The connection reached its lifetime of 3,600 seconds.Reconnect straight away for a fresh stream.
1008Your key, plan or a limit changed (401, 403 or 429 on a refresh), or you sent a message.Read the error frame. Fix the key or plan, or wait for the rate limit, before reconnecting.
1009A snapshot was larger than 8,000,000 bytes.Narrow markets or bookmakers, or lower limit, then reconnect.
1011A refresh failed on our side.Reconnect after a short wait, backing off if it repeats.
1013Your client read too slowly and the backlog passed 1,000,000 bytes.Process frames faster, or narrow the filters, then reconnect.

Reconnecting

Every stream ends, at the latest when it reaches its lifetime, so build reconnecting in from the start:

  1. On close, look at the code. Reconnect at once after 1000.
  2. After 1011 or a dropped network, wait a second and reconnect, doubling the wait each time it fails, up to a minute or so.
  3. After 1008, 1009 or 1013, change something first: the error frame or the table above says what. Reconnecting unchanged gets the same result.
  4. Keep at most 2 streams open per key, and close one before opening another. A new connection's first snapshot is the full current state, so nothing needs replaying.

Want the movement between snapshots too? Read History and snapshots, or see Live tracker for a complete example.

Need help?FAQ · Email support

Search across the SockOdds documentation and catalogue.