WebSocket

Connect

WSwss://api.sportsapi.app/?ticket={ticket}Business

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.

Protocol 4: handle the fixture frame — a client that skips it misses every match that kicks off while it is connected. A delta’s changes are a partial Fixture: merge them key by key into what you hold (nested objects such as homeScore, clock and cricket innings merge too — they never replace the whole object). Every frame carries ts (server time, Unix ms). Close codes: 1008 invalid or expired ticket (mint a new one) — or, on an open connection, its API key was revoked, the account suspended or the plan changed (the reason says which; checked at once on those events and every minute), 1013 your plan’s concurrent-connection limit is reached. The server sends a protocol ping every heartbeatMs — connections that stop answering are dropped. A score that goes DOWN (a disallowed goal, a corrected basket) arrives up to 1.5 s after the source sends it (football: 5 s), together with its lowered period scores: the source sometimes re-sends an older score for a moment, and such flaps are dropped (since 2026-10-24).

Every /v2 call can also answer the common errors.

Frame flowOpen tester →
← { "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

ChannelGradeSnapshotUpdates
livescoresFullFixture[] — every in-progress fixturefixture frames (id, sport, data: Fixture) when a fixture enters; deltas (id, sport, changes) for the fixtures you hold
sport:{slug}FullFixture[] — that sport’s in-progress fixturesfixture frames when a fixture enters; deltas (id, sport, changes) for the fixtures you hold
fixture:{id}FullFixture, 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}FullFixture[] — 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}FullRaceSnapshot — { 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

1008invalid, 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)
1013your plan’s concurrent-connection limit is reached — close another connection, retry later
WsErrorCodeWebSocket error code.10 values
bad_frameThe frame was not valid JSON, not a known shape, or lacks a field it needs (`message` names the field, e.g. "channel: required").
unknown_channelNot a channel name we serve (or an unknown sport slug).
channel_forbiddenThe channel is not in your plan’s grade.
unknown_leagueleague:{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_fixturefixture:{id} names a fixture id we never issued — REST answers the same id with 404 not_found. The channel is not counted.
unknown_racerace:{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_subscribedresync / unsubscribe of a channel this connection is not on.
too_many_channelsYou hit maxChannels on this connection.
snapshot_failedThe 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_limitedMore than 60 frames in 10 seconds on this connection; frames are dropped until the window resets.
Reconnecting: mint a fresh ticket each time (tickets are single-use). Apply deltas to the snapshot you hold; if a 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
FieldTypeMeaning
type"welcome"
protocolintegerProtocol 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.
heartbeatMsintegerThe server pings this often; answer pings (the browser does) or be dropped.
maxChannelsintegerChannels allowed on this connection.
replaybooleanWhether `lastSeq` replay is available on your plan.
WsSnapshotFull state of a channel: sent on subscribe and on resync.5 fields
FieldTypeMeaning
type"snapshot"
channelstring
seqintegerThe channel’s current sequence number. Opaque and increasing — not 0-based; the next delta has seq + 1.
tsintegerServer time, Unix milliseconds.
dataFixture[] | Fixture | RaceSnapshot | nulllivescores / 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
FieldTypeMeaning
type"delta"
channelstring
seqintegerPrevious seq + 1 on this channel; a gap means a delta was lost — send resync.
tsintegerServer time, Unix milliseconds.
idintegerOur fixture id.
sportstringSport slug of the fixture.
changesFixturePatchThe `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
FieldTypeMeaning
type"fixture"
channelstring
seqintegerPrevious seq + 1 on this channel (shared with the deltas).
tsintegerServer time, Unix milliseconds.
idintegerOur fixture id.
sportstringSport slug of the fixture.
dataFixtureA 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
FieldTypeMeaning
type"subscribed"
channelstring
seqintegerThe channel’s current seq after the replay — the next delta has seq + 1.
tsintegerServer time, Unix milliseconds.
replayedintegerHow 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
FieldTypeMeaning
type"winProbability"
channelstring
seqintegerPrevious seq + 1 on this channel (shared with the channel’s deltas).
tsintegerServer time, Unix milliseconds.
idintegerOur fixture id.
sport"football"Sport slug of the fixture (win probability is football only).
modelWinProbabilityModelModel version (the docs describe each). A new version is announced in the changelog.
pointWinProbabilityPointA 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
FieldTypeMeaning
type"commentary"
channelstring
seqintegerPrevious seq + 1 on this channel (shared with the channel’s deltas).
tsintegerServer time, Unix milliseconds.
idintegerOur fixture id.
sport"football"Sport slug of the fixture (commentary is football only).
entryCommentaryEntryOne 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
FieldTypeMeaning
type"events"
channelstring
seqintegerPrevious seq + 1 on this channel (shared with the channel’s deltas).
tsintegerServer time, Unix milliseconds.
idintegerOur fixture id.
sportstringSport slug of the fixture.
eventsKeyedIncident[]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.
changesIncidentChangesWhat 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
FieldTypeMeaning
type"statistics"
channelstring
seqintegerPrevious seq + 1 on this channel (shared with the channel’s deltas).
tsintegerServer time, Unix milliseconds.
idintegerOur fixture id.
sportstringSport slug of the fixture.
statisticsStatisticsMatch 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
FieldTypeMeaning
type"lineups"
channelstring
seqintegerPrevious seq + 1 on this channel (shared with the channel’s deltas).
tsintegerServer time, Unix milliseconds.
idintegerOur fixture id.
sportstringSport slug of the fixture.
lineupsLineupsLineups 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
FieldTypeMeaning
type"playByPlay"
channelstring
seqintegerPrevious seq + 1 on this channel (shared with the channel’s deltas).
tsintegerServer time, Unix milliseconds.
idintegerOur fixture id.
sportstringSport slug of the fixture.
startintegerIndex (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.
totalintegerLength 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.
entriesPlayByPlayEntry[]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
FieldTypeMeaning
type"momentum"
channelstring
seqintegerPrevious seq + 1 on this channel (shared with the channel’s deltas).
tsintegerServer time, Unix milliseconds.
idintegerOur fixture id.
sportstringSport slug of the fixture.
momentumMomentumMomentum 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
FieldTypeMeaning
type"shotmap"
channelstring
seqintegerPrevious seq + 1 on this channel (shared with the channel’s deltas).
tsintegerServer time, Unix milliseconds.
idintegerOur fixture id.
sportstringSport slug of the fixture.
shotsShot[]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
FieldTypeMeaning
type"race"
channelstring
seqintegerPrevious seq + 1 on this channel.
tsintegerServer time, Unix milliseconds.
idintegerOur race id.
sportstringSport slug of the race (motorsport, cycling).
dataRaceA 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
FieldTypeMeaning
type"raceResults"
channelstring
seqintegerPrevious seq + 1 on this channel.
tsintegerServer time, Unix milliseconds.
idintegerOur race id.
sportstringSport slug of the race.
sessioninteger | nullThe session the classification is of.
resultsRaceResult[]The session’s whole classification, as /v2/races/{id}/results?session=.
WsUnsubscribedConfirms an unsubscribe.3 fields
FieldTypeMeaning
type"unsubscribed"
channelstring
tsintegerServer time, Unix milliseconds.
WsPingApplication-level heartbeat, sent every heartbeatMs next to the protocol ping. No reply needed.2 fields
FieldTypeMeaning
type"ping"
tsintegerServer time, Unix milliseconds.
WsPongReply to your { "type": "ping" }.2 fields
FieldTypeMeaning
type"pong"
tsintegerServer time, Unix milliseconds.
WsErrorSomething you sent was refused. The connection stays open.5 fields
FieldTypeMeaning
type"error"
codeWsErrorCodeWebSocket error code.
messagestring
channel?string
tsintegerServer time, Unix milliseconds.