---
title: WebSockets overview
description: The two live channels, authentication, subscription protocol, limits and close codes.
badge: addon
---

# WebSockets

Push-based live data over WebSocket. One addon ($3/mo at
[/addons/](/addons/)) unlocks both channels; the token is the same one you
use for REST.

Hands-on guide with working clients:
[Connect to live WebSockets](/docs/guides/websockets/).

## Channels

| URL | Sports | Notes |
|---|---|---|
| `wss://sports.bzzoiro.com/live/football/` | Football | **Recommended.** Unified channel: score/stats, ball position, odds; on fully-covered matches also per-action events with pitch coordinates ("WS+") |
| `wss://sports.bzzoiro.com/ws/live/` | Football + tennis | Legacy multi-sport channel; tennis subscriptions go here with `"sport": "tennis"` |

Frame references: [Football channel](/docs/websocket/football/) ·
[Tennis channel](/docs/websocket/tennis/).

## Authentication

Same API token, two transports:

```
wss://sports.bzzoiro.com/live/football/?token=YOUR_API_KEY
```

or the `token` subprotocol (preferred — keeps keys out of URL logs):

```js
new WebSocket("wss://sports.bzzoiro.com/live/football/", ["token", "YOUR_API_KEY"]);
```

On failure the server **accepts** the socket, sends one `error` frame, then
closes:

| Close code | Error `code` | Meaning |
|---|---|---|
| `4401` | `auth_required` | Missing/invalid token |
| `4402` | `subscription_required` | No active WebSocket addon |
| `4404` | `not_found` | Unknown WebSocket path |

## Client → server protocol

All frames are JSON:

| Frame | Effect |
|---|---|
| `{"action":"subscribe","event_id":N}` | Follow a match |
| `{"action":"subscribe","event_id":N,"bookmaker_slug":"…"}` | Also stream one bookmaker's odds for it |
| `{"action":"subscribe","event_id":N,"sport":"tennis"}` | Tennis (on `/ws/live/` only) |
| `{"action":"unsubscribe","event_id":N}` | Stop following |
| `{"action":"ping"}` | → `{"type":"pong"}` |

**Limit: 10 concurrent subscriptions per socket** (match and bookmaker
subscriptions counted separately; football and tennis share the budget on
`/ws/live/`).

## Error frames

`{"type":"error","code":…,"message":…}` with codes: `bad_frame`, `bad_json`,
`bad_action`, `bad_event_id`, `not_tracked` (match has no live coverage),
`limit` (subscription cap), `bad_slug`, `unknown_bookmaker`. Only the three
4xxx cases above close the socket.

## Which matches are covered

Check the REST live list: `GET /api/v2/events/live/` →

- `live_websocket: true` — subscribable on the WebSocket
- `websocket_plus: true` — full per-action coverage (`action` frames with
  coordinates)

Subscribing to an uncovered match returns `not_tracked`.

## Operational notes

- Server pings every 25 s; unanswered pings drop the connection (~20 s
  timeout). Standard clients handle this automatically.
- Subscriptions don't survive reconnects — re-subscribe after reconnecting.
- No rate limit on the socket itself; connection read timeout is 1 hour of
  silence.
- Try real frames without code at the [debug console](/websocket/debug/).
