Live Odds: REST Polling vs WebSocket vs Delta Streams
REST polling, the live WebSocket or the delta Stream API? How PulseScore's three ways to get live odds compare on limits, bandwidth, code and plans.
Poll the REST API when you need a board every so often, open the live WebSocket when you want every in-play event of one sport pushed about once a second, and use the Stream API when you need exact copies of many bookmakers' boards over one connection. All three serve the same normalized documents from 60+ bookmakers; they differ in what they cost you in requests, sockets and bandwidth.
The three options at a glance
| What | REST polling | Live WebSocket | Stream API (beta) |
|---|---|---|---|
| Endpoint | /api/{bookmaker}/... | /api/{bookmaker}/ws/live | /api/stream/ws |
| Unit | one bookmaker, one page | one bookmaker, one sport | many bookmakers and sports |
| Updates | when you ask | full board about every second | only changes, once a second |
| Plans | every plan | PRO, MAX or ULTRA | PRO, MAX or ULTRA |
REST polling: simple, metered per bookmaker
Every bookmaker answers the same paths under its own prefix (/api/v3/bet365, /api/fanduel, /api/ps3838 and so on), so a poller written for one works for all of them. Lists are paginated: limit defaults to 5 and tops out at 30, so a sport with 90 live events costs three requests per refresh.
The limit applies per bookmaker, so polling five bookmakers on PRO gives you five requests a second in total:
| Plan | Requests per bookmaker | Requests per month |
|---|---|---|
| BASIC | 1 req/sec | 500 |
| STARTER | 1 req/min | 30,000 |
| PRO | 1 req/sec | Unlimited |
| MAX | 3 req/sec | Unlimited |
| ULTRA | 6 req/sec | Unlimited |
Two habits make polling cheaper. Send Accept-Encoding: gzip: boards are 6–14× smaller on the wire, and a 14.5 KB live-events reply becomes about 1 KB. And read the message of every 429, because it tells you which of three cases you hit: a plain rate limit (wait one window), a paused key (ten rejections within ten seconds on one bookmaker pause the key for 60 seconds, announced with a Retry-After header), or an exhausted monthly quota, which only next month or an upgrade fixes. On STARTER, 30,000 requests a month is about 1,000 a day, so budget before you schedule.
import time
import requests
BASE = "https://api.pulsescore.net/api/v3/bet365"
PACE = 1.05 # seconds between calls to one bookmaker: a bit over 1 / your plan's req/sec
session = requests.Session()
session.headers.update({"X-Secret": "YOUR_API_KEY", "Accept-Encoding": "gzip"})
def get(path, **params):
delay = 1.0
while True:
r = session.get(BASE + path, params=params, timeout=15)
if r.status_code != 429:
r.raise_for_status()
return r.json()
try:
message = str(r.json().get("message", ""))
except ValueError:
message = ""
if "Monthly quota" in message:
raise RuntimeError(message) # waiting will not help until next month
retry_after = r.headers.get("Retry-After") # set while the key is paused
if retry_after:
time.sleep(float(retry_after))
else:
time.sleep(60 if "per minute" in message else delay)
delay = min(delay * 2, 30)
def live_board(sport):
events, page = [], 1
while True:
body = get("/live-events", sport=sport, page=page, limit=30)
events += body["events"]
if not body["hasNextPage"]:
return events
page += 1
time.sleep(PACE)
while True:
board = live_board("soccer")
print(len(board), "live soccer events")
time.sleep(PACE)Polling is the right tool for prematch boards, one-off lookups (an event by id, a league's events, results) and anything on BASIC and STARTER, which are REST-only. For live odds it runs into the page math above: most of your budget goes on re-downloading prices that did not move. The getting started guide walks through the endpoints themselves.
The live WebSocket: one sport, the whole board every second
wss://api.pulsescore.net/api/{bookmaker}/ws/live?key=YOUR_API_KEY&sport=soccer (bet365 uses /api/v3/bet365/ws/live) pushes a frame about once a second with every live event of that sport, in the same schema as REST. The first message is {"type": "connected", ...} with your plan and the bookmaker's validSports. A sport the bookmaker does not carry closes the socket with 4004; a sport with no live events right now just sends empty frames until it has some.
Because every frame is complete, there is no state to repair: replace your board with each frame. The price is that every frame carries the whole live board again, and each socket is one bookmaker and one sport. Sockets are counted per bookmaker: live soccer from five bookmakers is five sockets and fits PRO, but soccer and tennis from the same bookmaker is two sockets on that bookmaker, which takes MAX or ULTRA.
// Node 22+ or a browser: WebSocket is built in.
const WS_URL = "wss://api.pulsescore.net/api/fanduel/ws/live?key=YOUR_API_KEY&sport=soccer";
const STOP = new Set([4001, 4003, 4004, 4010, 4012, 4029]); // a retry cannot fix these
let board = new Map();
let delay = 1000;
function connect() {
const ws = new WebSocket(WS_URL);
ws.onmessage = (msg) => {
const frame = JSON.parse(msg.data);
if (frame.type === "connected") {
delay = 1000;
console.log(`connected to ${frame.bookmaker} ${frame.sport} on ${frame.plan}`);
return;
}
board = new Map(frame.data.map((event) => [event.eventId, event]));
console.log(`${board.size} live events`);
};
ws.onclose = (e) => {
if (STOP.has(e.code)) return console.error(`stopped: ${e.code} ${e.reason}`);
setTimeout(connect, delay);
delay = Math.min(delay * 2, 30000);
};
}
connect();The Stream API: the board once, then only what changed
The Stream API puts every bookmaker and sport your plan allows on one socket. Each stream (bookmaker, kind and sport, where kind is live, prematch or racing) sends its full board once in pages of about 1 MB, then once a second only the events, markets and selections that changed, keyed by market and selection so your copy stays exact:
wss://api.pulsescore.net/api/stream/ws?key=YOUR_API_KEY&bookmakers=bet365,onexbet&sports=soccer,tennis&kinds=live,prematchUse bookmakers=all for every bookmaker; kinds defaults to live,prematch and racing is opt-in. Frames are compressed with permessage-deflate when your client offers it, 6–9× smaller on the wire. Every frame carries a seq: keep the last one per stream and reconnect with since to get resumed plus only the deltas you missed, as long as you are back within 120 seconds. Our own deploys use the same path: you see a 1012 close, reconnect at once and resume without a new snapshot.
Two honest limits. Changes are batched once a second per stream, so a delta typically lands 1–2 seconds after the bookmaker's write: this is not a sub-second feed. And prematch boards are big: a large bookmaker's prematch soccer snapshot is 20–450 MB, which is why live streams take their snapshot first and prematch follows in the background.
The reference clients, dependency-free apply.js and apply.py from the Stream API documentation, apply the deltas, route frames by stream, reconnect with since and stop on the close codes a retry cannot fix:
import { StreamConnection } from "./apply.js";
const conn = new StreamConnection({
url: "wss://api.pulsescore.net/api/stream/ws",
key: "YOUR_API_KEY",
streams: [
{ bookmaker: "bet365", kind: "live", sport: "soccer" },
{ bookmaker: "onexbet", kind: "live", sport: "soccer" },
],
onFrame: ({ stream, type }, client) => {
if (type === "delta" || type === "snapshot_end") {
console.log(stream, client.board(stream).size, "events");
}
},
onState: (state, info) => console.log(state, info), // connecting | open | reconnecting | closed
});
conn.connect();In Python the same class takes on_frame=lambda stream, kind, client, raw: ... and runs with asyncio.run(conn.run()) on the websockets package. Stream limits count per account across all your connections, and live and prematch of one sport count once:
| Plan | Sockets per bookmaker | Stream connections | Distinct sports |
|---|---|---|---|
| PRO | 1 | 1 | 1 |
| MAX | 3 | 2 | 3 |
| ULTRA | 6 | 3 | all |
Which one should you use?
| Use case | Method | Plan |
|---|---|---|
| Exploring the data, occasional snapshots | REST | BASIC |
| A few leagues' prematch odds, refreshed hourly | REST | STARTER |
| Live odds of one sport from a few bookmakers | Live WebSocket | PRO |
| Live and prematch of one sport on every bookmaker | Stream API | PRO |
| Several sports across every bookmaker | Stream API | MAX or ULTRA |
| Lookups next to a socket: an event, a league, results | REST | any plan |
Pitfalls that cost people hours
- 4012 is one of your own clients. Sockets are budgeted per bookmaker across both WebSocket products, and a stream connection takes one socket on every bookmaker it declares (all of them with
bookmakers=all). On PRO, abookmakers=allstream and any/ws/livesocket evict each other. Find the second client instead of reconnecting in a loop. - Replacing a socket at the cap has a cooldown. One replacement per 5 seconds per bookmaker; a second attempt gets 429 (or 4429 after the upgrade) with
retryAfterSec. - Python websockets closes slow readers itself. Its keepalive gives up after 20 seconds when the pong waits behind a big snapshot, and you see 1011. Connect with
ping_timeout=None(or 120 seconds and more),max_size=Noneand a largermax_queue, process frames off the receive loop, and always reconnect withsince. - 4030 means you fell behind. More than 120 seconds behind the live feed, or over 256 MB of changes not yet read once your snapshots are out, closes the socket. Reconnect with
since(finished streams resume) and read faster or declare fewer streams per socket. - Compression is not always on. Browsers, Node fetch, Python requests and Go decompress gzip for you; .NET HttpClient and Java HttpClient need it switched on.
REST paths and parameters are in the API docs, plan prices on the pricing page.
Frequently asked questions
Is the PulseScore Stream API a sub-second odds feed?
No. Changes are batched once per second per stream, so a delta typically arrives one to two seconds after the bookmaker's write. The live WebSocket pushes a full frame of the sport about once a second.
Can I use the live WebSocket or the Stream API on the free plan?
No. BASIC and STARTER are REST-only. The live WebSocket and the Stream API need PRO, MAX or ULTRA.
Do the live WebSocket and the Stream API share a connection limit?
Yes. Sockets are budgeted per bookmaker across both products (PRO 1, MAX 3, ULTRA 6 per bookmaker), and a stream connection uses one socket on every bookmaker it declares. A connection beyond the budget closes the oldest one with code 4012.
What happens to my stream when PulseScore deploys?
Every socket receives an error frame and a 1012 close while the new process takes over. Reconnect at once with since per stream and the server answers resumed plus the deltas you missed, without a new snapshot.
How fast can I poll the REST API?
The limit is per bookmaker: BASIC 1 req/sec (500 requests a month), STARTER 1 req/min (30,000 requests a month), PRO 1 req/sec (no monthly cap), MAX 3 req/sec (no monthly cap), ULTRA 6 req/sec (no monthly cap). Ten rejections within ten seconds on one bookmaker pause the key for 60 seconds.
Keep reading
Getting Started with the PulseScore API
Make your first PulseScore API call: get a free key, fetch live and pre-match odds as normalized JSON, page through boards, use the WebSocket and handle 429s.
How to Choose a Sports Odds API: 10 Things to Check
A practical checklist for evaluating a sports odds API: coverage, market depth, live latency, delivery, schema, racing, results, rate limits, pricing and docs.