Documentation
Stream APIBeta
One WebSocket, every bookmaker and sport your plan allows: each board once, then only what changed, once a second. This page is the complete reference — protocol version 2. The product overview is at /stream-api.
Overview
Delta streams are the low-bandwidth alternative to the WebSocket feed: one socket, as many bookmakers and sports as your plan allows, prematch, live and racing. Each stream sends its full board once (in ~1 MB pages) and then, once per second, only what changed — keyed by market and selection, so a client keeps an exact copy of every board with a few KB/s instead of hundreds. Available on PRO, MAX and ULTRA.
Connect
Declare the whole subscription in the URL; the server checks it against your plan before the upgrade and subscribes every bookmaker × sport × kind that exists:
wss://api.pulsescore.net/api/stream/ws?key=YOUR_API_KEY&bookmakers=bet365,onexbet&sports=soccer,tennis&kinds=live,prematch
# bookmakers=all for every bookmaker; kinds defaults to live,prematch — add racing for races
# optional, prematch only: &markets=MATCH_RESULT,TOTALS &from=2026-09-12T00:00:00Z&to=2026-09-13T00:00:00ZFor per-stream league filters or to resume streams after a drop, connect with only key and send one JSON message within 5 seconds:
{"streams":[
{"bookmaker":"bet365","kind":"prematch","sport":"soccer","league":"United Kingdom||England Premier League"},
{"bookmaker":"onexbet","kind":"live","sport":"soccer","since":7684229893122425111}
]}Capabilities
GET /api/stream/capabilities (no key needed) is the source of truth for what you can declare: the socket path, the protocol version, the limits of every plan, and each bookmaker with the sports it streams live and prematch. Sport lists follow what each bookmaker publishes and are refreshed every 10 minutes.
{ "protocol": 2, "endpoint": "/api/stream/ws", "discoveredAt": "…",
"plans": { "pro": { "connections": 1, "sports": 1, "bookmakers": 1000 },
"max": { "connections": 2, "sports": 3, "bookmakers": 1000 },
"ultra": { "connections": 3, "sports": 1000, "bookmakers": 1000 } }, // 1000 = all
"bookmakers": [ { "id": "tab", "label": "TAB", "prematchSports": ["soccer", …], "liveSports": ["tennis", …],
"liveSportsOffered": ["soccer", "tennis", …], "racingSports": ["greyhounds", "harness", "horse-racing"] }, … ] }Plan Limits
Counted per account across all your connections; live and prematch of the same sport count once.
| Plan | Connections | Distinct sports | Bookmakers |
|---|---|---|---|
| PRO | 1 | 1 | all |
| MAX | 2 | 3 | all |
| ULTRA | 3 | all | all |
58 bookmakers and 39 sports are streamable today; the live list is at /api/stream/capabilities. A declaration over your limits is refused before the upgrade with HTTP 402 and a message; a connection beyond your cap replaces your oldest one (close code 4012) — unless it names the socket it replaces with replaces=<conn> (see Reconnect rules), in which case only that socket is closed.
Sockets are budgeted per bookmaker across both WebSocket products: PRO 1, MAX 3, ULTRA 6 per bookmaker, shared between /api/<bookmaker>/ws/live and delta streams. A delta stream connection uses one socket on every bookmaker it declares (all of them with bookmakers=all), so the oldest socket beyond that budget — legacy or stream — is closed with 4012, and a replacement inside the 5-second cooldown is refused with 429 (or 4429 after the upgrade) and retryAfterSec.
Refusals before the upgrade
Anything that can never stream is refused during the HTTP handshake, before a socket exists, with a JSON body {"error", "message"}. Browsers surface these as a 1006 close; every other client sees the status.
- 400not a WebSocket handshake
WebSocket upgrade required (Connection: Upgrade, Upgrade: websocket)
- 400bad URL declaration
unknown bookmaker … / unknown sport … (see /api/stream/capabilities) / kinds must be any of live, prematch, racing / from/to must be RFC 3339 timestamps / no stream matches the declaration / declaration too large / at most 256 markets
- 401no key
Missing API key
- 401unknown, malformed or denylisted key
Invalid API key
- 402plan without WebSocket access
WebSocket streaming requires a PRO, MAX, or ULTRA plan. Upgrade at https://pulsescore.net/dashboard
- 402subscription not active or trialing
Subscription is not active. Update payment at https://pulsescore.net/dashboard
- 402declaration over the sports cap
Your plan allows N sport(s); this connection together with your other connections would use M (live and prematch of one sport count once)
- 402declaration over the bookmakers cap
Your plan allows N bookmakers; this connection together with your other connections would use M
- 403account blocked
Account suspended
- 429at a socket cap inside the 5-s cooldown
At your plan's N-socket limit on <bookmaker> (shared between /ws/live and /api/stream/ws): wait 5s to replace a connection, or close one first — retryAfterSec: 5
- 429too many invalid keys from your address
Too many rejected API keys from this address; retry in Ns — retryAfterSec, Retry-After
- 401 / 402 / 403key refused 10× within 10 s
This API key was rejected repeatedly and is temporarily blocked from the WebSocket. Check your plan and subscription at https://pulsescore.net/dashboard (60 s, same status as the refusal)
- 503authentication backend unreachable
Authentication temporarily unavailable, retry shortly
| Status | When | Message |
|---|---|---|
| 400 | not a WebSocket handshake | WebSocket upgrade required (Connection: Upgrade, Upgrade: websocket) |
| 400 | bad URL declaration | unknown bookmaker … / unknown sport … (see /api/stream/capabilities) / kinds must be any of live, prematch, racing / from/to must be RFC 3339 timestamps / no stream matches the declaration / declaration too large / at most 256 markets |
| 401 | no key | Missing API key |
| 401 | unknown, malformed or denylisted key | Invalid API key |
| 402 | plan without WebSocket access | WebSocket streaming requires a PRO, MAX, or ULTRA plan. Upgrade at https://pulsescore.net/dashboard |
| 402 | subscription not active or trialing | Subscription is not active. Update payment at https://pulsescore.net/dashboard |
| 402 | declaration over the sports cap | Your plan allows N sport(s); this connection together with your other connections would use M (live and prematch of one sport count once) |
| 402 | declaration over the bookmakers cap | Your plan allows N bookmakers; this connection together with your other connections would use M |
| 403 | account blocked | Account suspended |
| 429 | at a socket cap inside the 5-s cooldown | At your plan's N-socket limit on <bookmaker> (shared between /ws/live and /api/stream/ws): wait 5s to replace a connection, or close one first — retryAfterSec: 5 |
| 429 | too many invalid keys from your address | Too many rejected API keys from this address; retry in Ns — retryAfterSec, Retry-After |
| 401 / 402 / 403 | key refused 10× within 10 s | This API key was rejected repeatedly and is temporarily blocked from the WebSocket. Check your plan and subscription at https://pulsescore.net/dashboard (60 s, same status as the refusal) |
| 503 | authentication backend unreachable | Authentication temporarily unavailable, retry shortly |
Frames
Every frame that belongs to a stream carries stream (bookmaker/kind/sport). Events inside snapshots and upserts are exactly the documents the REST API serves.
{"t":"connected","plan":"ultra","protocol":2,"conn":"9f2c…d1","limits":{"connections":3,"sports":1000,"bookmakers":1000},"streams":[…]} // 1000 = all; conn → replaces= on reconnect
{"t":"snapshot","stream":"bet365/prematch/soccer","seq":…,"part":1,"of":224,"events":[…]} // pages of ~1 MB
{"t":"snapshot_end","stream":"bet365/prematch/soccer","seq":…,"count":1781}
{"t":"resumed","stream":"onexbet/live/soccer","seq":<your since>} // instead of a snapshot when since was honoured
{"t":"delta","stream":"onexbet/live/soccer","seq":…,"ts":"…","changes":[
{"eventId":"E1","op":"upsert","event":{…}},
{"eventId":"E2","op":"remove","reason":"expired"}, // expired | removed | filtered
{"eventId":"E3","op":"patch","set":{"score":{…}},"mSet":[{"k":"<marketId>","isActive":false}],
"sel":[{"m":"<marketId>","k":"<selectionId>","odds":2.1,…}],"selDel":[{"m":"<marketId>","k":"<selectionId>"}]}]}
{"t":"hb"} // every 10 s when idle
{"t":"error","stream":"bet365/live/curling","code":4004,"message":"…","validSports":[…]} // per stream, socket stays open
{"t":"error","stream":"onexbet/live/soccer","code":4031,"message":"…"} // per stream: a fresh snapshot follows
{"t":"error","code":1012,"message":"Service restarting, reconnect with since="} // socket-level: a close frame followsApply a patch in the order set → unset → mDel → mAdd → mSet → selDel → sel; markets are keyed by marketId and selections by selectionId. Every value is absolute, so re-applying a frame is harmless. Changes that happen while your snapshot pages are still arriving are sent as one catch-up delta afterwards — a slow reader never misses anything. Keep the last seq per stream and pass it as since on reconnect to resume without a new snapshot (120 s window).
Applying deltas
Keep one board per stream: a map of events by eventId; inside an event keep markets as an ordered map by market key and each market's selections by selection key.
- Market key =
marketIdwhen present and non-empty, elsecanonicalMarket|period|line|rawName(missing parts are empty strings). Selection key =selectionId, elsecanonicalOutcome|rawName|line. Numbers in keys have no exponent and no trailing .0, booleans are true/false, ObjectIDs are 24-hex; a numericeventIdis rendered the same way. - Apply a patch in the order
set → unset → mDel → mAdd → mSet → selDel → sel.setassigns whole top-level values (a value may be null);unsetdeletes;mDeldeletes markets by key;mAddinserts full markets (an existing key is replaced in place);mSetmerges scalars into the market without touching selections, a null value removes that scalar;selDeldeletes selections;selreplaces the whole selection object (m and k are addressing only). upsertreplaces the event,removedeletes it, a snapshot withpart: 1resets the board. Every value is absolute, so re-applying a frame is idempotent.- Never sent, never diffed:
_id,createdAt,updatedAt,markets[].updatedAt,markets[].oddsSig. Market and selection order on the wire is not significant. Numeric ids above 2^53 cannot be parsed exactly by JavaScript; the bookmakers streamed today send such ids as strings.
The reference clients below implement exactly these rules and are tested against the server's own diff engine.
Racing streams
Horse racing, greyhounds, harness and trotting stream as kind=racing. Racing is opt-in: kinds defaults to live,prematch, so a socket that never declares racing never receives a race. The bookmakers that have races list them as racingSports in the capabilities (Bet365, TAB, Betfair, Sportsbet AU and others); for the rest the list is empty.
wss://api.pulsescore.net/api/stream/ws?key=YOUR_API_KEY&bookmakers=tab,bet365&sports=horse-racing,greyhounds&kinds=racing- A race is not an event: it has no markets. It is the race document as the REST racing endpoints serve it, with its runners under
horsesorgreyhounds— the bookmaker's spelling, kept as is (several file greyhound races underhorses). - Frames,
seq,sinceand close codes are the same as for events. The change envelope'seventIdcarries the raceid. - Keep one board per stream: races by
id, and runners inside a race by runner key —idwhen present, elsenumber|trap|name. When several runners of one race share an id, every one of them is keyedid|number|trap|name, and runners nothing tells apart get##2,##3, … in document order. The reference clients do this for you. - Apply a patch in the order
set → unset → rDel → rSet.rDeldeletes runners by key;rSetcarries whole runner objects with their key ink(addressing only) — an existing runner is replaced in place, a new one is appended. A runner whose price moved arrives complete. - Prematch filters (
league,from,to,markets) do not apply: they are ignored in the URL form and refused (4032) on a racing stream in the JSON form.
{ "t":"delta", "stream":"tab/racing/horse-racing", "seq":…, "ts":"…", "changes":[
{ "eventId":"SANDOWN_R9", "op":"patch",
"set": { "isInPlay":"true" },
"rSet": [ { "k":"1", "id":"1", "number":"1", "name":"INSINUATING", "active":true, … } ],
"rDel": [ "7" ] } ] }Both reference clients keep racing boards with RaceBoard; StreamConnection picks it for every …/racing/… stream on its own.
Resuming with since
seq is the Mongo cluster time of the newest change on that stream, so it is identical across our servers and only ever grows on a socket. After a drop, reconnect with the payload form and since = the last seq you applied, per stream. Within the 120-second ring buffer you get resumed plus only the deltas you missed; otherwise a full snapshot whose part 1 resets that board. Three cases always start with a snapshot: filtered prematch streams, a stream that reports seq: 0 (no change since it was activated), and a since older than the ring. Redeclare exactly the streams connected listed (bookmaker, kind, sport, filters) so your plan counters do not change.
What happens when we deploy
Our deploys never cost you your board. The new process loads the live state, catches up and starts accepting before the old one lets go, then every socket receives one error frame and close with code 1012. Reconnect immediately with since per stream: the server answers resumed and the handful of deltas you missed. Measured end to end: reconnected in about 0.35 s, no snapshot, no interval between two deltas longer than the stream's normal one second. A client that does not reconnect on 1012 stays disconnected.
Reconnect rules
- Reconnect with exponential backoff (0.5 s doubling to 30 s, jittered) on 1012, 1006, 4029, 4429, 4030 and 1009, and on a 502 / 503 / 429 handshake refusal. Honour
retryAfterSecwhen the error frame carries one. - Put
replaces=<conn>in the reconnect URL, with theconnof your lastconnectedframe, and keep it until a newconnectedarrives. A dropped socket is not always reported to us at once; naming it makes us close that one, instead of your oldest connection when you are at your plan's cap. The reference clients do this for you. - Stop on 4001, 4003, 4010, 4011, 4012, 4013, 4032, 4034 and 4035 — a retry cannot fix those. 4012 in particular means another of your own clients took the slot.
- Keep your boards across the reconnect; only a snapshot with
part: 1resets one. Treat 4031 and 1013 as per-stream notices the server handles by itself. - Before the upgrade, refusals are plain HTTP with a JSON body: 401 key, 402 plan / subscription / declaration over your caps, 403 blocked, 429 cooldown, 400 bad declaration, 503 auth temporarily unavailable.
Close codes
- 1012socket
service deploy or restart
What to do: reconnect at once with since per stream → resumed
- 1006 / 1001socket
network drop, or an HTTP refusal seen by a browser
What to do: reconnect with backoff
- 1009socketno retry
you sent a message over 64 KB
What to do: fix the client
- 1013per stream
stream could not be activated (database scan failed); retried automatically
What to do: nothing
- 4001socketno retry
invalid or missing API key
What to do: fix the key
- 4003socketno retry
plan without WebSocket access
What to do: upgrade
- 4004per stream
sport unknown for this bookmaker (validSports attached); other streams proceed
What to do: check capabilities
- 4010 / 4013socketno retry
key revoked, account blocked, plan lost stream access / subscription inactive
What to do: fix the account
- 4011socketno retry
plan downgraded; this connection no longer fits the new limits
What to do: reconnect with fewer streams
- 4012socketno retry
replaced by a newer connection of your account (connection cap, or the per-bookmaker socket budget shared with /ws/live), or by your own reconnect that named this socket in replaces=
What to do: stop — do not reconnect in a loop
- 4029socket
server-wide connection limit
What to do: retry later with backoff
- 4030socket
slow consumer: more than 120 s behind the live feed, or over 256 MB of changes not yet received (both counted only after all your snapshots are out), or one stream's snapshot took over 5 minutes from its turn — the message says which stream
What to do: reconnect with since: every finished stream resumes, only the unfinished ones snapshot again; read faster or declare fewer streams per socket
- 4031per stream
stream state rebuilt on the server; a fresh snapshot follows automatically
What to do: nothing — part 1 resets the board
- 4032socketno retry
bad declaration or filter (payload form)
What to do: fix the declaration
- 4034 / 4035socketno retry
declaration over your plan's sports / bookmakers cap (payload form)
What to do: declare less or close another connection
- 4036socket
your socket was idle and answered none of the last ten keepalive pings (5 minutes) — nothing was queued in front of them, so the connection is gone
What to do: let your library answer pings (a blocked event loop is the usual cause), then reconnect with since
- 4429socket
at the socket cap inside the 5-s reconnect cooldown (HTTP 429 before the upgrade)
What to do: wait retryAfterSec
| Code | Level | Meaning | What to do |
|---|---|---|---|
| 1012 | socket | service deploy or restart | reconnect at once with since per stream → resumed |
| 1006 / 1001 | socket | network drop, or an HTTP refusal seen by a browser | reconnect with backoff |
| 1009 | socket | you sent a message over 64 KB | fix the clientno retry |
| 1013 | per stream | stream could not be activated (database scan failed); retried automatically | nothing |
| 4001 | socket | invalid or missing API key | fix the keyno retry |
| 4003 | socket | plan without WebSocket access | upgradeno retry |
| 4004 | per stream | sport unknown for this bookmaker (validSports attached); other streams proceed | check capabilities |
| 4010 / 4013 | socket | key revoked, account blocked, plan lost stream access / subscription inactive | fix the accountno retry |
| 4011 | socket | plan downgraded; this connection no longer fits the new limits | reconnect with fewer streamsno retry |
| 4012 | socket | replaced by a newer connection of your account (connection cap, or the per-bookmaker socket budget shared with /ws/live), or by your own reconnect that named this socket in replaces= | stop — do not reconnect in a loopno retry |
| 4029 | socket | server-wide connection limit | retry later with backoff |
| 4030 | socket | slow consumer: more than 120 s behind the live feed, or over 256 MB of changes not yet received (both counted only after all your snapshots are out), or one stream's snapshot took over 5 minutes from its turn — the message says which stream | reconnect with since: every finished stream resumes, only the unfinished ones snapshot again; read faster or declare fewer streams per socket |
| 4031 | per stream | stream state rebuilt on the server; a fresh snapshot follows automatically | nothing — part 1 resets the board |
| 4032 | socket | bad declaration or filter (payload form) | fix the declarationno retry |
| 4034 / 4035 | socket | declaration over your plan's sports / bookmakers cap (payload form) | declare less or close another connectionno retry |
| 4036 | socket | your socket was idle and answered none of the last ten keepalive pings (5 minutes) — nothing was queued in front of them, so the connection is gone | let your library answer pings (a blocked event loop is the usual cause), then reconnect with since |
| 4429 | socket | at the socket cap inside the 5-s reconnect cooldown (HTTP 429 before the upgrade) | wait retryAfterSec |
A server-initiated close is always preceded by an error frame with the same code and a message; per-stream errors keep the socket open.
Filters (prematch streams)
league: exact, case-insensitive match on the event's league, spelled the way that bookmaker spells it (bet365:United Kingdom||England Premier League); payload form only.from/to: inclusive RFC 3339 bounds onstartTime; events without a start time are excluded when a bound is set.markets: keeps only markets whosecanonicalMarketormarketIdis listed (case-sensitive, at most 256); events keep streaming even when none of their markets match.- An event entering your view arrives as
upsert, one leaving it asremovewithreason: "filtered"; patches are cut to the allowed markets. Live streams accept no filters, and filtered streams always start with a snapshot.
Keeping the socket alive
The server sends hb when nothing else went out for 10 seconds and pings every 30 seconds; browsers and every mainstream library answer those for you. Ten unanswered pings (5 minutes) close the socket with 4036 only while it is idle — a pong that is queued behind the snapshot or the deltas you asked for is never held against you, and a client that is merely reading slowly is judged by the 4030 limits instead. Send nothing after the declaration. Your key is re-checked every 5 minutes: a revoked key, a blocked account or a lapsed subscription closes the socket with 4010 or 4013, a plan change applies the new limits and closes the connections that no longer fit with 4011, oldest kept first.
Your library's own keepalive. Most client libraries also ping the server and close the socket themselves when the pong is late. Our pong is immediate, but your library can only see it if it is reading the socket: when your consumer is slower than the stream and the receive queue is full, the reader pauses, the pong waits behind the data, and the library gives up. Python websockets does this after 20 s and closes with 1011 keepalive ping timeout; you then reconnect into a full snapshot and the cycle repeats. If you see 1011 (or 1006 right after a big snapshot) on your side, that is your client, not the server. Connect with ping_timeout=None (or 120 s and more), max_size=None, a larger max_queue, process frames off the receive loop, and always reconnect with since so the snapshot is not repeated. Node ws and PHP amphp have the same knobs (their ping and pong timeouts). The reference clients below are configured this way.
Limits
- Payload declaration
- one JSON message within 5 s of connecting, at most 64 KB after decompression → 4032
- Declared streams
- at most 16384 per connection (bookmakers × sports × kinds) → 400 / 4032
- Market filters
- at most 256 ids per stream → 400 / 4032
- Messages after the declaration
- ignored; one over 64 KB closes the socket with 1009
- Per-stream queue
- 64 frames; when it overflows the stream switches to coalesced catch-up deltas (the net change per event) — nothing is lost
- Falling behind
- more than 120 s behind the live feed, or over 256 MB of changes not yet received, once all your snapshots are out → 4030
- Snapshot phase
- streams take their snapshot 4 at a time (live first, then racing, then prematch); each must finish within 5 minutes of its turn → 4030
- Keepalive
- a ping every 30 s; ten unanswered pings (5 minutes) close an idle socket with 4036 (never one that is still receiving data). Your own library's ping timeout is the usual cause of a 1011: raise it (Python websockets: ping_timeout=None or ≥ 120 s, max_queue up, max_size=None)
- Resume window
- the last 120 s of deltas per stream (survives our deploys)
- Reconnect at the cap
- one replacement per 5 s per bookmaker; a second attempt → 429 / 4429 with retryAfterSec
- Repeated refusals
- a key refused 10× within 10 s is quarantined for 60 s; 30 invalid keys from one address within a minute → 429 for the rest of it
- Server capacity
- 5000 sockets per process → 4029
| Payload declaration | one JSON message within 5 s of connecting, at most 64 KB after decompression → 4032 |
| Declared streams | at most 16384 per connection (bookmakers × sports × kinds) → 400 / 4032 |
| Market filters | at most 256 ids per stream → 400 / 4032 |
| Messages after the declaration | ignored; one over 64 KB closes the socket with 1009 |
| Per-stream queue | 64 frames; when it overflows the stream switches to coalesced catch-up deltas (the net change per event) — nothing is lost |
| Falling behind | more than 120 s behind the live feed, or over 256 MB of changes not yet received, once all your snapshots are out → 4030 |
| Snapshot phase | streams take their snapshot 4 at a time (live first, then racing, then prematch); each must finish within 5 minutes of its turn → 4030 |
| Keepalive | a ping every 30 s; ten unanswered pings (5 minutes) close an idle socket with 4036 (never one that is still receiving data). Your own library's ping timeout is the usual cause of a 1011: raise it (Python websockets: ping_timeout=None or ≥ 120 s, max_queue up, max_size=None) |
| Resume window | the last 120 s of deltas per stream (survives our deploys) |
| Reconnect at the cap | one replacement per 5 s per bookmaker; a second attempt → 429 / 4429 with retryAfterSec |
| Repeated refusals | a key refused 10× within 10 s is quarantined for 60 s; 30 invalid keys from one address within a minute → 429 for the rest of it |
| Server capacity | 5000 sockets per process → 4029 |
Sports and bookmaker names
Bookmaker ids and sport keys are exactly the strings /api/stream/capabilities lists per bookmaker and kind — kebab-case sports such as table-tennis or ice-hockey; esports and e-sports are the same sport on every bookmaker (the legacy live feed's validSports keeps each bookmaker's own spelling, bet365's being e-sports). A declaration may also use the REST spelling with underscores (table_tennis) — it resolves to the same stream. Every frame's stream id uses the bookmaker's own kebab-case spelling. The lists follow what each bookmaker publishes and are refreshed every 10 minutes; a sport a bookmaker no longer has answers 4004 on your next connect. Event, market and selection ids are per bookmaker.
Latency and bandwidth
Changes are batched once per second per stream, so a delta typically arrives 1–2 seconds after the bookmaker's write; this is not a sub-second feed. Frames are compressed with permessage-deflate when your client offers it (6–9× smaller on the wire): a live soccer stream of one bookmaker is roughly 50–150 KB/s uncompressed, and a full prematch soccer board of a large bookmaker is 20–450 MB for its snapshot. Streams take their snapshot four at a time — live first, then racing, then prematch, in the order you declared them — so your live boards are complete within seconds while the big prematch boards follow, and a stream that has its snapshot keeps delivering deltas while later ones ship theirs. If a socket drops half way, reconnect with since: every finished stream resumes and only the unfinished ones are sent again.
Reference clients
Two dependency-free files implement everything above — the board applier, the per-stream router and a connection keeper that reconnects, redeclares with since and stops on the fatal codes: apply.js (ES module, browser or Node ≥ 22) and apply.py (Python ≥ 3.8; the connection keeper uses the websockets package).
import { StreamConnection } from "./apply.js";
const conn = new StreamConnection({
url: "wss://api.pulsescore.net/api/stream/ws",
key: "YOUR_API_KEY",
streams: [ // or put bookmakers=&sports= in the url instead
{ bookmaker: "bet365", kind: "live", sport: "soccer" },
{ bookmaker: "bet365", kind: "prematch", sport: "soccer", markets: ["MATCH_RESULT"] },
],
onFrame: ({ stream, type }, client) => {
if (type === "delta" || type === "snapshot_end") render(stream, client.board(stream).toArray());
},
onState: (state, info) => console.log(state, info), // connecting | open | reconnecting | closed
});
conn.connect();
// conn.close() when doneimport asyncio
from apply import StreamConnection # pip install websockets
conn = StreamConnection(
"wss://api.pulsescore.net/api/stream/ws", key="YOUR_API_KEY",
streams=[{"bookmaker": "bet365", "kind": "live", "sport": "soccer"}],
on_frame=lambda stream, kind, client, raw: kind == "delta" and render(stream, client.board(stream).to_list()),
on_state=lambda state, info: print(state, info),
)
asyncio.run(conn.run()) # returns on a fatal close or conn.close()Without the keeper, feed frames to StreamClient.apply() and reconnect yourself with the payload form and client.since().
FAQ
- I reconnected with since and got a snapshot. The stream was filtered, had
seq: 0, or yoursincewas older than the 120-second ring. The snapshot resets that board. - I keep getting 4012. Another client of your own account took the slot: the stream connection cap, or the per-bookmaker socket budget shared with the legacy WebSocket. Close the other client instead of retrying.
- 402 on sports although I declared few. Sports and bookmakers are counted across all your open connections; the message names the total the declaration would reach.
- Can I add a stream without reconnecting? No — the declaration is fixed for the life of the socket; open another connection or reconnect with the new set.
- Can the deploy lose changes? No. Every change is delivered at least once; re-applying a frame is harmless because every value is absolute.
The full wire contract — key derivation, filters, close codes, the resume rules and reference appliers for JavaScript and Python — is published by the stream service itself: /api/stream/doc.