> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polynode.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Event Reference

> Complete schema for every event type emitted by /ws/live and /ws/odds.

Every event on both WebSocket channels is a JSON object with a top-level `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:**

```json theme={null}
{
  "type": "welcome",
  "channel": "live",
  "conn_id": 1,
  "message": "connected to books-relayer live channel"
}
```

| 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):

```json theme={null}
{
  "type": "snapshot",
  "channel": "live",
  "ts": 1776210900000,
  "count": 11,
  "games": [
    {
      "game_id": "40664-16839-2026-04-14",
      "pn_slug": "lib-gar-est-2026-04-14",
      "pn_league_code": "lib",
      "sport_category": "soccer",
      "start_date": "2026-04-14T22:00:00Z",
      "status": "live",
      "venue": "Estadio Jorge Luis Hirschi",
      "home_team": {
        "name": "Club Estudiantes de La Plata",
        "abbreviation_pn": "est",
        "logo": "https://cdn.opticodds.com/team-logos/soccer/2857.png"
      },
      "away_team": {
        "name": "Cusco FC",
        "abbreviation_pn": "gar",
        "logo": "https://cdn.opticodds.com/team-logos/soccer/12453.png"
      },
      "scores": {
        "status": "Live",
        "score_home": 2,
        "score_away": 1,
        "period": "2H",
        "clock": "94",
        "is_live": true
      }
    }
  ]
}
```

| 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](/api-reference/books/games-live#game-object). |

### 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:

1. Receive `snapshot` — initialize your in-memory view of every live game.
2. Receive `score_change` / `status_change` / `game_final` events — update the matching entry in your view.
3. On disconnect + reconnect — discard your state, wait for the new snapshot, restart from step 2.

This is the **standard pattern** for any polynode WebSocket that serves stateful data (scores, orderbooks, position books). Ephemeral event streams (mempool, trade prints) don't need a snapshot because there's no persistent state to baseline.

### What `snapshot` does NOT contain

* **Market snapshots are not included** even on `/ws/odds`. For initial market state, call the REST endpoint [`GET /v1/games/{id}/markets`](/api-reference/books/game-markets) once per game you care about, then stream `price_change` deltas 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 = true` at 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):

```json theme={null}
{
  "type": "score_change",
  "game_id": "40664-16839-2026-04-14",
  "pn_slug": "lib-gar-est-2026-04-14",
  "pn_league_code": "lib",
  "score_home": 2,
  "score_away": 1,
  "period": "2H",
  "clock": "64",
  "ts": 1776208972203
}
```

| 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.      |

**Channels:** `/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:**

```json theme={null}
{
  "type": "status_change",
  "game_id": "40664-16839-2026-04-14",
  "pn_slug": "lib-gar-est-2026-04-14",
  "pn_league_code": "lib",
  "old_status": "unplayed",
  "new_status": "live",
  "ts": 1776208972203
}
```

| 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.                             |

**Channels:** `/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:**

```json theme={null}
{
  "type": "game_final",
  "game_id": "40664-16839-2026-04-14",
  "pn_slug": "lib-gar-est-2026-04-14",
  "pn_league_code": "lib",
  "score_home": 2,
  "score_away": 1,
  "ts": 1776210000000
}
```

| 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. |

**Channels:** `/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:**

```json theme={null}
{
  "type": "new_game",
  "game_id": "40664-16839-2026-04-14",
  "pn_slug": "lib-gar-est-2026-04-14",
  "pn_league_code": "lib",
  "ts": 1776200000000
}
```

| 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. |

**Channels:** `/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):

```json theme={null}
{
  "type": "price_change",
  "game_id": "40664-16839-2026-04-14",
  "pn_slug": "lib-gar-est-2026-04-14",
  "pn_league_code": "lib",
  "pn_market_type": "correct_score",
  "book": "888sport",
  "outcome": "Club Estudiantes de La Plata 2:1",
  "old_price": 275.0,
  "new_price": 230.0,
  "points": null,
  "ts": 1776208974186
}
```

| 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](/api-reference/books/market-types) for the complete set.                                           |
| `book`                                 | string         | Sportsbook name. See [List Sportsbooks](/api-reference/books/books).                                                                                                                                                      |
| `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 $150 to win $100), positive = underdog (`+200` = risk $100 to win $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.                                                                                                                                                                                                  |

**Channels:** `/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:**

```json theme={null}
{"action": "ping"}
```

**Server replies:**

```json theme={null}
{"type": "pong", "ts": 1776208974186}
```

## Putting it together — correlating events after a goal

The real wire sequence after a goal in a soccer game:

1. Poll cycle runs, detects `is_live=true` + new score.
2. **`score_change`** fires on both channels with the new score.
3. **Multiple `price_change` events** fire on `/ws/odds` as each book reprices the relevant outcomes (moneyline, correct score, totals, next goal, anytime goalscorer, etc.).
4. Clients see the score **before** the price movements because the diff engine always emits state changes first.

This ordering means you can reliably implement "when a goal is scored, wait N ms and see which books moved fastest" logic without race conditions.
