---
title: Connect to live WebSockets
description: Authenticate, subscribe and handle frames on the BSD live channels — with working JavaScript and Python clients.
badge: addon
---

# 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](/docs/websocket/football/) ·
[Tennis](/docs/websocket/tennis/).

## What you need

1. An API token ([Authentication](/docs/authentication/)).
2. The **WebSocket addon** ($3/mo) from [/addons/](/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:

```js
// 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.

```json
{"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

```js
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
};
```

```python
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 `error` frame (`bad_json`) but the socket stays
  open.

## Try it without code

The [live debug console](/websocket/debug/) 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.
