Streaming
Keep one connection open and receive an event's prices whenever they change. Pro plans 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:
| Parameter | Streams |
|---|---|
event_id | One event, like GET /v1/events/{event_id}/odds. |
league | Up 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 bookmakersFrames
Every message is one JSON object with a type:
type | When | Body |
|---|---|---|
snapshot | On connect, then whenever the prices change. | The odds route's body, data, meta and links, with type added. |
heartbeat | A refresh found nothing changed. | at: when the check ran, in UTC. |
error | A 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
| Limit | Value |
|---|---|
| Refresh interval | 15 seconds |
| Open streams per key | 2 |
| Longest connection | 3,600 seconds |
| Events per league snapshot | 20 |
| Largest snapshot | 8,000,000 bytes |
| Largest unread backlog | 1,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
| Code | Meaning | What to do |
|---|---|---|
1000 | The connection reached its lifetime of 3,600 seconds. | Reconnect straight away for a fresh stream. |
1008 | Your 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. |
1009 | A snapshot was larger than 8,000,000 bytes. | Narrow markets or bookmakers, or lower limit, then reconnect. |
1011 | A refresh failed on our side. | Reconnect after a short wait, backing off if it repeats. |
1013 | Your 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:
- On close, look at the code. Reconnect at once after
1000. - After
1011or a dropped network, wait a second and reconnect, doubling the wait each time it fails, up to a minute or so. - After
1008,1009or1013, change something first: the error frame or the table above says what. Reconnecting unchanged gets the same result. - 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.