GuideWebSocketStream APIREST

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.

By the PulseScore teamPublished 7 min read

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

WhatREST pollingLive WebSocketStream API (beta)
Endpoint/api/{bookmaker}/.../api/{bookmaker}/ws/live/api/stream/ws
Unitone bookmaker, one pageone bookmaker, one sportmany bookmakers and sports
Updateswhen you askfull board about every secondonly changes, once a second
Plansevery planPRO, MAX or ULTRAPRO, 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:

PlanRequests per bookmakerRequests per month
BASIC1 req/sec500
STARTER1 req/min30,000
PRO1 req/secUnlimited
MAX3 req/secUnlimited
ULTRA6 req/secUnlimited

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.

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

javascript
// 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:

text
wss://api.pulsescore.net/api/stream/ws?key=YOUR_API_KEY&bookmakers=bet365,onexbet&sports=soccer,tennis&kinds=live,prematch

Use 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:

javascript
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:

PlanSockets per bookmakerStream connectionsDistinct sports
PRO111
MAX323
ULTRA63all

Which one should you use?

Use caseMethodPlan
Exploring the data, occasional snapshotsRESTBASIC
A few leagues' prematch odds, refreshed hourlyRESTSTARTER
Live odds of one sport from a few bookmakersLive WebSocketPRO
Live and prematch of one sport on every bookmakerStream APIPRO
Several sports across every bookmakerStream APIMAX or ULTRA
Lookups next to a socket: an event, a league, resultsRESTany 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, a bookmakers=all stream and any /ws/live socket 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=None and a larger max_queue, process frames off the receive loop, and always reconnect with since.
  • 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.