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

# Changelog

> Every change to the wire — field shapes, event types, endpoints, and behaviour

This page is the single record of changes to the feed's **wire contract**: field names and shapes,
event-type strings, endpoint paths, response shapes, and behaviour that a consumer parses or depends on.
If a change could affect how you read the feed, it is listed here.

<Note>
  **Get emailed when a breaking change ships.** Sign in to your console, open **Updates**, and add
  the address that should receive notices — a team list works, and it does not have to be your
  sign-in address. You will be emailed once per release that carries a breaking change, never for
  the rest. Everything on this page is also listed there, filtered to the sports you follow.
</Note>

## How to read this

* **Every entry is labelled `breaking` or not**, in its heading. Breaking means an integration that
  was correct against the previous contract, and follows the [tolerance rules](#what-we-guarantee-and-what-we-ask-of-you)
  below, stops working or starts silently misreading.
* **A breaking change does not always bump [`contractVersion`](#contractversion)** — the version
  covers each sport's event and state schema, so a change to the catalog, history, health, or auth
  surfaces has no version to move. Read the heading label, not the version, to know if you are affected.
* Changes ship **immediately** on merge — there is no dual-emit deprecation window. Pin your integration to
  a known `contractVersion` and treat a bump as a signal to read this page before upgrading.
* Entries are newest-first, grouped **Added / Changed / Removed**.

## What we guarantee, and what we ask of you

Some changes are breaking for a strict parser and harmless for a tolerant one. So that "breaking"
means something precise rather than depending on how your client is written, we classify every
change against these four assumptions about your integration:

1. **Ignore object fields you don't recognise.** We add fields without warning; a parser that
   rejects unknown keys will break on an addition we consider safe.
2. **Ignore event `type` values you don't recognise** — don't throw on an unmatched branch. New
   event types appear as a sport's vocabulary grows.
3. **Treat `league` and `sport` as opaque strings.** They are open vocabularies; new values appear
   when we add a league or a sport.
4. **Don't parse ids.** `sessionId` and `gameId` are opaque — their format is not a contract, and
   it has changed before.

Given those, an added field, an added event type, an added endpoint, an added optional query
parameter, and a field that becomes *more* guaranteed (nullable → always present) are **not**
breaking. Removing or renaming a field, changing a field's type or meaning, a field becoming
nullable, removing an event type, changing a value in an open vocabulary, moving an endpoint,
changing what a status code means, and rejecting input we previously accepted **are**.

## `contractVersion`

The WebSocket **hello frame** and every history response carry a `contractVersion` string.
**Each sport's feed versions independently** — basketball, soccer, baseball, and football each
carry their own `contractVersion` — so the schema identity is the **pair**
(`sport`, `contractVersion`); compare **both**. Versions count independently per sport, so "v5" alone is
ambiguous — both fields ride beside each other wherever the version appears. It bumps
**only** when that sport's wire schema does — a field added, removed, renamed, an event type added, or a
field's meaning changed. Logic fixes and additive optional fields that carry a default do **not** bump it.

It is a schema fingerprint, not a breakage signal: it moves for changes that cannot affect you
(a contract catching up to bytes you already receive), and stays put for changes that can
(a catalog field rename). Use it to detect that the schema you fold against has moved; use each
entry's `breaking` label to decide whether you must act.

Treat a pair you don't recognise as "read the changelog before trusting this frame":

```python theme={null}
EXPECTED = ("basketball", "v9")  # the identity is the pair — the version alone collides across sports
if (frame["sport"], frame["contractVersion"]) != EXPECTED:
    raise RuntimeError(
        f"feed is {(frame['sport'], frame['contractVersion'])}, integration expects {EXPECTED} "
        f"— see https://.../changelog before upgrading"
    )
```

***

<a id="2026-07-24-football-v5" />

## 2026-07-24 — football v5 (breaking): `break` renamed to `period_end`

### Changed

* The `break` event type is renamed **`period_end`** — same bare event (no payload), new name.
  It asserts that the current quarter ended: `state.period` still holds the quarter that just
  ended, and the next `snap`/`kickoff` advances `period`. All behaviour is unchanged; only the
  `type` string moved. Update any `"break"` matching to `"period_end"`.

<a id="2026-07-24-basketball-v9" />

## 2026-07-24 — basketball v9 (breaking): `break` renamed to `period_end`

### Changed

* The `break` event type is renamed **`period_end`** — same bare event (no payload), new name.
  It asserts that the current period ended: `state.clock` moves to `BREAK`, `state.period` still
  holds the period that just ended, and the next `live` press advances `period`. All behaviour is
  unchanged; only the `type` string moved. Update any `"break"` matching to `"period_end"`.

<a id="2026-07-23-soccer-v6" />

## 2026-07-23 — soccer v6 (breaking): `correlationId` removed from the wire envelope

`WireEvent.correlationId` is removed from the envelope on every sport's feed, not just defaulted
to `null`. It threaded an open→resolve span (e.g. a VAR review), but the pairing was already
readable without it: a review's id is on `state.openVar.varId` for every frame the review is
open, and at most one review is ever open at a time, so there's no ambiguity to resolve. See
[Core Concepts — Open then resolve](/concepts#open-then-resolve).

### Removed

* `correlationId` field on every event. If you were using it to identify which VAR review a frame
  belongs to, read `state.openVar.varId` instead — present on every frame the review is open,
  `null` once `var_resolve` closes it.

<a id="2026-07-23-football-v4" />

## 2026-07-23 — football v4 (breaking): `correlationId` removed from every event

### Removed

* `correlationId` field on every event — was always `null` on the football feed (the field-goal
  attempt and the flag are plain state flags, not an id'd pair); the field itself is now gone
  rather than always-null.

<a id="2026-07-23-basketball-v8" />

## 2026-07-23 — basketball v8 (breaking): `correlationId` removed from every event

`contractVersion` bumps again on top of v7 (below). Part of a shared-envelope change retiring
`WireEvent.correlationId` across every sport in the same PR — see the soccer v6 entry below for
the full rationale.

### Removed

* `correlationId` field on every event — always `null` on the basketball feed already (see the v6
  entry below); the field itself is now gone rather than always-null.

<a id="2026-07-23-basketball-v7" />

## 2026-07-23 — basketball v7: `team` payload constraint tightened

`contractVersion` bumps again on top of v6 (below) — no field was added, removed, or renamed;
`team`'s *constraint* tightened, which is enough to bump on its own (a payload that used to
validate can now be rejected).

### Changed

* `team` on `2pt_attempt` / `3pt_attempt` / `ft_attempt` / `made` / `miss` payloads is now
  **required and non-nullable** (`"A" | "B"`, no longer `"A" | "B" | null` as in v6). A shot press with no team
  — previously representable, though never producible by the reference scout client — now 422s
  at ingest instead of validating. `OpenAttempt.team` (state) tightens the same way, for the same
  reason: it's only ever constructed from an event that now guarantees one.

<a id="2026-07-23-basketball-v6" />

## 2026-07-23 — basketball v6 (breaking): shot attempts and resolutions self-describe on the event

`contractVersion` bumps again on top of v5 (below) — the WS-control-message documentation bump in
v5 is unaffected by this one. Shot attempts and their resolutions are now self-describing on the
event itself, rather than requiring a read of the state snapshot.

### Added

* `team` field on `2pt_attempt` / `3pt_attempt` / `ft_attempt` payloads — `"A" | "B" | null` (the
  shooter's side; `null` if opened before a team was known).
* `team` and `shot_type` fields on `made` / `miss` payloads — the shot's side and type
  (`"2pt" | "3pt" | "ft"`), so a `made`/`miss` frame carries what it resolved without a lookup
  elsewhere in the frame.

### Removed

* `GameState.lastResolved` (and its `ResolvedShot` shape) — superseded by the `made`/`miss`
  payload fields above.
* `OpenAttempt.attemptId` — no replacement.
* `WireEvent.correlationId` is now always `null` on the basketball feed (previously carried the
  open attempt's id, e.g. `"a17"`, from the attempt press through its `made`/`miss`). Pairing an
  attempt to its resolution no longer needs an id: at most one attempt is ever open at a time, and
  the resolving event now names its own team/shot type directly. Soccer's VAR-review
  `correlationId` is unaffected.

<a id="2026-07-23-baseball-v12" />

## 2026-07-23 — baseball v12 (breaking): `correlationId` removed from every event

### Removed

* `correlationId` field on every event — was always `null` on the baseball feed (no open/resolve
  pair exists in this feed's vocabulary); the field itself is now gone rather than always-null.

<a id="2026-07-22-soccer-v5" />

## 2026-07-22 — soccer v5: WS control messages join the versioned schema

`contractVersion` bumps, but nothing you receive over the wire changes.

### Added

* The WS control messages you already receive — `hello`, `ping`, `error`, `session_closed` — are now
  part of the documented, versioned schema (see [WebSocket feed](/api-reference/websocket)). The
  `error` frame's `code` is formally a closed set: `"not_found" | "not_authorized" | "unavailable"`.
  No field, value, or byte you see is different; this only makes an already-true shape official.

<a id="2026-07-22-football-v3" />

## 2026-07-22 — football v3: WS control messages join the versioned schema

`contractVersion` bumps again on top of v2 (below) — the wire reshape in v2 is unaffected by this
bump; it is the same mechanical, no-wire-change bump as the other three sports above.

### Added

* The WS control messages you already receive — `hello`, `ping`, `error`, `session_closed` — are now
  part of the documented, versioned schema (see [WebSocket feed](/api-reference/websocket)). The
  `error` frame's `code` is formally a closed set: `"not_found" | "not_authorized" | "unavailable"`.
  No field, value, or byte you see is different; this only makes an already-true shape official.

<a id="2026-07-22-football-v2" />

## 2026-07-22 — football v2: field goal, extra point, and two-point conversion events

### Added

* `field_goal {team}` event — +3, credited immediately to the stated side.
* `extra_point {team}` event — +1, credited immediately to the stated side.
* `two_point_conversion {team}` event — +2, credited immediately to the stated side.

### Changed

* `fumble` event renamed to `loose_ball` (state field `fumble` → `looseBall`), widened to also
  cover a muffed ball and a carrier crossing the line of scrimmage while the ball is live, not
  only a strip/botched snap.
* The try after a touchdown split from `try_result {team, points}` into `extra_point {team}` /
  `two_point_conversion {team}`; a failed try now emits no event — the play closes on `end`.

### Removed

* `fg_attempt` / `fg_good` / `fg_no_good` events and the `fgOpen` state field. A field goal is now
  a single `field_goal {team}` event, pressed only when the kick is good — a miss is signaled by
  the following `turnover` (blocked and returned) or `end` (no return). `snap` under an `fg_unit`
  formation read is the "kick coming" cue.

`contractVersion` for football bumps to `v2`.

<a id="2026-07-22-basketball-v5" />

## 2026-07-22 — basketball v5: WS control messages join the versioned schema

`contractVersion` bumps, but nothing you receive over the wire changes.

### Added

* The WS control messages you already receive — `hello`, `ping`, `error`, `session_closed` — are now
  part of the documented, versioned schema (see [WebSocket feed](/api-reference/websocket)). The
  `error` frame's `code` is formally a closed set: `"not_found" | "not_authorized" | "unavailable"`.
  No field, value, or byte you see is different; this only makes an already-true shape official.

<a id="2026-07-22-baseball-v11" />

## 2026-07-22 — baseball v11: WS control messages join the versioned schema

`contractVersion` bumps, but nothing you receive over the wire changes.

### Added

* The WS control messages you already receive — `hello`, `ping`, `error`, `session_closed` — are now
  part of the documented, versioned schema (see [WebSocket feed](/api-reference/websocket)). The
  `error` frame's `code` is formally a closed set: `"not_found" | "not_authorized" | "unavailable"`.
  No field, value, or byte you see is different; this only makes an already-true shape official.

<a id="2026-07-21-lost-tape-honesty" />

## 2026-07-21 — all sports (breaking, no bump): `historyAvailable` on catalog rows, `410` for a lost log

The per-sport event/state contracts are unchanged (no `contractVersion` bump). Previously, a
published session whose stored event log was lost (storage incident) answered the same `404` as a
session that never existed — a catalog row could point at a `/history` that denied the session was
ever published. Both surfaces now tell the truth.

### Added

* `historyAvailable` (boolean) on every [catalog](/api-reference/catalog) row — whether `/history`
  can serve that session's log right now. Almost always `true`; `false` marks a dead link you can
  skip without fetching it.

### Changed

* [`GET /api/history/{session}`](/api-reference/history) for a **published** session whose log is
  gone now answers **`410`** (`detail`: `"event log no longer available"`) instead of `404`.
  `404` again always means what the docs promise: never published (`"session not found"`) or a
  malformed id (`"invalid session id"`) — each with its own `detail` string. Access is checked
  before the `410`, so `401`/`403` behave exactly as before.

<a id="2026-07-21-catalog-score" />

## 2026-07-21 — all sports (no bump): `score` on catalog rows

The per-sport event/state contracts are unchanged (no `contractVersion` bump); this adds one field
to the [catalog](/api-reference/catalog) row. Previously the score reached you only in-stream (the
WebSocket, or by folding `/history`) — a catalog-only view had matchups but no numbers.

### Added

* `score` (object | null) on every [catalog](/api-reference/catalog) row — `{ a, b }`, aligned to
  `matchup`. The live running score on `status=open` rows, the final score on `status=closed` rows,
  `null` when unavailable. For a live session the WebSocket remains the authoritative running score;
  the catalog field is a convenience for listing views. Additive — existing parsers are unaffected.

<a id="2026-07-21-catalog-limit" />

## 2026-07-21 — all sports (no bump): `?limit=` on the sessions catalog

Additive query parameter on the [catalog](/api-reference/catalog); no shape change, no
`contractVersion` bump.

### Added

* `?limit=N` on `GET /api/sessions` — caps the response to the first `N` rows in the status
  ordering (so `?status=closed&limit=25` is the 25 most-recently-closed sessions). Omitted, or a
  non-positive value, means no cap — the prior behaviour, so existing integrations are unaffected.

<a id="2026-07-20-health-identity-fields" />

## 2026-07-20 — all sports (no bump): `sport` and `league` on `/health`

The per-sport event/state contracts are unchanged (no `contractVersion` bump): `sport`/`league` are
session identity, not game-state vocabulary — full state still lives on the WebSocket and history
snapshots.

### Added

* `sport` and `league` fields on [`GET /api/health/{session}`](/api-reference/health) responses —
  lets a caller holding only a bare session id resolve which sport it is without a full
  [`/history`](/api-reference/history) fetch. Always present once a session exists, so existing
  parsers are unaffected.

<a id="2026-07-18-ws-keepalive-ping" />

## 2026-07-18 — all sports (no bump): WebSocket keepalive `ping` messages

### Added

* The WebSocket feed now sends `{"type": "ping"}` roughly **every 15 seconds** between event
  frames. It is a control message like `hello` and `error` — not a `WireFrame` (no `outSeq`,
  `state`, or `matchup`) — so branch on `type` before treating a message as a frame and ignore
  it (see [Applying frames](/api-reference/websocket#applying-frames)). It exists to keep the
  connection visibly active end to end; silence longer than \~45 seconds now reliably means the
  connection is dead, so you can also use it as a staleness signal before reconnecting.

No `contractVersion` bump: frames themselves are unchanged, and clients following the documented
"branch on `type` first" rule are unaffected. If your parser treats every message as a `WireFrame`
without checking `type`, add the check before upgrading — the ping carries none of the frame fields.

<a id="2026-07-17-football-v1" />

## 2026-07-17 — football v1: new sport: football (NFL)

### Added

* **New sport: football (NFL).** Catalog rows, the WebSocket hello frame, and `/history` responses
  may now carry `"sport": "football"` (league slug `nfl`). The event vocabulary and state shape are
  documented on the new [Football](/sports/football) page; the contract starts at (`football`, `v1`).
  Existing sports' contracts are unchanged — if you don't parse football sessions, nothing to do.

<a id="2026-07-16-catalog-open-close-vocabulary" />

## 2026-07-16 — all sports (breaking, no bump): catalog `live`/`ended` renamed to `open`/`closed`

The catalog's "live/ended" wording is gone. "Live" bundled two different facts — that a session is
**published** (you may see it) and that it is still **open** (streaming) — and read as if it also
promised "happening right now." The catalog now names the two orthogonal axes plainly: **published**
(the access grant — `publishedAt`) and **open → closed** (the session's own lifecycle —
`openedAt` / `closedAt`).

### Changed

* `GET /api/sessions?status=` values renamed: **`live` → `open`**, **`ended` → `closed`**.
  `status=open` (still the default when omitted) lists published, still-streaming sessions;
  `status=closed` lists published sessions that have since closed (tape still readable via
  [`/history`](/api-reference/history)). `status=live` / `status=ended` now return `422`.
* Catalog row field renames: **`createdAt` → `openedAt`** (when the session opened) and
  **`endedAt` → `closedAt`** (when it closed). Same values and same null semantics — `closedAt` is
  `null` on `status=open` rows, set on `status=closed` rows. `sessionId`, `gameId`, `league`,
  `sport`, `matchup`, `publishedAt` are unchanged.
* Ordering is unchanged in effect: `status=open` by `publishedAt`, `status=closed` by `closedAt`
  (the same timestamp the old `endedAt` ordered by).

No `contractVersion` bump: the catalog is the top-level REST contract, not one of the per-sport feed
contracts that carry a version — so there is no number to move. Treat **this entry** as the break
signal: update your `status=` values and the two field names before upgrading.

<a id="2026-07-14-catalog-sport-non-null" />

## 2026-07-14 — all sports (no bump): catalog `sport` guaranteed non-null

### Changed

* The catalog's `sport` field is now **guaranteed present** on every session row — typed plain
  `string`, never `null`. Every league maps to exactly one sport, and sessions can only be
  created under a league in the published vocabulary (see
  [league slugs](/api-reference/catalog#league-slugs)), so the mapping cannot miss. No shape
  change for existing rows (no row ever actually carried `null`), so no version bump — but if
  your parser typed `sport` as nullable, you can drop the null branch.

<a id="2026-07-14-baseball-v10" />

## 2026-07-14 — baseball v10 (breaking): `mound` renamed to `pitch`

### Changed

* Event type renamed **`mound` → `pitch`**. Effect on state is unchanged (opens a play, clears the
  per-play flags). The press marks the pitcher beginning the delivery — it is anchored at the
  **start of the delivery**, not at release.
* Event type renamed **`play_hit` → `contact`**. The press claims **bat-on-ball contact** — at press
  time fair or foul (let alone a safe reach) is not yet known, so the old name overpromised: this
  event was never the official "hit" statistic, and contact can resolve into an out, an error, or a
  foul closed by `end` with nothing else recorded. Effect on state is unchanged (sets the per-play
  flag, clears every base).
* State field renamed **`hit` → `contact`** (boolean, the per-play flag a `home_run` requires),
  matching the event.

If you parse baseball frames: update the two event-type strings and the state field, and assert on
(`baseball`, `v10`). Histories of games scouted before this change still carry the old type strings —
fold them under the old names if you replay them.

<a id="2026-07-13-service-probe-field-removal" />

## 2026-07-13 — all sports (breaking, no bump): `sports` removed from the `/health` probe

The per-sport event/state contracts are unchanged (no `contractVersion` bump): this touches only
the ungated service liveness probe, which carries no sport schema.

### Removed

* `sports` on the `GET /health` response — the field echoed a compile-time constant (every
  deployment serves all sports), so it could never tell a consumer anything actionable. The probe
  is now the bare `{"status": "ok"}`. The sports the feed covers are listed in the
  [Sports overview](/sports/overview); what is actually live comes from the authenticated
  [`GET /api/sessions`](/api-reference/catalog) catalog.

<a id="2026-07-13-sandbox-mock-sessions" />

## 2026-07-13 — all sports (no bump): sandbox mock sessions

### Added

* **[Sandbox](/sandbox):** `POST /api/sandbox/sessions` mints a private mock session — a recorded
  real game replayed through the production pipeline at a configurable `speed` — so you can test
  your integration end-to-end (catalog discovery, WebSocket frames including `undo`/`score_set`
  corrections, `session_closed`, `/history` backfill) without waiting for a live game. One active
  mock per account; retriggering replaces it.
* Catalog: new optional `sandbox=true` query parameter on `GET /api/sessions` — a mode switch
  listing only your own mock sessions. **Default listings never contain mocks**, so existing
  consumers see no change.
* Hello frame and `/history` response: optional `"sandbox": true` key, present **only** on mock
  sessions and absent (never `false`) on real games — existing parsers are unaffected, which is
  also why no sport's `contractVersion` bumps.

<a id="2026-07-13-league-slug-rename" />

## 2026-07-13 — all sports (breaking, no bump): soccer league slug `fifa.world` renamed to `wc`

### Changed

* Soccer's league slug is renamed **`fifa.world` → `wc`** everywhere the `league` field
  appears (catalog entries, history responses, the WS hello frame) and in the catalog's
  `?league=` filter. League slugs are short lowercase abbreviations (no dots); the current
  vocabulary is listed on the [catalog page](/api-reference/catalog#league-slugs). No shape
  change — `league` is still an open-vocabulary string, so no version bump — but if you filter
  or compare against `fifa.world`, switch to `wc`. Entitlements were migrated server-side; no
  action needed there.
* Newly minted session ids are league-prefixed for readability (e.g. `nba-h8Fq2Kx9zL3mP`).
  Session ids remain **opaque** — same character set, do not parse them; existing ids are
  unchanged.

<a id="2026-07-12-ws-unavailable-refusal" />

## 2026-07-12 — all sports (no bump): WebSocket `unavailable` refusal when auth cannot be verified

### Changed

* When the server cannot verify **any** credential — its auth infrastructure is unreachable or
  misconfigured, an outage on our side — a WebSocket connect is now answered with the standard
  informative refusal: the connection is accepted, one `{"type": "error", "code": "unavailable"}`
  frame is sent, then the socket closes with `1011`. This is the **one retriable refusal**:
  reconnect with backoff, and do not rotate your token (it was never examined). Previously this
  surfaced exactly like a rejected credential (a pre-accept handshake refusal) or as a bare
  handshake `500`, so a retry policy could not tell "my token is bad, stop" from "the server is
  down, retry". Credential refusals themselves are unchanged (pre-accept, uniform, observed as a
  failed connection). The `ErrorFrame` `code` union gains `"unavailable"`.
  See [Close codes](/api-reference/websocket#close-codes).

<a id="2026-07-12-session-addressed-health" />

## 2026-07-12 — all sports (breaking, no bump): session-addressed, sport-uniform `/api/health`

Session health is now **session-addressed and sport-uniform**. The per-sport event/state contracts
are unchanged (no `contractVersion` bump): full game state in sport vocabulary lives on the
WebSocket and history snapshots, which are untouched.

### Changed

* `GET /{sport}/health/{session}` → **`GET /api/health/{session}`** — session-addressed like
  `/api/subscribe` and `/api/history`: the session id alone is the address, the server resolves
  the sport. The old sport-prefixed path is gone. Same auth (any valid account, Bearer or
  `?token=`), same 404-before-auth ordering.
* The response is now **one fixed schema for every sport**: `{status, phase, matchup, score,
  scoutDark, scoutLastSeenAgeS}`. `phase` is the session lifecycle phase (`"open"` / `"closed"`),
  as before.

### Removed

* The sport-specific game-state fields on the session-health response: `clock`, `period`
  (basketball); `matchPhase`, `playersOnPitch`, `scoreUnderReview` (soccer); `inning`, `half`,
  `outs`, `bases`, `final` (baseball). Health is a monitoring read; read game state from the
  WebSocket frames or the history backfill, where these fields live unchanged in the state
  snapshot.
* `GET /{sport}/health` — the three per-sport liveness probes were copies of the same answer;
  the one service probe is `GET /health` (unchanged, still ungated). Their ungated `sessions`
  id list goes with them — live session ids are listed by the authenticated
  [`GET /api/sessions`](/api-reference/catalog) catalog.

<a id="2026-07-11-ws-unknown-path-refusal" />

## 2026-07-11 — all sports (no bump): WebSocket refusal frame for an unknown path

### Changed

* A WebSocket connect to a URL path with **no WebSocket endpoint** (a typo, or a retired path
  shape like the old sport-prefixed `/{sport}/subscribe/{session}`) now gets the standard
  refusal: the connection is accepted, one `{"type": "error", "code": "not_found"}` frame is
  sent, then the socket closes with `4404` — the same answer as an unknown session id. Do not
  reconnect; fix the URL. Previously these connects failed the handshake with a bare `500`
  (surfacing in a browser as close `1006` with no reason), indistinguishable from an outage.
  See [Close codes](/api-reference/websocket#close-codes).

<a id="2026-07-10-sessions-catalog" />

## 2026-07-10 — all sports (no bump): sessions catalog at `/api/sessions`

### Added

* `GET /api/sessions?status=live|ended` — the catalog is now the **sessions catalog** (the rows
  were always sessions: the old `gameId` was documented as "the session id"). `status=live` (the
  default when omitted) is exactly the old `/api/games/live` behavior and filtering; `status=ended`
  lists sessions that were published and have since ended (newest-ended first) — previously an
  ended session vanished from every list while staying readable via `/history`. A never-published
  session appears in neither. Any other `status` value is a `422`. Same auth as before: a valid
  token always required, operator/scout see all, consumers entitled ∪ public.
  See [Sessions catalog](/api-reference/catalog).
* `?league=<slug>` on `GET /api/sessions` — optional single-value filter, composable with
  `status`. It only ever narrows what you may already see (a consumer filtering an unentitled
  league gets only that league's public sessions); an unknown league returns `200` with an empty
  list, not an error.
* New per-session fields in the catalog row: `catalogGameId` (string | null — the
  scheduling-catalog game this session covers, the same identifier session creation accepts;
  `null` for a manually-created session), `createdAt` (string | null — when the session was
  registered; `null` for rows from before 2026-07-10, exact backfill pending), and `endedAt`
  (string | null — when the session ended; always `null` on `status=live` rows).

### Changed

* Catalog field renames: response key `games` → `sessions`; row field `gameId` → `sessionId`
  (same value — the `{session}` segment for `/api/subscribe/{session}` and
  `/api/history/{session}`). `league`, `sport`, `matchup`, `publishedAt` are unchanged.

### Removed

* `GET /api/games/live` — replaced by `GET /api/sessions` (its default `status=live` slice is
  byte-for-byte the same filtering); the old path now returns `404`. No `contractVersion` bump:
  the per-sport frame/event contracts are untouched — this changes how you list sessions, not how
  you parse any frame (same framing as the prior endpoint moves below).

<a id="2026-07-10-session-id-game-id-renames" />

## 2026-07-10 — all sports (breaking, no bump): `sessionId` and `gameId` naming rule across the feed

One naming rule now holds across the whole feed: **`sessionId` is the feed/address (the
`{session}` URL segment); `gameId` is the real-world game it records.** No bump — these renames
ship in the same release train as the catalog reshape below, folding into the same notice, so no
consumer ever parses the intermediate shapes.

### Changed

* `GET /api/history/{session}`: response field `gameId` → `sessionId` (same value — the echo of
  the `session` path parameter; the value was always the session id, the name now says so).
* `GET /api/sessions`: row field `catalogGameId` → `gameId` (same value — the game this session
  records, shared by all sessions covering the same game; `null` for manually-created sessions;
  the group-by key for a game-level view). Freed for this meaning by the history rename above.
* The WebSocket hello frame and live frames are unaffected — they never carried `gameId`.

<a id="2026-07-09-top-level-games-live" />

## 2026-07-09 — all sports (no bump): top-level `/api/games/live` catalog

### Added

* `GET /api/games/live` — the live-games catalog is now a single **top-level** endpoint (no sport
  prefix); one call lists every live game you may watch across all sports, same game-object shape
  (`gameId`, `league`, `sport`, `matchup`, `publishedAt`) and same auth (a valid token is required)
  as before. See [Live games catalog](/api-reference/catalog).

### Removed

* `GET /<sport>/games/live` on all three mounts (`/basketball`, `/soccer`, `/baseball`) — the handler
  was sport-blind, so every copy returned identical cross-sport data. These paths now return `404`;
  call `GET /api/games/live` instead. No `contractVersion` bump: the per-sport frame/event contracts
  are untouched — this changes where you fetch the catalog, not how you parse any frame.

<a id="2026-07-09-sport-on-hello-and-history" />

## 2026-07-09 — all sports (no bump): `sport` on the hello frame and `/history`

### Added

* `sport` on the WebSocket **hello frame** and the `GET /api/history/{session}` response —
  `"basketball"` | `"soccer"` | `"baseball"`, beside `contractVersion` in both places. The schema
  identity is the **pair** (`sport`, `contractVersion`): versions count independently per sport, so
  a version-only pin passes even when the wrong sport's feed is wired into your parser — and a
  session-addressed URL carries no sport to catch that. Pin both (see
  [`contractVersion`](#contractversion) above).
* `league` on the WebSocket **hello frame** and the `GET /api/history/{session}` response — the
  session's league slug (e.g. `nba`, `fifa.world`), the same value the catalog carries. Session
  metadata like `matchup`, sent once on the hello rather than per frame. `null` on `/history` for a
  log written before the session lifecycle. Additive optional fields: no `contractVersion` bump,
  and the per-sport event/state contracts are unchanged.

<a id="2026-07-09-session-addressed-feed" />

## 2026-07-09 — all sports (no bump): session-addressed `/api/subscribe` and `/api/history`

### Added

* `WS /api/subscribe/{session}` and `GET /api/history/{session}` — the live feed and the history
  backfill are now **session-addressed, top-level** endpoints: the session id alone is the address
  and the server resolves the session's sport. Same frames, same response shape, same auth as the
  old sport-prefixed paths. The catalog's `sport` field is no longer needed to build URLs — it
  tells you which event vocabulary and state shape to parse the feed with.

### Removed

* `WS /{sport}/subscribe/{session}` and `GET /{sport}/history/{session}` on all three mounts
  (`/basketball`, `/soccer`, `/baseball`) — these paths now return `404`; connect to
  `WS /api/subscribe/{session}` and fetch `GET /api/history/{session}` instead. No
  `contractVersion` bump: the per-sport frame/event contracts are untouched — this changes where
  you connect, not how you parse any frame.

<a id="2026-07-09-health-requires-token" />

## 2026-07-09 — all sports (breaking, no bump): `/health/{session}` now requires a token

### Changed

* `GET /{sport}/health/{session}` now **actually requires the
  token** these docs have always claimed it requires (Bearer or `?token=`; any valid account —
  no entitlement needed). Previously it answered without one. An unauthenticated call now gets
  `401` (`"authentication required"` / `"invalid token"`) instead of `200`; send the same token
  you already use for `/api/history`. Response shape is unchanged, and the bare `/{sport}/health`
  liveness probe stays token-free — no `contractVersion` bump.

<a id="2026-07-08-sport-on-live-games" />

## 2026-07-08 — all sports (no bump): `sport` on live-games rows

### Added

* Each game object in the `GET /<sport>/games/live` response now carries `sport` (`"basketball"` |
  `"soccer"` | `"baseball"`) — the feed mount that serves the game, so you no longer need your own
  league→sport mapping to know which WebSocket/history prefix to use. `null` for sessions registered
  before 2026-07-08. Additive optional field: no `contractVersion` bump, and the per-sport event/state
  contracts are unchanged.

<a id="2026-07-02-bases-confirmed-runner-claims" />

## 2026-07-02 — all sports (no bump): `bases` flags are confirmed-runner claims, not verified occupancy

### Changed

* **Documentation only — no wire values change and `contractVersion` stays `v9`.** The `bases` flags (and the `set_base` payload's `occupied`) are **confirmed-runner claims**, not verified occupancy: `true` means the scout confirmed a runner safe on that base; `false` means *no confirmed runner* — an absence of a claim (never marked, cleared by `play_hit`/`inning_advance`, or retracted), **not** a statement that the base was checked and found empty. This has been the only reliable reading since v8 started clearing the bases on `play_hit`; the docs previously implied `false` = empty.
* **Reading pre-v8 history under this semantics.** Games logged before v8 were scouted without the clear-on-hit rule, so re-folding their event logs with a current reducer yields `bases` that go dark after each `play_hit` (scouts then had no reason to re-confirm unchanged runners). Under the clarified reading that projection is still *valid* — the flags say "no confirmed runner", nothing more — but do not treat `false` in a pre-v8 replay as evidence a base was unoccupied. No historical logs were or will be modified.

<a id="2026-07-02-baseball-v9" />

## 2026-07-02 — baseball v9 (breaking): `toggle_base` renamed to `set_base`

### Changed

* Event type `toggle_base` renamed to `set_base` — same payload (`base`, `occupied`), same absolute-occupancy semantics. The old name was legacy from when the field was still a flip; it never actually flipped.

<a id="2026-07-02-baseball-v8" />

## 2026-07-02 — baseball v8 (breaking): `play_hit` now clears `bases`

### Changed

* `play_hit` now clears `bases` (all three occupancy bits reset to `false`) the instant it fires — a ball in play puts every runner's safety back in question, so the scout re-marks who's safe with `toggle_base` rather than the feed assuming everyone advances. Previously `play_hit` left `bases` untouched. A following `home_run` in the same play still credits every runner who was on base at the moment of the hit; only the wire-visible `bases` snapshot changes.

<a id="2026-07-02-baseball-v7" />

## 2026-07-02 — baseball v7 (breaking): pitch-level event family and at-bat state fields removed

### Removed

* The never-emitted pitch-level event family: event types `batter_up`, `pitch`, `hit`, `walk`, `batter_out`, `inning_change`, and `outs_set` are no longer part of the baseball vocabulary. No scout UI ever produced them, no session ever recorded one, and they never appeared in these docs — the baseball feed has always carried only the manual-scorekeeper events documented on the [Baseball](/sports/baseball) page. If you branch on event `type`, nothing you have ever received is affected.
* State snapshot fields `balls`, `strikes`, and `currentBatter` — the pitch-level at-bat axes. They rode every baseball frame but were only ever their defaults (`0` / `0` / `null`) since no event could change them, and they were never documented. Drop them from your parsed shape; every other snapshot field is unchanged.
* The `pitchResults` enum key in `baseballContract.generated.json` (vocabulary of the removed `pitch` event).

<a id="2026-07-01-history-retracts-seq" />

## 2026-07-01 — baseball v6, basketball v4, soccer v4: `/history` undo events carry `retractsSeq`

### Added

* `undo` events in `GET /history` responses now carry the server-stamped `retractsSeq` — the `seq` of the press the undo cancelled (`null` when nothing remained, or for a log recorded before this change) — matching what the live WebSocket frame already carried. Strike the row whose `seq` matches instead of re-deriving retractions from a fold.
* New `serverStampedPayloadFields` key in every generated contract: the payload fields the **server** adds per event type (the scout never sends them) — today `{ "undo": ["retractsSeq"] }`. Consumer-facing payloads are `scoutEventPayloadFields` ∪ `serverStampedPayloadFields`. **No change to live-frame bytes**; the `/history` addition above is additive. The version bumps reflect the contract artifacts' bodies changing.

<a id="2026-07-01-basketball-v3" />

## 2026-07-01 — basketball v3: contract documents the shared wire envelope

### Changed

* The basketball contract now documents the shared wire envelope (`ingestEnvelopeFields`, `wireFrameFields`, `wireEventFields`) and the state `snapshotFields` — fields that were **already present on every basketball frame**. **No change to the bytes you receive.** The `contractVersion` bump (`v2 → v3`) reflects the contract catching up to the wire, and aligns basketball with baseball/soccer so that any future change to the shared envelope now bumps *all three* sports' versions, not just two.

## Baseline

Changes before this changelog existed are not itemised here — the feed was pre-release with no external
consumers. The current shape of every endpoint is documented under [API Reference](/api-reference/websocket).
This changelog records every change from here forward.
