Skip to main content
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.
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.

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 below, stops working or starts silently misreading.
  • A breaking change does not always bump 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”:

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

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

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.

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.

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.

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.

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.

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.

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.

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

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

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 fumblelooseBall), 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.

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

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

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

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

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

Additive query parameter on the 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.

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} responses — lets a caller holding only a bare session id resolve which sport it is without a full /history fetch. Always present once a session exists, so existing parsers are unaffected.

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

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 page; the contract starts at (football, v1). Existing sports’ contracts are unchanged — if you don’t parse football sessions, nothing to do.

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: liveopen, endedclosed. 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). status=live / status=ended now return 422.
  • Catalog row field renames: createdAtopenedAt (when the session opened) and endedAtclosedAt (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.

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

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

Changed

  • Event type renamed moundpitch. 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_hitcontact. 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 hitcontact (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.

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; what is actually live comes from the authenticated GET /api/sessions catalog.

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

Added

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

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

Changed

  • Soccer’s league slug is renamed fifa.worldwc 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. 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.

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.

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

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.

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.
  • ?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 gamessessions; row field gameIdsessionId (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).

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 gameIdsessionId (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 catalogGameIdgameId (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.

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.

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.

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

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.

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.

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.

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.

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.

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.

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

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 scoutEventPayloadFieldsserverStampedPayloadFields. No change to live-frame bytes; the /history addition above is additive. The version bumps reflect the contract artifacts’ bodies changing.

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. This changelog records every change from here forward.