Connect to live WebSockets#
Polling tells you the score every few seconds; the WebSocket pushes it the moment it changes. This guide gets you from zero to a working connection. Channel-level frame references: Football · Tennis.
What you need#
- An API token (Authentication).
- The WebSocket addon ($3/mo) from /addons/. Without it the
server accepts the connection, sends an error frame and closes with code
4402.
Pick a channel#
| URL | Use for |
|---|---|
wss://sports.bzzoiro.com/live/football/ |
Football — recommended. Score, stats, ball position, odds; on covered matches also per-action events with pitch coordinates |
wss://sports.bzzoiro.com/ws/live/ |
Multi-sport legacy channel — football and tennis on one socket ("sport": "tennis" on subscribe) |
Authenticate#
Two transports for the same token:
// Option A — query string (simplest)
const ws = new WebSocket("wss://sports.bzzoiro.com/live/football/?token=YOUR_API_KEY");
// Option B — WebSocket subprotocol (keeps the token out of URL logs; preferred)
const ws = new WebSocket("wss://sports.bzzoiro.com/live/football/", ["token", "YOUR_API_KEY"]);
Authentication failures close the socket after an explanatory error frame:
| Close code | Meaning |
|---|---|
4401 |
Missing or invalid token |
4402 |
Token OK but no active WebSocket subscription |
4404 |
Unknown WebSocket path |
Subscribe#
Send JSON frames. Up to 10 concurrent subscriptions per socket.
{"action": "subscribe", "event_id": 223510}
{"action": "subscribe", "event_id": 223510, "bookmaker_slug": "pinnacle"}
{"action": "unsubscribe", "event_id": 223510}
{"action": "ping"}
Which matches are live-trackable? The REST live list flags them:
GET /api/v2/events/live/ → live_websocket: true (and websocket_plus:
true for full per-action coverage). Bookmaker slugs come from
GET /api/v2/bookmakers/.
The server confirms with a subscribed frame containing a full snapshot
(event state, recent positional data, current odds), then streams updates.
Minimal working client#
const ws = new WebSocket("wss://sports.bzzoiro.com/live/football/", ["token", "YOUR_API_KEY"]);
ws.onopen = () => ws.send(JSON.stringify({ action: "subscribe", event_id: 223510 }));
ws.onmessage = (msg) => {
const frame = JSON.parse(msg.data);
switch (frame.type) {
case "subscribed": console.log("snapshot", frame.event, frame.odds); break;
case "event": console.log("score", frame.score, frame.time.display); break;
case "livedata": console.log("ball", frame.situation, frame.coordinates); break;
case "action": console.log("action", frame.action_type, frame.x, frame.y); break;
case "odds": console.log("odds", frame.odds); break;
case "error": console.warn(frame.code, frame.message);
}
};
ws.onclose = (e) => {
if (e.code === 4401) console.error("bad token");
else if (e.code === 4402) console.error("subscription required — /addons/");
else setTimeout(connect, 3000); // transient — reconnect with backoff
};
import asyncio, json, websockets
async def main():
url = "wss://sports.bzzoiro.com/live/football/?token=YOUR_API_KEY"
async with websockets.connect(url) as ws:
await ws.send(json.dumps({"action": "subscribe", "event_id": 223510}))
async for raw in ws:
frame = json.loads(raw)
if frame["type"] == "event":
print(frame["score"], frame["time"]["display"])
asyncio.run(main())
Staying connected#
- The server pings every 25 s; browsers and standard libraries answer automatically. If you implement your own client, reply to pings or you'll be dropped after ~20 s.
- On unexpected close (not 4401/4402), reconnect with exponential backoff and re-send your subscriptions — subscriptions do not survive a reconnect.
{"action":"ping"}→{"type":"pong"}works as an application-level liveness check.- Malformed JSON gets an
errorframe (bad_json) but the socket stays open.
Try it without code#
The live debug console connects with a demo token and shows raw frames for any currently-covered match — the fastest way to see real payloads before writing a client.