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
breakingor 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
contractVersionand 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:- 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.
- Ignore event
typevalues you don’t recognise — don’t throw on an unmatched branch. New event types appear as a sport’s vocabulary grows. - Treat
leagueandsportas opaque strings. They are open vocabularies; new values appear when we add a league or a sport. - Don’t parse ids.
sessionIdandgameIdare opaque — their format is not a contract, and it has changed before.
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
breakevent type is renamedperiod_end— same bare event (no payload), new name. It asserts that the current quarter ended:state.periodstill holds the quarter that just ended, and the nextsnap/kickoffadvancesperiod. All behaviour is unchanged; only thetypestring moved. Update any"break"matching to"period_end".
2026-07-24 — basketball v9 (breaking): break renamed to period_end
Changed
- The
breakevent type is renamedperiod_end— same bare event (no payload), new name. It asserts that the current period ended:state.clockmoves toBREAK,state.periodstill holds the period that just ended, and the nextlivepress advancesperiod. All behaviour is unchanged; only thetypestring 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
correlationIdfield on every event. If you were using it to identify which VAR review a frame belongs to, readstate.openVar.varIdinstead — present on every frame the review is open,nulloncevar_resolvecloses it.
2026-07-23 — football v4 (breaking): correlationId removed from every event
Removed
correlationIdfield on every event — was alwaysnullon 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
correlationIdfield on every event — alwaysnullon 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
teamon2pt_attempt/3pt_attempt/ft_attempt/made/misspayloads is now required and non-nullable ("A" | "B", no longer"A" | "B" | nullas 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
teamfield on2pt_attempt/3pt_attempt/ft_attemptpayloads —"A" | "B" | null(the shooter’s side;nullif opened before a team was known).teamandshot_typefields onmade/misspayloads — the shot’s side and type ("2pt" | "3pt" | "ft"), so amade/missframe carries what it resolved without a lookup elsewhere in the frame.
Removed
GameState.lastResolved(and itsResolvedShotshape) — superseded by themade/misspayload fields above.OpenAttempt.attemptId— no replacement.WireEvent.correlationIdis now alwaysnullon the basketball feed (previously carried the open attempt’s id, e.g."a17", from the attempt press through itsmade/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-reviewcorrelationIdis unaffected.
2026-07-23 — baseball v12 (breaking): correlationId removed from every event
Removed
correlationIdfield on every event — was alwaysnullon 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). Theerrorframe’scodeis 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). Theerrorframe’scodeis 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
fumbleevent renamed toloose_ball(state fieldfumble→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}intoextra_point {team}/two_point_conversion {team}; a failed try now emits no event — the play closes onend.
Removed
fg_attempt/fg_good/fg_no_goodevents and thefgOpenstate field. A field goal is now a singlefield_goal {team}event, pressed only when the kick is good — a miss is signaled by the followingturnover(blocked and returned) orend(no return).snapunder anfg_unitformation 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). Theerrorframe’scodeis 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). Theerrorframe’scodeis 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/historycan serve that session’s log right now. Almost alwaystrue;falsemarks a dead link you can skip without fetching it.
Changed
GET /api/history/{session}for a published session whose log is gone now answers410(detail:"event log no longer available") instead of404.404again always means what the docs promise: never published ("session not found") or a malformed id ("invalid session id") — each with its owndetailstring. Access is checked before the410, so401/403behave 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 tomatchup. The live running score onstatus=openrows, the final score onstatus=closedrows,nullwhen 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=NonGET /api/sessions— caps the response to the firstNrows in the status ordering (so?status=closed&limit=25is 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
sportandleaguefields onGET /api/health/{session}responses — lets a caller holding only a bare session id resolve which sport it is without a full/historyfetch. 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 likehelloanderror— not aWireFrame(nooutSeq,state, ormatchup) — so branch ontypebefore 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.
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
/historyresponses may now carry"sport": "football"(league slugnfl). 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:live→open,ended→closed.status=open(still the default when omitted) lists published, still-streaming sessions;status=closedlists published sessions that have since closed (tape still readable via/history).status=live/status=endednow return422.- Catalog row field renames:
createdAt→openedAt(when the session opened) andendedAt→closedAt(when it closed). Same values and same null semantics —closedAtisnullonstatus=openrows, set onstatus=closedrows.sessionId,gameId,league,sport,matchup,publishedAtare unchanged. - Ordering is unchanged in effect:
status=openbypublishedAt,status=closedbyclosedAt(the same timestamp the oldendedAtordered by).
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
sportfield is now guaranteed present on every session row — typed plainstring, nevernull. 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 carriednull), so no version bump — but if your parser typedsportas nullable, you can drop the null branch.
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 byendwith 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 ahome_runrequires), matching the event.
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
sportson theGET /healthresponse — 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 authenticatedGET /api/sessionscatalog.
2026-07-13 — all sports (no bump): sandbox mock sessions
Added
- Sandbox:
POST /api/sandbox/sessionsmints a private mock session — a recorded real game replayed through the production pipeline at a configurablespeed— so you can test your integration end-to-end (catalog discovery, WebSocket frames includingundo/score_setcorrections,session_closed,/historybackfill) without waiting for a live game. One active mock per account; retriggering replaces it. - Catalog: new optional
sandbox=truequery parameter onGET /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
/historyresponse: optional"sandbox": truekey, present only on mock sessions and absent (neverfalse) on real games — existing parsers are unaffected, which is also why no sport’scontractVersionbumps.
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→wceverywhere theleaguefield 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 —leagueis still an open-vocabulary string, so no version bump — but if you filter or compare againstfifa.world, switch towc. 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 with1011. 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 handshake500, 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). TheErrorFramecodeunion 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/subscribeand/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}.phaseis 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 isGET /health(unchanged, still ungated). Their ungatedsessionsid list goes with them — live session ids are listed by the authenticatedGET /api/sessionscatalog.
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 with4404— the same answer as an unknown session id. Do not reconnect; fix the URL. Previously these connects failed the handshake with a bare500(surfacing in a browser as close1006with 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 oldgameIdwas documented as “the session id”).status=live(the default when omitted) is exactly the old/api/games/livebehavior and filtering;status=endedlists 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 otherstatusvalue is a422. Same auth as before: a valid token always required, operator/scout see all, consumers entitled ∪ public. See Sessions catalog.?league=<slug>onGET /api/sessions— optional single-value filter, composable withstatus. 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 returns200with 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;nullfor a manually-created session),createdAt(string | null — when the session was registered;nullfor rows from before 2026-07-10, exact backfill pending), andendedAt(string | null — when the session ended; alwaysnullonstatus=liverows).
Changed
- Catalog field renames: response key
games→sessions; row fieldgameId→sessionId(same value — the{session}segment for/api/subscribe/{session}and/api/history/{session}).league,sport,matchup,publishedAtare unchanged.
Removed
GET /api/games/live— replaced byGET /api/sessions(its defaultstatus=liveslice is byte-for-byte the same filtering); the old path now returns404. NocontractVersionbump: 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 fieldgameId→sessionId(same value — the echo of thesessionpath parameter; the value was always the session id, the name now says so).GET /api/sessions: row fieldcatalogGameId→gameId(same value — the game this session records, shared by all sessions covering the same game;nullfor 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/liveon all three mounts (/basketball,/soccer,/baseball) — the handler was sport-blind, so every copy returned identical cross-sport data. These paths now return404; callGET /api/games/liveinstead. NocontractVersionbump: 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
sporton the WebSocket hello frame and theGET /api/history/{session}response —"basketball"|"soccer"|"baseball", besidecontractVersionin 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 (seecontractVersionabove).leagueon the WebSocket hello frame and theGET /api/history/{session}response — the session’s league slug (e.g.nba,fifa.world), the same value the catalog carries. Session metadata likematchup, sent once on the hello rather than per frame.nullon/historyfor a log written before the session lifecycle. Additive optional fields: nocontractVersionbump, 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}andGET /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’ssportfield 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}andGET /{sport}/history/{session}on all three mounts (/basketball,/soccer,/baseball) — these paths now return404; connect toWS /api/subscribe/{session}and fetchGET /api/history/{session}instead. NocontractVersionbump: 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 gets401("authentication required"/"invalid token") instead of200; send the same token you already use for/api/history. Response shape is unchanged, and the bare/{sport}/healthliveness probe stays token-free — nocontractVersionbump.
2026-07-08 — all sports (no bump): sport on live-games rows
Added
- Each game object in the
GET /<sport>/games/liveresponse now carriessport("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.nullfor sessions registered before 2026-07-08. Additive optional field: nocontractVersionbump, 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
contractVersionstaysv9. Thebasesflags (and theset_basepayload’soccupied) are confirmed-runner claims, not verified occupancy:truemeans the scout confirmed a runner safe on that base;falsemeans no confirmed runner — an absence of a claim (never marked, cleared byplay_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 onplay_hit; the docs previously impliedfalse= 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
basesthat go dark after eachplay_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 treatfalsein 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_baserenamed toset_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_hitnow clearsbases(all three occupancy bits reset tofalse) the instant it fires — a ball in play puts every runner’s safety back in question, so the scout re-marks who’s safe withtoggle_baserather than the feed assuming everyone advances. Previouslyplay_hitleftbasesuntouched. A followinghome_runin the same play still credits every runner who was on base at the moment of the hit; only the wire-visiblebasessnapshot 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, andouts_setare 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 eventtype, nothing you have ever received is affected. - State snapshot fields
balls,strikes, andcurrentBatter— 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
pitchResultsenum key inbaseballContract.generated.json(vocabulary of the removedpitchevent).
2026-07-01 — baseball v6, basketball v4, soccer v4: /history undo events carry retractsSeq
Added
undoevents inGET /historyresponses now carry the server-stampedretractsSeq— theseqof the press the undo cancelled (nullwhen nothing remained, or for a log recorded before this change) — matching what the live WebSocket frame already carried. Strike the row whoseseqmatches instead of re-deriving retractions from a fold.- New
serverStampedPayloadFieldskey in every generated contract: the payload fields the server adds per event type (the scout never sends them) — today{ "undo": ["retractsSeq"] }. Consumer-facing payloads arescoutEventPayloadFields∪serverStampedPayloadFields. No change to live-frame bytes; the/historyaddition 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 statesnapshotFields— fields that were already present on every basketball frame. No change to the bytes you receive. ThecontractVersionbump (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.