Connect
Connect to the url from the ticket. The server greets you with a welcome frame (protocol 4 since 2026-10-23; 3 earlier that day, 2 from 2026-10-19); then subscribe to channels. A subscription starts with a snapshot (or, after a lastSeq replay or a repeated subscribe, a subscribed frame), followed by frames carrying a per-channel seq that increases by one per frame — an opaque number (it starts from the server clock, not 0). On livescores, sport:{slug} and league:{id} a fixture that enters the channel’s set (it kicks off, or the server first sees it) arrives as a fixture frame with the whole Fixture; deltas then patch it, and only fixtures you were given are patched — a fixture that leaves (finished, canceled) gets one last delta saying so.
Every /v2 call can also answer the common errors.
← { "type": "welcome", "protocol": 4, "heartbeatMs": 30000, "maxChannels": 50, "replay": true } → { "type": "subscribe", "channel": "livescores" } ← { "type": "snapshot", "channel": "livescores", "seq": 1760800000000, "ts": 1760800000000, "data": [ Fixture, … ] } ← { "type": "fixture", "channel": "livescores", "seq": 1760800000001, "ts": 1760800002480, "id": 201330, "sport": "basketball", "data": { "id": 201330, "sport": "basketball", "status": { "type": "inprogress", "phase": "inprogress", "code": 13, "description": "1st quarter", "period": "period1", "finish": null }, … } } ← { "type": "delta", "channel": "livescores", "seq": 1760800000002, "ts": 1760800004120, "id": 201324, "sport": "football", "changes": { "awayScore": { "current": 1, "periods": { "period2": 1 } } } } ← { "type": "delta", "channel": "livescores", "seq": 1760800000003, "ts": 1760800009310, "id": 201324, "sport": "football", "changes": { "status": { "code": 100, "description": "Ended", "type": "finished", "phase": "finished", "finish": "regular", "period": null }, "winner": "away", "clock": null } } // Reconnect with the last seq you hold (Business): the missed frames, then subscribed → { "type": "subscribe", "channel": "livescores", "lastSeq": 1760800000003 } ← { "type": "subscribed", "channel": "livescores", "seq": 1760800000003, "ts": 1760800030000, "replayed": 0 } // fixture:{id} — play by play appended from index start (since 2026-10-24) ← { "type": "playByPlay", "channel": "fixture:201330", "seq": 1760800040000, "ts": 1760800041900, "id": 201330, "sport": "basketball", "start": 117, "total": 118, "entries": [ { "type": "scoreChange", "side": "home", "period": "period3", "periodSecond": 401, "text": "Jump shot made", "homeScore": 69, "awayScore": 74, "scoreType": "twoPointer", … } ] } ← { "type": "shotmap", "channel": "fixture:201324", "seq": 1760800050000, "ts": 1760800050500, "id": 201324, "sport": "football", "shots": [ { "side": "away", "outcome": "goal", "minute": 37, "period": "period1", … }, … ] } → { "type": "subscribe", "channel": "fixture:999999999999" } ← { "type": "error", "code": "unknown_fixture", "channel": "fixture:999999999999", "message": "no fixture with that id", "ts": 1760800031000 } → { "type": "unsubscribe", "channel": "sport:tennis" } ← { "type": "error", "code": "not_subscribed", "channel": "sport:tennis", "message": "not subscribed to this channel", "ts": 1760800032000 }
Channels
| Channel | Grade | Snapshot | Updates |
|---|---|---|---|
| livescores | Full | Fixture[] — every in-progress fixture | fixture frames (id, sport, data: Fixture) when a fixture enters; deltas (id, sport, changes) for the fixtures you hold |
| sport:{slug} | Full | Fixture[] — that sport’s in-progress fixtures | fixture frames when a fixture enters; deltas (id, sport, changes) for the fixtures you hold |
| fixture:{id} | Full | Fixture, or null if unavailable (an id we never issued → error unknown_fixture, not counted toward maxChannels) | id, sport, changes — plus, while the fixture is in play: events frames (id, sport, events: the whole keyed list, changes: added / updated / removed) when its match events change, statistics frames (id, sport, statistics) and lineups frames (id, sport, lineups) when those change — fetched from REST (the live feed carries none of them): events ~20 s and ~45 s after each score / red-card / status change and every 30 s, statistics and lineups every 30 s, pushed only on a change. Since 2026-10-24 also playByPlay frames (id, sport, start, total, entries: the list from index start on — keep your first start entries and append; start beyond what you hold → reread REST; football and basketball, polled every 20 s), momentum frames (id, sport, momentum: the whole object) and shotmap frames (id, sport, shots: the whole list) — football, every 30 s; pushed only on a change, to at most 20 subscribed fixtures at once (the most-subscribed). For football also winProbability frames (id, sport, model, point) when an outcome moves ≥ 1 pp, and commentary frames (id, sport, entry) for each new AI commentary entry |
| league:{id} | Full | Fixture[] — that league’s in-progress fixtures (our league id; unknown id → error unknown_league) | fixture frames when a fixture enters; deltas (id, sport, changes) for the fixtures you hold |
| race:{id} | Full | RaceSnapshot — { race: Race, session, results: RaceResult[] }: the race with its sessions, the session running (else the last finished) and its classification (an id that is no race → error unknown_race) | race frames (id, sport, data: Race) when a session starts / ends or the race changes, and raceResults frames (id, sport, session, results) when the running session’s classification changes — polled from REST (race sports have no live feed): the race every 60 s while a session runs or starts within 15 min, results every 30 s, pushed only on a change |
Client frames
{ "type": "subscribe", "channel": "…", "lastSeq"?: n }Start a channel; answered with a snapshot. On the Full grade, passing the last seq you hold replays the missed frames instead, then a { "type": "subscribed", "channel", "seq", "ts", "replayed" } frame (the server keeps 5 minutes of frames per channel; older gaps get a fresh snapshot). A subscribe to a channel you are already on is answered with subscribed (replayed 0). |
{ "type": "unsubscribe", "channel": "…" }Stop a channel; answered with { "type": "unsubscribed", "channel", "ts" } — or error not_subscribed when you are not on it. |
{ "type": "resync", "channel": "…" }Request a fresh snapshot of a channel you are subscribed to (error not_subscribed otherwise). |
{ "type": "ping" }Application-level ping; answered with { "type": "pong", "ts": <ms> }. |
Close codes
| 1008 | invalid, expired or already used ticket — mint a new one; on an open connection: the API key was revoked, the account suspended, or the plan no longer includes this WebSocket grade (reason “plan changed; reconnect” = reconnect with a new ticket) |
| 1013 | your plan’s concurrent-connection limit is reached — close another connection, retry later |
WsErrorCodeWebSocket error code.10 values
| bad_frame | The frame was not valid JSON, not a known shape, or lacks a field it needs (`message` names the field, e.g. "channel: required"). |
| unknown_channel | Not a channel name we serve (or an unknown sport slug). |
| channel_forbidden | The channel is not in your plan’s grade. |
| unknown_league | league:{id} names a league id we never issued (use the ids of /v2/leagues and of fixtures’ league) — REST answers the same id with 404 not_found. |
| unknown_fixture | fixture:{id} names a fixture id we never issued — REST answers the same id with 404 not_found. The channel is not counted. |
| unknown_race | race:{id} names no race (a season or session id, or one we never issued) — /v2/races/{id} answers the same id with 404. The channel is not counted. |
| not_subscribed | resync / unsubscribe of a channel this connection is not on. |
| too_many_channels | You hit maxChannels on this connection. |
| snapshot_failed | The snapshot could not be built — send resync to retry (for league:{id}, when the league id could not be checked right now: subscribe again). |
| rate_limited | More than 60 frames in 10 seconds on this connection; frames are dropped until the window resets. |
seq is skipped, send resync, or on the Full grade resubscribe with lastSeq to replay the gap.Server frames
Every frame the server sends, field by field. A delta's changes is a FixturePatch: deep-merge it onto the Fixture you hold. A fixture frame adds a Fixture to a list channel.
WsWelcomeFirst frame after connecting.5 fields
| Field | Type | Meaning |
|---|---|---|
| type | "welcome" | |
| protocol | integer | Protocol version — 4 since 2026-10-23 evening (a fixture entering a list channel arrives as a `fixture` frame); 3 from 2026-10-23, 2 from 2026-10-19. |
| heartbeatMs | integer | The server pings this often; answer pings (the browser does) or be dropped. |
| maxChannels | integer | Channels allowed on this connection. |
| replay | boolean | Whether `lastSeq` replay is available on your plan. |
WsSnapshotFull state of a channel: sent on subscribe and on resync.5 fields
| Field | Type | Meaning |
|---|---|---|
| type | "snapshot" | |
| channel | string | |
| seq | integer | The channel’s current sequence number. Opaque and increasing — not 0-based; the next delta has seq + 1. |
| ts | integer | Server time, Unix milliseconds. |
| data | Fixture[] | Fixture | RaceSnapshot | null | livescores / sport:{slug} / league:{id}: Fixture[] (league: that league’s in-progress fixtures); fixture:{id}: Fixture, or null if unavailable; race:{id}: RaceSnapshot, or null if unavailable. |
WsDeltaOne fixture changed. On livescores / sport:{slug} / league:{id} a delta only names a fixture the channel already gave you (in its snapshot or a `fixture` frame); a fixture that leaves the channel (finished, canceled) gets one last delta saying so. Deltas carry live-feed changes and the corrections a board refresh makes; a score that goes DOWN (a disallowed goal, a corrected basket) arrives up to 1.5 s after the source sends it — the source sometimes re-sends an older score for a moment, and such flaps are dropped; a REST refetch of /fixtures/{id} can still be a few seconds newer — resync now and then if you need it exact.7 fields
| Field | Type | Meaning |
|---|---|---|
| type | "delta" | |
| channel | string | |
| seq | integer | Previous seq + 1 on this channel; a gap means a delta was lost — send resync. |
| ts | integer | Server time, Unix milliseconds. |
| id | integer | Our fixture id. |
| sport | string | Sport slug of the fixture. |
| changes | FixturePatch | The `changes` of a WebSocket delta: a partial Fixture with the same names and nesting. Deep-merge it onto the Fixture you hold — objects merge key by key, any other value (null included) replaces. Only these fields are ever sent. |
WsFixturelivescores / sport:{slug} / league:{id}: a fixture entered the channel’s set (it kicked off, or the server first saw it) — the whole Fixture, as a snapshot would hold it. Add it to your list; later deltas patch it. Not sent on fixture:{id} channels.7 fields
| Field | Type | Meaning |
|---|---|---|
| type | "fixture" | |
| channel | string | |
| seq | integer | Previous seq + 1 on this channel (shared with the deltas). |
| ts | integer | Server time, Unix milliseconds. |
| id | integer | Our fixture id. |
| sport | string | Sport slug of the fixture. |
| data | Fixture | A match. The same object on every endpoint and in WebSocket snapshots. |
WsSubscribedConfirms a subscribe that did not send a snapshot: a `lastSeq` replay (after the replayed frames), or a second subscribe to a channel you are already on.5 fields
| Field | Type | Meaning |
|---|---|---|
| type | "subscribed" | |
| channel | string | |
| seq | integer | The channel’s current seq after the replay — the next delta has seq + 1. |
| ts | integer | Server time, Unix milliseconds. |
| replayed | integer | How many frames the replay sent (0 = nothing was missed, or a subscribe to a channel you are already on). |
WsWinProbabilityfixture:{id} channels of in-play football fixtures: the win-probability estimate moved by at least one percentage point (checked on every change of the fixture and once a minute). `point` is the endpoint’s `current` (with the reason it moved) and `point.at` is when it was computed — NOT a point of the stored curve, whose minute points sit at minute ends: build a chart from GET /v2/fixtures/{id}/win-probability and keep it current with `?since=`.8 fields
| Field | Type | Meaning |
|---|---|---|
| type | "winProbability" | |
| channel | string | |
| seq | integer | Previous seq + 1 on this channel (shared with the channel’s deltas). |
| ts | integer | Server time, Unix milliseconds. |
| id | integer | Our fixture id. |
| sport | "football" | Sport slug of the fixture (win probability is football only). |
| model | WinProbabilityModel | Model version (the docs describe each). A new version is announced in the changelog. |
| point | WinProbabilityPoint | A point of the win-probability curve: an estimate and why it was emitted. |
WsCommentaryfixture:{id} channels of football fixtures: a new AI commentary entry was written. The frame’s `seq` is the channel’s (shared with the deltas); `entry.seq` is the entry’s number in GET /v2/fixtures/{id}/commentary — two different counters.7 fields
| Field | Type | Meaning |
|---|---|---|
| type | "commentary" | |
| channel | string | |
| seq | integer | Previous seq + 1 on this channel (shared with the channel’s deltas). |
| ts | integer | Server time, Unix milliseconds. |
| id | integer | Our fixture id. |
| sport | "football" | Sport slug of the fixture (commentary is football only). |
| entry | CommentaryEntry | One AI commentary entry. |
WsEventsfixture:{id} channels of fixtures in play: the match events changed (goal, card, substitution, VAR decision, period, …). Fetched from REST by the server — the live feed carries no events: refetched about 20 s and 45 s after each score, red-card or status change (the source lists a goal 18–33 s after the score moves) and every 30 s while in play (cards and substitutions have no signal). Sent only when the list changed; a goal that is chalked off arrives in `changes.removed`, never before REST confirms it. The list is whole, so a missed frame costs nothing; before the first one, read GET /v2/fixtures/{id}/events.8 fields
| Field | Type | Meaning |
|---|---|---|
| type | "events" | |
| channel | string | |
| seq | integer | Previous seq + 1 on this channel (shared with the channel’s deltas). |
| ts | integer | Server time, Unix milliseconds. |
| id | integer | Our fixture id. |
| sport | string | Sport slug of the fixture. |
| events | KeyedIncident[] | The WHOLE events list now, oldest first — the same incidents as GET /v2/fixtures/{id}/events, each with its key. Replace your copy with it. |
| changes | IncidentChanges | What an events frame changed against the previous one. |
WsStatisticsfixture:{id} channels of fixtures in play: the match statistics changed — the whole object, as GET /v2/fixtures/{id}/statistics. Polled from REST every 30 s while someone is subscribed (plus once after full time); sent only when the content changed. Expect ~30–60 s behind the pitch.7 fields
| Field | Type | Meaning |
|---|---|---|
| type | "statistics" | |
| channel | string | |
| seq | integer | Previous seq + 1 on this channel (shared with the channel’s deltas). |
| ts | integer | Server time, Unix milliseconds. |
| id | integer | Our fixture id. |
| sport | string | Sport slug of the fixture. |
| statistics | Statistics | Match statistics, per period. |
WsLineupsfixture:{id} channels of fixtures in play: the lineups changed — the whole object, as GET /v2/fixtures/{id}/lineups (per-player statistics included). Polled every 30 s while subscribed until they are confirmed and unchanged for 10 minutes; substitutions arrive as events.7 fields
| Field | Type | Meaning |
|---|---|---|
| type | "lineups" | |
| channel | string | |
| seq | integer | Previous seq + 1 on this channel (shared with the channel’s deltas). |
| ts | integer | Server time, Unix milliseconds. |
| id | integer | Our fixture id. |
| sport | string | Sport slug of the fixture. |
| lineups | Lineups | Lineups of both sides. |
WsPlayByPlayfixture:{id} channels of football and basketball fixtures in play: the source’s play by play (text commentary) changed. Polled from REST every 20 s while someone is subscribed (the source’s own page re-polls every 15–20 s), plus once after full time; sent only when the list changed, from the first changed entry on. Before the first frame, read GET /v2/fixtures/{id}/play-by-play.9 fields
| Field | Type | Meaning |
|---|---|---|
| type | "playByPlay" | |
| channel | string | |
| seq | integer | Previous seq + 1 on this channel (shared with the channel’s deltas). |
| ts | integer | Server time, Unix milliseconds. |
| id | integer | Our fixture id. |
| sport | string | Sport slug of the fixture. |
| start | integer | Index (0-based) in the whole list of the first entry in `entries`. Keep your first `start` entries, drop the rest, append `entries`. Usually your list’s length (new lines appended); smaller when the source corrected or removed a line. |
| total | integer | Length of the whole list now (= start + entries.length). Your list is out of step when `start` is beyond its length — read GET /v2/fixtures/{id}/play-by-play again. |
| entries | PlayByPlayEntry[] | The entries from `start` on, oldest first, as GET /v2/fixtures/{id}/play-by-play lists them. |
WsMomentumfixture:{id} channels of football fixtures in play: the momentum graph changed — the whole object, as GET /v2/fixtures/{id}/momentum (one point per minute, so about once a minute). Polled every 30 s while subscribed, plus once after full time; sent only when it changed.7 fields
| Field | Type | Meaning |
|---|---|---|
| type | "momentum" | |
| channel | string | |
| seq | integer | Previous seq + 1 on this channel (shared with the channel’s deltas). |
| ts | integer | Server time, Unix milliseconds. |
| id | integer | Our fixture id. |
| sport | string | Sport slug of the fixture. |
| momentum | Momentum | Momentum graph: per minute (football, basketball, handball, ice hockey, …) or per game (tennis). |
WsShotmapfixture:{id} channels of football fixtures in play: the shot map changed (a new shot, or the source corrected one). Polled every 30 s while subscribed, plus once after full time; sent only when it changed. Expect ~30–60 s behind the pitch.7 fields
| Field | Type | Meaning |
|---|---|---|
| type | "shotmap" | |
| channel | string | |
| seq | integer | Previous seq + 1 on this channel (shared with the channel’s deltas). |
| ts | integer | Server time, Unix milliseconds. |
| id | integer | Our fixture id. |
| sport | string | Sport slug of the fixture. |
| shots | Shot[] | The WHOLE shot map now, oldest first, as GET /v2/fixtures/{id}/shotmap. Replace your copy with it. |
WsRacerace:{id}: the race changed — a session started or ended, a status or schedule moved. The whole Race, as GET /v2/races/{id}. Polled from REST every 60 s while a session runs or starts within 15 minutes (the live feed carries nothing for race sports).7 fields
| Field | Type | Meaning |
|---|---|---|
| type | "race" | |
| channel | string | |
| seq | integer | Previous seq + 1 on this channel. |
| ts | integer | Server time, Unix milliseconds. |
| id | integer | Our race id. |
| sport | string | Sport slug of the race (motorsport, cycling). |
| data | Race | A race: a motorsport race weekend (practice, qualifying, sprint, race) or a cycling race (one-day race or stage race). |
WsRaceResultsrace:{id}: a session’s classification changed. Polled every 30 s while the session runs, and once more when it ends; sent only when it changed.8 fields
| Field | Type | Meaning |
|---|---|---|
| type | "raceResults" | |
| channel | string | |
| seq | integer | Previous seq + 1 on this channel. |
| ts | integer | Server time, Unix milliseconds. |
| id | integer | Our race id. |
| sport | string | Sport slug of the race. |
| session | integer | null | The session the classification is of. |
| results | RaceResult[] | The session’s whole classification, as /v2/races/{id}/results?session=. |
WsUnsubscribedConfirms an unsubscribe.3 fields
| Field | Type | Meaning |
|---|---|---|
| type | "unsubscribed" | |
| channel | string | |
| ts | integer | Server time, Unix milliseconds. |
WsPingApplication-level heartbeat, sent every heartbeatMs next to the protocol ping. No reply needed.2 fields
| Field | Type | Meaning |
|---|---|---|
| type | "ping" | |
| ts | integer | Server time, Unix milliseconds. |
WsPongReply to your { "type": "ping" }.2 fields
| Field | Type | Meaning |
|---|---|---|
| type | "pong" | |
| ts | integer | Server time, Unix milliseconds. |
WsErrorSomething you sent was refused. The connection stays open.5 fields
| Field | Type | Meaning |
|---|---|---|
| type | "error" | |
| code | WsErrorCode | WebSocket error code. |
| message | string | |
| channel? | string | |
| ts | integer | Server time, Unix milliseconds. |