type field that disambiguates the payload shape. This page documents all five event types plus the welcome handshake message.
welcome
Sent exactly once, immediately after a successful WebSocket upgrade. Always the first frame.
Real captured:
| Field | Type | Description |
|---|---|---|
type | string | Always "welcome". |
channel | string | "live" or "odds" — which channel you’re subscribed to. |
conn_id | integer | Server-assigned unique connection id. Useful for tracing in server logs. |
message | string | Human-readable handshake message. |
snapshot
The second frame every subscriber receives, immediately after welcome and before any delta events. Contains the complete list of currently-live games with their full state. Use this as your baseline — apply all subsequent score_change / status_change / game_final / price_change events on top of this snapshot to maintain a correct live view.
Emitted once, per-subscriber. Not broadcast. Not repeated. Each new connection gets its own fresh snapshot.
Real captured (from the public endpoint during a Copa Libertadores match):
| Field | Type | Description |
|---|---|---|
type | string | Always "snapshot". |
channel | string | "live" or "odds" — matches the channel you subscribed to. |
ts | integer | Unix epoch milliseconds when the snapshot was built server-side. |
count | integer | Number of live games in games[]. May be 0 during quiet periods. |
games | array | Full Game objects for every game where scores.is_live = true at snapshot time. Same shape as List Live Games. |
Why snapshot-on-connect?
Before this existed, a new subscriber on/ws/live would see nothing until the next score change fired — which could be seconds or minutes depending on the match. With the snapshot, you know the complete live state from the moment you connect, and can start rendering a scoreboard immediately.
Apply events in order:
- Receive
snapshot— initialize your in-memory view of every live game. - Receive
score_change/status_change/game_finalevents — update the matching entry in your view. - On disconnect + reconnect — discard your state, wait for the new snapshot, restart from step 2.
What snapshot does NOT contain
- Market snapshots are not included even on
/ws/odds. For initial market state, call the REST endpointGET /v1/games/{id}/marketsonce per game you care about, then streamprice_changedeltas from there. Blasting full market state for every live game × every book × every outcome on connect would be tens of MB and pointless for most consumers. - Unplayed games are not included. Only games where
scores.is_live = trueat snapshot build time appear. - Final games are not included. Once a game transitions to
final, it drops out of the live set immediately.
score_change
Emitted whenever score_home, score_away, period, or clock changes on any polled game.
Real captured (Estudiantes 2-1 Cusco, Copa Libertadores, 64th minute):
| Field | Type | Description |
|---|---|---|
type | string | Always "score_change". |
game_id | string | Upstream canonical game id. |
pn_slug | string | null | Polynode slug. null for games without a resolvable slug. |
pn_league_code | string | League code (nba, epl, lib, etc.). |
score_home | integer | Current home score. |
score_away | integer | Current away score. |
period | string | null | Current period (e.g. "2H", "Q3", "7"). |
clock | string | null | Clock within the period (sport-specific format). |
ts | integer | Unix epoch milliseconds when the change was detected. |
/ws/live, /ws/odds
status_change
Emitted when a game transitions between unplayed → live → final. Fires alongside score_change when the transition happens at a boundary.
Schema:
| Field | Type | Description |
|---|---|---|
type | string | Always "status_change". |
game_id, pn_slug, pn_league_code | — | See score_change above. |
old_status | string | Previous status. Typically "unplayed" or "live". |
new_status | string | New status. Typically "live" or "final". |
ts | integer | Unix epoch milliseconds. |
/ws/live, /ws/odds
game_final
Fires immediately after a status_change into final or ended. Provides the final score as a convenience so you don’t have to join status_change + the last score_change.
Schema:
| Field | Type | Description |
|---|---|---|
type | string | Always "game_final". |
game_id, pn_slug, pn_league_code | — | See score_change above. |
score_home, score_away | integer | Final score. |
ts | integer | Unix epoch milliseconds of the transition. |
/ws/live, /ws/odds
new_game
Emitted the first time the poller sees a particular game_id. Typically fires when a game first appears on upstream’s schedule (days before start), not when it kicks off. For kickoff, watch status_change from unplayed → live.
Schema:
| Field | Type | Description |
|---|---|---|
type | string | Always "new_game". |
game_id, pn_slug, pn_league_code | — | See score_change above. |
ts | integer | Unix epoch milliseconds when we first ingested the game. |
/ws/live, /ws/odds
price_change
The core odds-movement event. Fires whenever any book’s price on any outcome changes.
Real captured (888sport shortening their “Estudiantes 2:1” correct-score price from +275 to +230 as Estudiantes went up 2-1):
| Field | Type | Description |
|---|---|---|
type | string | Always "price_change". |
game_id, pn_slug, pn_league_code | — | See score_change above. |
pn_market_type | string | Polynode market type code (moneyline, spreads, totals, points, correct_score, etc.). See List Market Types for the complete set. |
book | string | Sportsbook name. See List Sportsbooks. |
outcome | string | Outcome label. Sport-specific: team name for moneyline ("Miami Heat"), "Over 2.5" / "Under 2.5" for totals, player name for player props, score string like "Club Estudiantes de La Plata 2:1" for correct score. |
old_price | number | null | Previous American odds. null for a brand-new outcome we hadn’t seen before. |
new_price | number | Current American odds. Negative = favorite (-150 = risk 100), positive = underdog (+200 = risk 200). |
points | number | null | Spread or total line when applicable (-6.5, 2.5, 217.5). null for moneyline and most player props. |
ts | integer | Unix epoch milliseconds. |
/ws/odds only. (This event is not delivered on /ws/live.)
pong
Response to a client-sent {"action":"ping"} JSON message. Used for application-level keepalive when WebSocket protocol Pings are stripped by intermediate proxies.
Client sends:
Putting it together — correlating events after a goal
The real wire sequence after a goal in a soccer game:- Poll cycle runs, detects
is_live=true+ new score. score_changefires on both channels with the new score.- Multiple
price_changeevents fire on/ws/oddsas each book reprices the relevant outcomes (moneyline, correct score, totals, next goal, anytime goalscorer, etc.). - Clients see the score before the price movements because the diff engine always emits state changes first.

