What's new

Recent improvements to the Big Balls Sports Data API, in reverse chronological order.

2026-10-14

  • GET /v1/leagues/:id/leaders now serves soccer league leaders. The leaders data was loaded but no route could reach it, so the view behind it could never have answered a request. The route is now wired and returns the stored leader rows.
  • The NHL projected starting goalie now publishes its accuracy next to its coverage. The agreement rate and the number of sides it was scored over are returned as a scoped pair, so you can see how far the projection has been tested rather than reading a bare percentage.

2026-10-13

  • GET /v1/football/matches/:id/shots serves shot-level soccer data, Edge and above. Each shot carries pitch coordinates, expected goals, minute, result, situation, body part and the preceding action.
  • GET /v1/nhl/projected-goalies returns a projected starting goalie with a measured agreement rate of 84.0% across 1,585 scored sides. The projection is derived from our own saves market. It is labelled projected, not confirmed, and the response metadata carries the accuracy figures.
  • NHL power-play units are derived and served per team per game. GET /v1/stored/matches/:id/power-play-units returns the top five skaters on each team's first and second unit, ranked by ice time on the man advantage.
  • NHL skater and goalie expected-goals season splits are served at Edge. GET /v1/nhl/skaters/xg-splits and GET /v1/nhl/goalies/xg-splits return season-level expected-goals detail.
  • GET /v1/nba/players/{id}/season-splits reaches 57,185 rows that no route could fetch before. Filter by season, season type, measure and stat type.
  • Kickoff weather now serves the stored archive before it tries upstream. Historical conditions stay available for a match even when the live lookup cannot answer, so the same request stops flipping between a value and nothing.
  • NBA game kickoff evidence is now servable, and only where sources agree. A kickoff time is returned when two or more independent records agree on it, so a single disputed timestamp is withheld rather than published as settled.
  • Team profile pages are live for 32 NHL teams, 30 MLB teams, five European soccer leagues and MLS. Each profile is captured from the data we already serve.
  • The terms now state that showing the data is not redistributing it, and cover storage, model use and derived outputs. Displaying our data in your product was always allowed; the clause says so explicitly, and the usage-rights section now addresses caching, training and derived figures directly.
  • Two stale claims on GET /v1/matches/:id/statistics are corrected in the docs. The documented coverage and plan for that route no longer overstate what it returns.
  • The dashboard resolves your daily request cap in one place. The quota card and the usage statistics panel read the same resolver, so the two surfaces can no longer show different limits for the same account.

2026-10-12

  • Point-in-time reads land across odds, player props and availability. Pass as_of with a timestamp and you get the state as it was recorded then. A timestamp outside the observed window returns a 422 that names the coverage gap or floor it fell outside, rather than quietly serving the nearest row it could find.
  • GET /v1/nfl/surface-snapshots makes 572,842 stored rows reachable. The data was held and had no route; it now has one, gated to paid plans.
  • Per-player special-teams time on ice is served at Edge. GET /v1/stored/matches/:id/stats now carries power-play and penalty-kill ice time per player.
  • 59,514 NHL play participants are recovered, and the column can no longer go empty. Giveaways, takeaways and similar plays had dropped the athlete they belonged to. They now attribute correctly, and the participant column is NOT NULL so the same gap cannot reopen.
  • GET /v1/leagues/{id} answers for all 820 league display names it publishes. Names that only GET /v1/matches would accept now resolve here too.
  • NBA player props are actually being written. The writer had never fetched, and 275 runs recorded a clean zero instead of reporting that nothing arrived. The fetch is fixed and an empty run now fails loudly.
  • Team profile pages are live for 30 NBA teams.

2026-10-11

  • NHL history reaches back to 1997-98, the earliest season with complete fields. Seasons before that are not served rather than served thin.
  • A league depth request refuses instead of silently handing you a shallower archive. When the full span cannot be assembled, the route says so rather than returning a partial history that looks complete.
  • The discovery surface publishes the deep NBA archive, and every figure names the population it came from. Each count now says what it counted, so a number cannot be read against the wrong denominator.

2026-10-10

  • GET /v1/matches/:id/events distinguishes a second yellow from a straight red. Both used to arrive as the same dismissal event.
  • Historical NHL archive spelling aliases resolve. Older team spellings in the archive now map to the canonical team instead of missing.
  • Historical NHL box-score rows are joinable to a player. The rows carried no usable player link, so per-player history could not be assembled from them.
  • The published NHL history claim is now 29 seasons, back to 1997-98. The figure matches what is loaded.
  • Point-in-time NHL player props are now written on a schedule. The writer did not exist, so no prop history accumulated.
  • Canadian football odds are no longer filed as boxing. One league id was mapped to the wrong sport, which put CFL odds under boxing. Measured availability for boxing and Formula 1 odds is recorded alongside the fix.

2026-10-09

  • 26 seasons of per-game NHL player box scores are in production: 1,296,918 rows, verified.
  • ?sport=cricket on GET /v1/players returned 0 against 13,337 cricket players. The filter now returns them.
  • ?league= on GET /v1/players is an actual filter. It was accepted and silently ignored, so a league request returned the unfiltered set.
  • Three customer-visible wrong-data defects are fixed: cross-sport shot charts, league fixture precedence, and rejected league ids.
  • Predictions serve the 570 value-prop rows' own probabilities, and stop leading with rows that expired 124 days ago.
  • Standings refuse to derive a table their inputs cannot support. A league without the rows needed to order a table now says so rather than publishing an order that the data does not justify.

2026-10-08

  • Cricket ball-by-ball is served on Pro, with historical deliveries backfilled.
  • The NHL post-game lineup writer runs on a schedule. Post-game lineups now accumulate instead of depending on an ad hoc run.

2026-10-07

  • NCAA men's basketball and WNBA player stats are written and served. Both leagues were held with no player stats behind them.
  • ?league= resolves on both /v1/intelligence routes.
  • Invoice persistence survives a parse error. A single unreadable payload used to stop the whole write, so later invoices went unrecorded.
  • Sport hub pages honour sport-specific tiers. A hub no longer quotes a plan that does not apply to that sport.
  • The NBA player stats guide names the right route and a working field example.

2026-10-06

  • Baseball starting lineups attribute each starter to the team they played for at match time. They had been attributed by current roster, so a traded player appeared on the wrong side of an older game.
  • Stale live matches settle on a schedule, and the job reports whether it ran. The worker had recorded a healthy result while never running, so matches could sit frozen in a live state.
  • An image-processing advisory is patched (GHSA-wq5f-xc86-pv6w).

2026-10-05

  • Live odds movements stream over WebSocket on Pro.
  • /live and /graphql/ws refuse with the same shape, and both enforce the realtime plan gate. One transport enforced the gate and the other did not, so the same account could be refused on one and admitted on the other. Both now check, and both return the same refusal body.
  • Stored lineups elect one source per match. Two sources for one match could interleave, so a lineup could mix rows that belonged to different records of the same game.
  • NFL canonical matches are backfilled for 2017, 2018 and 2019.
  • The sports grace window is 7 days, up from 24 hours. A payment problem no longer cuts access the next day.
  • Seven betting calculators are live: arbitrage, parlay, round robin, hedge, Kelly, no-vig and implied probability.
  • The odds embed is available as an iframe.
  • Three dependency advisories are patched.

2026-10-04

  • Every 403 names the plan the account holds. A refusal used to leave you guessing which tier you were on, and the gate behind it now fails on an unknown plan rather than assuming the lowest.
  • UEFA Nations League 2026 and 2026 international friendlies are ingested and served. Two serving paths had been hiding internationals that were already loaded.
  • Both WC2026 team form facts are preserved. One was overwriting the other.
  • The welcome offer triggers on real sport usage.
  • Free-tier and live-edition claims are corrected on five tournament pages.
  • The comparison calculator returns accurate figures.

2026-10-03

  • Active MLB player headshot coverage is now 96.7% complete. We reconciled 42 of 47 missing headshots from existing identities, serving all images through our /v1/assets endpoint.
  • xG writes now report row counts and fail on partial fetches. The writer now wraps in withCronHeartbeat, reports changed, unchanged, and deleted counts, and guards conflict updates with IS DISTINCT FROM to exclude timestamps. Partial fetch failures, including dry runs, will now fail the run.
  • A new /dashboard/signup-trends page provides aggregate signup data. This read-only dashboard includes daily history, cumulative totals, rolling 7-day averages, and breakdowns by sport interest, acquisition channel, and landing page. Access is controlled by an exact server-only allowlist, independent of administrator status.
  • Stripe invoice state is now synchronized with a new stripe_invoices table. This table stores invoice details, subscription and customer joins, status, amounts, and hosted URLs. Delayed webhook payloads will not regress invoice state, and the attempt_count is protected with a database-side GREATEST function.
  • PFL fighter identities are now preserved when multiple fighters share the same name. This change ensures that distinct fighters, such as the two "Justin Clarke" entries, are correctly identified and displayed with unique athlete IDs, preventing data corruption and maintaining accurate fight card information.
  • PFL history now includes 82 events from 2005-12-09 through 2026-12-19. A deterministic batch process loads these events, failing closed on unreviewed same-name fighter identities to preserve data integrity. Odds coverage is now available for 16 of these events.
  • Redis round trips for authed requests are reduced by up to two. The recordResponseForBreaker and breakerCooldownState operations now use a single Lua eval command each, decreasing latency and Redis command usage. Limits and semantics remain unchanged.
  • PFL opening odds now load correctly. A parameter type mismatch prevented historical odds from being ingested for PFL events. This has been corrected by explicitly casting the sportsbook parameter to varchar(50) in SQL.
  • GET /v1/live-stats/{sport}/{matchId}/players no longer returns 502 errors for WNBA, NCAAB, or MLB placeholder matches. Previously, these requests were incorrectly routed, leading to upstream 404s and subsequent 502s. The gateway now correctly identifies and routes requests for WNBA, NCAAB, and MLB, returning appropriate 404s for placeholder events instead of 502s.

2026-10-02

  • PFL results and opening odds are now available. This includes leagues, event-identity cards, bouts, fighter profiles and records, completed results, and historical opening odds, ingested through the existing low-cron worker. Bout finality now overrides stale scheduled event status.
  • MMA ingest now preflights Node database reachability before fetching PFL data. This prevents wasted API calls when the database is unavailable, as seen in prior runs where 166 of 202 calls failed due to certificate issues.
  • The capability probe now inspects response bodies to differentiate between routes that refuse access entirely and fields that are omitted or null within a successful response. This change allows for precise measurement of field-level tier access, addressing a previous blind spot where entitlement claims were unverified for specific data points like expected_goals on /v1/matches/{id}/statistics. New manifest keys, fields_by_tier, field_tier_floors, and field_populated_tier_floors, detail these granular access levels.

2026-10-01

  • MMA fight cards now group by event identity instead of calendar date. This change prevents UFC and PFL events held on the same day from merging into a single card, and adds promotion=ufc|pfl to fight-card and bout routes. Stored MMA result fields are now surfaced without inventing missing results.

2026-09-29

  • NBA history now has one preferred surface: GET /v1/matches?league=nba. It reaches 72,014 of 72,014 unique games, including all 70,611 of 70,611 archive games across 80 seasons and 1,403 canonical matches not present in the archive. The depth panel reports that one route and its 70,745 finished games instead of presenting two overlapping spans. No plan loses access and no reachable row or field was removed.
  • Unknown historical tip-off times are now explicit instead of fabricated. A four-source sweep recovered real timestamps for 25,708 of 69,303 previously date-only games. The remaining 43,595 of 69,303 return kickoff_utc: null and additive time_precision: "date"; timestamped rows return time_precision: "timestamp". Exact-source disagreements remain unresolved and reported rather than being settled by row order or a fake midnight.
  • GET /v1/nba/games remains unchanged through 2027-03-31 and sunsets on 2027-04-01. Migrate during that window to GET /v1/matches?league=nba. We measured 51,621 calls from 118 keys across the affected routes from 2026-09-01 through 2026-09-29; query strings are not retained, so that is a conservative upper bound and we cannot identify in advance which clients assumed kickoff_utc was always non-null.
  • WC2026 group-stage predictions are now written within a single database transaction. This prevents data loss in the event of a failure between the delete and insert operations, ensuring that 56% of the predictions table is not left empty. The previous process involved two independent queries, which could result in deleted rows not being re-inserted.
  • The /v1/matches/:id endpoint now records the requested match ID on 404 responses. This allows for better attribution of errors and debugging of issues where a specific match ID could not be found.
  • New accounts receive a welcome email after their first sign-in. This email explains the platform's purpose, guides users through initial setup, and provides links to the dashboard and Discord. It is sent using existing email infrastructure and frequency controls, and is folded into the offer activation email when eligible to prevent duplicates.

2026-09-28

  • The pitch-level MLB dataset now lives on /mlb-api, and the standalone page it used to have 301s there. One section, Pitch-level data, with every field and its measured row count: 3,000,720 pitches across the 2023, 2024, 2025 and 2026 seasons, 10,184 games, 2,273 pitchers, 2,162 batters, through 2026-09-27. Pitch velocity on 2,988,834, spin rate on 2,976,804, pitch type and plate location on 2,988,186, arm angle on 2,705,971, exit velocity on 990,006 batted balls, launch angle on 991,469, barrel classification on 42,098, expected wOBA on 725,227, bat speed and swing length on 1,129,209, run-expectancy delta on 2,990,453. Measured against production on 2026-09-28. Nothing the old page documented was dropped.
  • Three corrections came with it. The figure on the old page read 2.89 million and its hero read 2.1 million; both were stale, and both were lower than what is actually loaded. The old page also promised "Free Tier, No Card" in its title while saying "Public endpoint in development" twice in its body — we checked production on 2026-09-28 and the route returns 404, so the free-tier promise is gone and the section states plainly that the data is held and not served. And sprint speed and outs above average were listed as coming; neither has ever been ingested, so they are now named as absent instead of promised.
  • Correction to the 2026-06-21 entry below: the 4xx circuit breaker numbers were wrong. The window is a rolling 1 hour, not 5 minutes; the absolute gate is 500 4xx and the ratio gate's floor is 200 requests on Free (a paid plan gets 10x both: 5,000 and 2,000; the 80% ratio itself is unchanged for every plan); and a trip's cooldown is 600 seconds (10 minutes), not 120. These are the numbers the gateway has enforced since the 2026-06-25 retune (docs/4xx-breaker-tuning-2026-06-25.md); the changelog entry below was never updated to match and has read wrong for three months. /docs/rate-limits and llms-full.txt are corrected; this entry is left as originally published, per our policy of correcting forward rather than editing a dated record.

2026-09-24

  • rank on standings is now a real table position, computed by us, for every league. It was the ingest's running counter over the vendor's conference/division tree — a position in the vendor's JSON, not in a table. Measured over the rows production actually served on 2026-09-24: 19 of 29 adjacent NBA pairs had the lower-ranked team holding the better record, 10 of 31 for NHL, 11 of 29 for MLS, 4 of 29 for MLB, 1 of 31 for NFL, and 2 of 137 for NCAAF. GET /v1/standings, GET /v1/stored/standings and GET /v1/teams/{id}/season now compute it at read time over exactly the rows in the response, so it is contiguous 1..N and the rows array is returned in that order. Affected served values in 10 of 12 loaded league-seasons; 336 API keys called one of those three endpoints successfully in the preceding 30 days.

  • NHL and NCAA football standings carry a rank for the first time. Their sources publish no league-wide rank — the NHL feed writes a conference rank and the NCAAF feed writes none — so rank was null on all 32 NHL rows and all 138 NCAAF rows. It is now populated on both. Nothing was removed to do this: the row count, the team set and every other field are identical, and the stored column every ingest writes is untouched.

  • The ordering and the tiebreak are stated, not implied. Association football leagues (including MLS and the Champions League) order on league points — 3 for a win, 1 for a draw — then goal difference, then goals for. Every other sport orders on win_pct as that league defines it, which for the NHL is points percentage rather than wins over games. Ties on the ordering metric are broken by more wins, then fewer losses, then team name, then team id, so the same rows always produce the same table and level teams get adjacent distinct positions rather than a shared one. Full description in the rank field of the OpenAPI spec.

  • The five big European leagues did not change at all, and that is the check. Their vendor rank was already correct on league points, and 0 of 96 rows across the Premier League, La Liga, Serie A, the Bundesliga and Ligue 1 moved. The first measurement of this defect read those leagues as clean on win percentage — which they could not help but be, since win_pct is null on every football row, so the scan compared 0 pairs. Re-run on the metric those tables are actually ordered by, the control both fires and passes.

2026-09-21

  • Match events now have one dependable response shape. GET /v1/matches/{id}/events always returns data as an array, including valid empty states, so integrations no longer need separate parsing branches for the same endpoint.

  • Documented query parameters behave more predictably. Boolean play filters now distinguish false from true; GET /v1/injuries?team= accepts a team id, name, or code consistently; and parameters that are intentionally non-filtering are identified in response metadata or documentation instead of silently implying that they changed the result.

  • GET /v1/games reports the sport and filter scope it actually used. Game rows round-trip the canonical sport rather than a formatting label, filters_applied.league distinguishes a league alias from an all-sport request, and unsupported ?season= requests return a clear 400 with the supported date-filter alternative instead of silently returning the current window.

  • Date-filtered history stays inside the requested sport's season boundary. Routes that derive historical access from ?date= now use each sport's real season calendar, including January rollover cases, so a date request cannot accidentally reach a different season through a calendar-year shortcut.

  • The TypeScript SDK can call the complete documented API surface. The generated operations module covers all 134 of 134 documented operations with typed request parameters, path validation, canonical query encoding, and caller-controlled cancellation. 68 of 134 operations also have contract-derived response payload types today; the remaining response schemas are reported honestly rather than asserted as complete.

  • API-key rotation preserves paid access more reliably. A newly issued key uses the stronger of the account's active subscription and its inherited key entitlement, preventing a rotation or temporary subscription-read failure from silently downgrading an otherwise valid paid account. This change only protects access; it does not reduce anyone's existing entitlement.

  • New implementation guides cover more real sports-data products. The tutorials now include soccer betting models, fantasy applications, live match centers and scouting workflows, plus NCAA football betting and game-condition analysis.

  • NCAA football fixture identity is cleaner without breaking saved links. Fifty-four duplicate scheduled records were consolidated into their completed fixtures, historical odds remained attached to the canonical games, and all 54 retired match UUIDs continue to resolve through aliases.

  • Coverage and operational visibility are more precise. Priority NFL, NCAA football, NHL and soccer leagues are separated in the admin visibility view, while refreshed all-sports, image, capability-freshness and workflow-health measurements make missing, stale and unavailable data easier to distinguish.

  • Platform security and legal discoverability improved. The web application stack was updated past its outstanding security advisories, and the licence-notice page is now indexable, included in the sitemap and linked from the shared footer. No API entitlement or response payload changed as part of that work.

2026-09-18

  • GET /v1/stored/players?league= now returns the league's current participants, and the count goes down. The league filter joined through teams.league_id, which is timeless identity metadata rather than seasonal competition membership: a club keeps that pointer after it leaves the competition. Measured in prod on 2026-09-18, ?league=epl returned 775 player records across 23 clubs while the latest stored 2026-27 standings contain 20 clubs — 102 of those 775 records were players whose clubs are not in the current season. Asking for Premier League players and being handed players who are not in the Premier League is wrong data, not extra data, so the filter is now bounded to the clubs in the league's latest standings season: 673 records across 20 clubs. The season that counts as current is chosen by played rows and parsed start year, never by string order on the season label. A league we hold no standings for is unchanged rather than becoming empty. ?team= and unfiltered requests are unchanged, and no roster row was rewritten.

  • This narrows a result the 2026-08-31 entry said would not change, and we cannot tell you whether it affected you. That entry stated that the /v1/stored/* league resolver meant "nothing that worked before has stopped working". That was true of the resolver and is no longer true of the result: a ?league= request that worked on 2026-08-31 now returns a smaller set. We are not able to say whether anyone depended on the removed clubs — usage_events records the route and the key but records no league and no season, so the question is not determinable from our own logs, and we would rather say that than imply the impact was measured.

  • The earlier population is still reachable: name the season. GET /v1/stored/players?league=epl&season=2025 returns the 2025-26 participants — 775 records across 23 clubs, the exact set the unqualified filter used to return. ?season= here is the same parameter, with the same meaning, as everywhere else in the API: the year the season STARTS. We did not invent a new spelling for it. It requires ?league=, because without a league there is no standings table to bound by, and a season we hold no standings for is a 400 that names the seasons we do hold rather than an empty 200 you cannot interpret. The other route to a historical directory is unchanged: GET /v1/teams?league=<code> still resolves through teams.league_id and still returns all 23 clubs, and GET /v1/stored/players?team=<team id> per club is untouched.

  • The response now states the scope it was computed under. GET /v1/stored/players returns meta.filters_applied with the sport, team, league, season and q actually applied, plus league_scope on a league-filtered request: { "mode": "latest_standings_season", "season": "2026-27", "leagues": [{ "league_id": "…", "seasons": ["2026-27"] }] }. mode is requested_season when you sent ?season=. The labels are reported per league, because a filter resolving to more than one league — or a season carrying two labels, as the NHL's does — cannot be described by one string; season is populated only when exactly one league resolved to exactly one label. An empty seasons array is a league we hold no standings for, which is not narrowed rather than returned empty. You can tell those cases apart from the response instead of inferring them from a count. meta.current_participants and meta.historical_directory name the other direction, whichever one you are not in.

  • A Premier League headshot that 502'd now falls back instead of failing. One stored upstream image URL began returning 403 at its origin, which our own asset proxy surfaced as a 502. A player whose headshot is on that retired host is now served a licensed image derived from their existing vendor identity, or null when no safe fallback exists. This is asset-proxy resilience, not an identity repair — the affected player record was verified correct against production, and no identity was merged, split or rewritten.

  • NHL game detail is deeper and its leaderboards are more reliable. GET /v1/nhl/games/{gameId}/plays?shooting_only=true returns shot events with distance, angle, computed xG, rebound, rush and empty-net fields when available. GET /v1/nhl/games/{gameId}/box-scores adds goalie lines and even-strength, power-play and short-handed splits. Counting-stat requests to GET /v1/nhl/leaders no longer fail on a missing query alias, and explicit historical seasons on GET /v1/nhl/team-leaders follow the existing historical-data entitlement.

  • Retired match UUIDs continue to resolve without bypassing access rules. Match, odds, statistics, play, prediction and weather reads resolve a known retired UUID to its canonical match before querying, while keeping each route's existing plan and sport gates. The consolidated Pittsburgh-Syracuse fixture now uses c21d6de7-6e30-439b-9f29-9767d7d4a224; requests using its retired UUID, c90b7dc6-0b3e-46cc-9619-62fcc941a56e, resolve to that record.

  • Cricket match lists accept an inclusive date range. GET /v1/cricket/matches?date=YYYY-MM-DD:YYYY-MM-DD filters by kickoff across both calendar days, uses UTC unless ?tz= supplies an IANA timezone, and echoes the exact applied window in meta.date_window. Backwards ranges return a 400, while pagination continues through the existing limit, offset and total fields.

  • The coverage page renders again and describes NCAAF season statistics accurately. /coverage no longer fails when loading its refresh-cadence declarations, and it now labels NCAAF player-season statistics as partial instead of omitting that data type.

  • La Liga event previews handle unavailable play-by-play without crashing. /la-liga-api accepts both stored event arrays and live availability envelopes. A fixture with no captured events now shows an explicit unavailable state instead of throwing or presenting an unexplained empty play-by-play panel.

2026-09-17

  • Soccer leaderboards no longer choose different default seasons. When ?season= is omitted, GET /v1/leagues/{id}/top-scorers and GET /v1/leagues/{id}/xg-leaders now use the newest season present in both datasets. Pass ?season=YYYY explicitly whenever you need to reproduce an older result.

2026-09-16

  • World Cup tournament odds return reliably under load. GET /v1/odds?tournament=wc2026 now reads the compact current-lines store instead of reducing the full historical ledger on every request. The response contract is unchanged, while the query no longer runs into the gateway timeout.

  • Weather coverage gaps are now data, not misleading 404 errors. GET /v1/matches/{id}/weather returns 200 for a valid match even when its venue or forecast is unavailable, with forecast: null, a stable unavailable_reason, and a human-readable note. A 404 now means only that the match itself does not exist.

  • Date filters can follow the caller's local calendar. Add an IANA timezone such as ?tz=America/New_York to date-filtered match, score, odds, cricket, fight-card, game, and schedule-context routes. UTC remains the default, and meta.date_window shows the exact bounds applied.

  • GET /v1/teams/{id} returns a real team profile without the long empty wait. The route now serves the stored team and record directly, shares its statistics logic with /v1/teams/{id}/stats, and returns a genuine 404 for an unknown team id.

  • Live NFL plays are selected by kickoff window instead of stale match status. GET /v1/live-stats/american_football/{matchId}/plays can follow an in-progress game even when its stored status has not caught up, and a live game with no captured plays reports that failure instead of looking like a legitimate empty result.

  • NFL team-stat rows now include a derived defense object. GET /v1/nfl/team-stats adds sacks, sack yards, interceptions, forced fumbles, blocked kicks, points allowed, and passing, rushing, and total yards allowed, derived from the opponent and defender records already held for the game.

  • Live box scores preserve the team a player represented in that game. GET /v1/live-stats/ice_hockey/{matchId}/players groups historical players by the fixture side they played for instead of their current club. Empty football and hockey box scores also include player_count, team_count, and status-aware guidance about whether polling again can help.

  • Public player and odds responses use canonical identities consistently. Live player ids, World Cup player ids, weather source labels, and sportsbook values are normalized before they reach customers. GET /v1/players also excludes aggregate placeholder rows and trims player names.

  • League pages deliver complete visual context again. NCAAF leaders can include player headshots, soccer events include player and assist identities with headshots, and repaired league crests remain present across leaderboards and event views.

2026-09-15

  • Webhook registration now has a secure delivery contract. POST /v1/webhooks creates a subscription and returns its signing secret once, deliveries carry an HMAC signature, failed attempts retry, and POST /v1/webhooks/{id}/unpark re-enables a destination after it is repaired.

  • Scheduled plan changes are visible to both people and integrations. During the final seven days before a scheduled change, responses include X-BBS-Access-Ends-At, X-BBS-Access-Ends-In-Days, and meta.access_ending, so an application can react without depending on email.

  • Every applied ?date= filter explains its UTC window. Stored match, score, odds, cricket, fight-card, and schedule responses include meta.date_window with the inclusive start and exclusive end used by the query. This makes midnight-boundary behavior explicit instead of making an evening game look missing.

  • Match and player responses stay scoped to the requested record. GET /v1/matches/{id} uses that game's live score, /v1/players returns named athlete rows rather than aggregates, and cricket scorecards attribute each innings by batting identity rather than a name prefix.

  • Soccer batch refreshes preserve newer match state and UTC kickoffs. Live and finished states take precedence over an older scheduled snapshot, and Bundesliga kickoff times are stored consistently in UTC.

  • Public media and identifiers are safer to embed. Team crests and player headshots can be served through the platform's asset route, top-scorer responses project canonical player ids, and sportsbook values use canonical public names.

2026-09-14

  • Live football plays carry more of the action. The live play response now includes field-goal distance and outcome, plus the canonical player id for the kicker when one can be resolved.

  • GraphQL match details now reach their statistics, events, and lineups. Match resolvers pass the fixture identifier expected by those loaders, fixing calls that previously declined every request even when the match data existed.

  • Live box scores name only the teams in the fixture. Team attribution is constrained to the match's home and away sides, preventing unrelated clubs from appearing in a two-team box score.

  • NCAAF leader rows link cleanly to player resources. GET /v1/ncaaf/leaders exposes the canonical player.id when the athlete is resolved, and live ingestion reuses existing player identities instead of creating duplicates.

  • API discovery now follows the contract automatically. The /v1/ endpoint index is generated from the OpenAPI operations, bringing previously undiscoverable documented routes into the index. /v1/coverage also reports play-by-play using only rows that can actually be served.

  • Seasonal subscription pauses are self-service. The dashboard can schedule a pause for an upcoming offseason and shows the effective dates before confirmation, while the API continues to report the account's current state accurately.

2026-09-13

  • Live sports now have a dedicated score resource and a reliable stream. GET /v1/scores exposes the live-score payload directly, while /live supports raw WebSocket clients without a second upgrade handler corrupting the connection. Stream messages include polled_at and emitted_at so clients can measure delivery latency themselves.

  • GET /v1/matches has one response envelope for every sport. Baseball, boxing, soccer, and the other supported sports now return the documented match array instead of switching to a score-shaped object according to runtime conditions. GET /v1/matches/{id}?sport=... also falls back to the stored match rather than returning 404 simply because the game is not on today's live board.

  • NFL and NCAAF play-by-play can be consumed while the game is live. GET /v1/live-stats/american_football/{matchId}/plays serves the in-game feed, while GET /v1/nfl/games/{gameId}/plays gives a clear empty-data meta response when no stored plays exist.

  • NFL live player stats are available through the football live-stats surface. GET /v1/live-stats/american_football/{matchId}/players returns group-qualified player fields, avoiding collisions between values such as passing and rushing yards.

  • NFL data is deeper across offense, defense, special teams, and game context. Public player and team statistics add defensive, kicking, return, fumble, and interception fields, while live updates add down, distance, possession, and the current play. Venue, roof, and kickoff-condition data provide the context needed for weather-aware analysis.

  • NCAA basketball tournament brackets are queryable end to end. GET /v1/ncaab/tournament reliably serves the full bracket with canonical school identities, seed, region, round, and a truthful season hint.

  • NCAAF fantasy leaders have a dedicated endpoint and product surface. GET /v1/ncaaf/leaders returns current leaderboards derived from player statistics, alongside standings and schedule examples on the NCAAF API page.

  • NHL play-by-play and raw box scores are now served. GET /v1/nhl/games/{gameId}/plays and /v1/nhl/games/{gameId}/box-scores expose the stored game detail through documented routes.

  • GET /v1/stored/matches returns again instead of failing during its count query. The count path now joins the same bout data referenced by its filters, restoring the endpoint without changing the response shape.

  • Current MLB box scores are available through the canonical stats routes. Player and team batting and pitching lines now flow through the same public match and statistics surfaces used by the rest of the API.

  • Injury responses for basketball, hockey, and football are more complete and reliable. Scheduled refreshes were restored, football roster availability is presented alongside the weekly injury report, and snapshots fail closed instead of silently replacing a newer view with an incomplete one.

  • Pagination and empty-result behavior are predictable. ?page= now either paginates or returns a clear validation error instead of being ignored, ?offset=0 is accepted as the first page, and valid empty play responses explain whether the game is unknown or simply has no captured data.

  • Customer-facing identifiers are canonical and source-neutral. Match, player, team, sportsbook, event-narrative, and documentation responses translate internal source keys and branded values to platform identities before delivery.

2026-08-31

  • The /v1/stored/* endpoints now take the canonical league code, and an unknown league says so instead of returning nothing. GET /v1/stored/players, /v1/stored/matches and /v1/stored/standings matched the league's DISPLAY NAME and nothing else, so ?league=La Liga returned rows while ?league=laliga returned an empty array with a 200 and no explanation — indistinguishable from a league we genuinely hold no rows for. Measured on all three endpoints on 2026-08-31. They now resolve through the same shared resolver /v1/standings, /v1/teams and /v1/leagues/{id}/* already use, which accepts the canonical key, the brand alias AND the exact display name, so nothing that worked before has stopped working. An unrecognised league is now a 400 naming the value you sent, with the list of valid codes, rather than a silent empty 200. The 2026-08-27 entry below said /v1/teams?league= takes the canonical key "like every other league-filtered endpoint" — these three were the endpoints that disagreed with that sentence, and now they don't. League codes are listed at /docs/league-codes.

    • Added 2026-09-18; the text above is unchanged. The sentence "nothing that worked before has stopped working" described the league RESOLVER and remains true of it, but it no longer describes the RESULT of GET /v1/stored/players?league=. See the 2026-09-18 entry at the top of this page for what changed and how to request the previous population.
  • ?season= now means the season, not the calendar year. Row counts will change on most sports, and the old numbers were wrong. A season query filtered on the calendar year of kickoff, which is correct only for leagues whose season starts and ends inside one year. For everything else it returned roughly half of one campaign and half of the next. Measured on 2026-08-31 for ?season=2025: NBA returned 1,310 matches of which 767 (59%) belonged to another season, EPL returned 378 of which 192 (51%) did, and Serie A, La Liga, Bundesliga and Ligue 1 all sat between 54% and 56% wrong. NHL failed differently and worse: it returned no wrong rows at all and 55% of the season missing — 626 of 1,391 — because a season starting in October only overlaps a calendar year for three months, so the response was complete-looking, internally consistent and half a season. There was no way for a caller to tell. ?season=2025 now means the campaign that STARTS in 2025: the 2025-26 season for the EPL, NBA and NHL, and the 2025 season for MLB, MLS and WNBA, whose seasons genuinely are calendar years and whose results are unchanged. Each league's own start month is resolved from the same registry the season labels come from, so a league that plays May to August is not forced into a rollover it does not have. This affects GET /v1/players/{id}/stats, GET /v1/teams/{id}/matches, GET /v1/teams/{id}/stats and GET /v1/matches wherever ?season= is sent. If you were compensating for the old behaviour — requesting two years and merging, or discarding rows outside a date range — you can stop, and you should, because that compensation now double-counts. The parameter is documented per endpoint in the OpenAPI spec; one route previously described the old behaviour in its own docs as "returns only the 2025 half. Documented as-is", and documenting it did not make it usable.

2026-08-28

  • NBA and NHL injury rows have stopped updating, and updated_at is how you tell. The upstream provider feeding these two stopped returning data. GET /v1/injuries?sport=basketball and ?sport=ice_hockey still return 200 with rows, but the newest NBA row carries an updated_at of 2026-07-26 and the newest NHL row 2026-07-01, so those responses describe a moment months back rather than today. Read updated_at on each row before treating a status as current. Soccer injuries are unaffected and still current. The 2026-06-02 entry below announced these two as live and refreshed every 30 minutes, and that is no longer true.

  • NFL injuries, depth charts and team stats are now served. Three endpoints, 64,425 rows, every one of which used to 404. The tables were being filled daily and nothing read them. GET /v1/nfl/injuries returns the league's mandated weekly injury and practice report (34,810 rows, seasons 2020 through 2025); call it bare and it resolves to the newest week, which it names in meta.resolved. GET /v1/nfl/depth-charts returns each team's open chart intervals, or the chart as it stood at a past instant with ?as_of=; rows carry open_interval rather than current, and meta.season_covered tells you which season the chart belongs to, because labelling a chart "current" is a claim about a season that has not started. GET /v1/nfl/team-stats returns team-week offensive totals including EPA and CPOE. All three carry meta.data_as_of from the row's own updated_at, and the writer only rewrites a row that actually changed, so that timestamp ages honestly when a feed goes quiet. Coverage reports these as off_season until Week 1 on 2026-09-09, not live. See /nfl-api and /injuries-api.

  • GET /v1/injuries?sport=american_football returns data instead of a 404 that contradicted itself. It answered "we do not serve injuries for 'american_football' yet" while the suggested_fix in the same payload listed american_football as live, with 34,810 rows sitting in the table. It now serves the NFL rows through the unified endpoint. Two upstream team abbreviations disagreed with ours (LA and WAS), which served team: null for those two franchises; both are mapped. baseball has been removed from the supported list on the same endpoint, because no baseball injury table exists and it was passing the gate only to 404 with a suggested_fix that excluded it.

  • Soccer injuries are current again across six leagues, MLS included, and ?league= narrows to one competition. Two defects were stacking. The season the ingest asked for rolled over in September for every league, which is right for European clubs and wrong for MLS and Brasileirão, whose seasons are calendar years: on 2026-08-27 that fetched MLS season 2025, returning a complete, plausible, nine-month-stale set that looks exactly like success. And as_of was computed from fetched_at, which the ingest re-stamps on every row every 30 minutes, so the endpoint reported today's timestamp over fixtures that had ended on 2026-05-29, 91 days earlier. as_of now anchors to fixture_date, so it can report stale when the data is stale. Measured today, all six leagues hold absence records against fixtures through 2026-08-30: EPL 3,264, La Liga 3,010, Serie A 2,921, Ligue 1 2,743, Bundesliga 2,473, MLS 2,381. ?league= previously resolved and was then ignored by the query, so every league returned the same rows; each league now gets its own rows and its own as_of. Supported values are EPL, La Liga, Serie A, Bundesliga, Ligue 1, MLS and UEFA Champions League; anything else is a 400 that lists them rather than a silent widening to all of them.

  • New: a page per league for soccer injuries. /premier-league-injury-api, /la-liga-injury-api, /serie-a-injury-api, /bundesliga-injury-api and /ligue-1-injury-api. Each one reads its own league's record count and newest fixture date at render time and prints them, so a page states how current that league actually is instead of asserting a refresh rate. They count absence records, not injuries: the reason field holds 134 distinct strings in production and no pattern splits injury from suspension without leaking in both directions, so the pages say the thing that is true of every row.

  • Combat sports no longer serve summed judges' scorecards as a final score. The cards are now published as the result they always were. matches.linescore holds judges' totals for MMA and boxing, not period points, and the score was being summed anyway: a UFC bout scored 30-27 by all three judges was served as score: {"home": 90, "away": 81}. Nobody wins a fight 90-81. On 43 finished bouts in production, score is now null with score_status: "unavailable", and two new fields carry the real answer: judge_scores republishes the cards, and decision gives winner, method (unanimous_decision, split_decision, majority_decision or draw), rounds_scored and judges. A bout that has scorecards went the distance by definition, so this is a result we already held rather than an inference. decision is omitted when the cards are not readable, because suppressing a wrong score must not depend on being able to derive a right one. Cancelled bouts also stop serving a placeholder 0-0, which for a fight is indistinguishable from a real draw. The 2026-07-25 entry below said MMA serves fight cards, matchups and odds but not results; for these 43 decisions that is no longer true, and the fighter records and non-decision finishes it also named are still not available. See /mma-api, /ufc-api and /boxing-api.

  • New: /about, who builds this and why it is published the way it is. The signup, key and call counts on it are read live rather than typed, on the page whose argument is that we publish what we can measure. It is the human counterpart to /coverage and /attribution.

2026-08-27

  • GET /v1/teams?league= takes the canonical league key, like every other league-filtered endpoint. It matched the league's display name and nothing else, so ?league=La Liga returned 23 teams and ?league=laliga returned an empty array with a 200 and no explanation. It now resolves through the same shared resolver /v1/standings and /v1/leagues/{id}/* use, which accepts the key, the brand alias and the exact display name, so nothing that worked has stopped working. An unknown league now returns a 400 naming the value you sent instead of a silent empty 200. The 2026-07-16 entry below said sport and league naming resolves through one canonical registry everywhere; this endpoint was the exception, and now it isn't. League codes are listed at /docs/league-codes.

  • GET /v1/standings no longer returns clubs that are not in the league. La Liga served 23 rows for a 20-team league, with three second-division sides sitting at games_played: 0 and holding ranks that were already taken: rank 6 was both Celta Vigo, with 38 games played, and Deportivo, with none. Anyone rendering rank got a Segunda side in La Liga's sixth place. Every loaded European league had the same shape. A row is now dropped only when its rank is non-null, it has no games played, and another team holds that same rank in the same league and season with games played, so a genuine mid-season tie survives and a table at season start survives whole. Verified live today: La Liga, EPL and Serie A return 20 rows, Bundesliga and Ligue 1 return 18, ranks contiguous from 1 with no duplicates. Leagues that store every row at rank null, such as the NHL and NCAAF, are untouched.

  • La Liga, Serie A, Bundesliga, Ligue 1 and college football are now polled for live scores. Four of our six soccer competitions and both college football divisions were absent from the score worker's league list, so they were never polled: across 60 days of finished matches, the worker had settled zero La Liga, Serie A and Ligue 1 games, and zero NCAAF games ever. Two things follow from a league being polled. Matches in those leagues now report status: "live" while they are being played, where scheduled previously covered both "not started" and "in progress" and a healthy in-play match was indistinguishable from a stuck one. And they settle promptly rather than late. College football needed more than a list entry: FBS and FCS arrive from one feed whose default response is capped at 25 events, so both divisions are now requested explicitly. See /ncaaf-api and /soccer-api.

  • The four soccer league pages now render real per-league data: xG boards, top-scorer boards and the upcoming fixture list. /la-liga-api, /serie-a-api, /bundesliga-api and /ligue-1-api each publish that league's own standings table, seasons held, per-data-type coverage, endpoint examples with that league's real ids, and now a ten-row expected-goals board with npxg, xa and goals-above-xG, a ten-row scorer board, and the next fixtures we hold. Every figure is read from the API at render time. One thing to know if you are calling these yourself: /v1/leagues/{id}/xg-leaders and /v1/leagues/{id}/top-scorers key on the hyphenated league code where /v1/standings and /v1/matches take the concatenated one. serie-a and ligue-1 return 200 on the boards; seriea and ligue1 return 404. laliga and bundesliga are the same either way. Send ?season= explicitly on /v1/leagues/{id}/top-scorers: it defaults to 2025 and will keep serving 2025 after the rest of your data has moved on.

  • Cricket coverage now names 13 competitions instead of 2, and GET /v1/cricket/matches/{id}/scorecard appears in a coverage surface for the first time. Cricket is our deepest sport by competition count, 768 competitions and 23,484 matches from 2001 to 2027, and the coverage manifest named two leagues of it. The scorecard endpoint serves full ball-by-ball batting and bowling cards for 22,539 finished matches, 97.8% of the sport, and was documented nowhere at all. Both are now in every generated surface, including the ones assistants read. See /cricket-api.

  • /docs/odds now lists the sportsbooks we actually carry: 25 of them, 6 reaching us from two independent providers. Pinnacle, Matchbook, ProphetX, Novig, Kalshi, Polymarket and Bet365 are among them. The list is generated from live odds joined to the book reference table, so a book we hold a reference row for but no longer receive is not published as carriage, and a build check fails when the committed copy drifts. The six two-source books are the set the two_source_agreed verification described in the 2026-07-13 entry below can actually corroborate.

2026-08-11

  • NCAA FCS (Football Championship Subdivision) is now live — a separate league from NCAAF, sharing the same American Football sport. GET /v1/matches?sport=american_football&league=ncaaf-fcs covers 139 FCS teams and a decade of regular-season history (2016-2025, weeks 1-15 — the FCS postseason playoff is not loaded yet). GET /v1/players/:id/stats now returns real per-game box-score stats for FCS players too — strong for 2021-2025 (88-99% of games), sparse before that: the upstream source's own historical FCS box-score collection doesn't reach back past 2021, a source limit, not a gap on our end. End-to-end across the 7,113 games that touch an FCS team: about 58%, 1,087,323 player_stats rows across 42,761 players. No standings, Elo ratings, or odds for FCS yet.
  • FBS-vs-FCS "buy games" are no longer missing from NCAAF (FBS) — 1,024 of them now carried. These were previously excluded entirely (2026-08-08 entry below) because the FCS opponent had no resolvable roster. GET /v1/matches?sport=american_football&league=ncaaf now returns 9,204 games (up from 8,180), and shows up on either team's own record via GET /v1/teams/:id/form and GET /v1/teams/:id/h2h-intelligence. Per-game player-stat coverage for FBS rose from ~87% to 93% of finished games as a result (4,092,224 rows across 8,188 games).

2026-08-08

  • NCAAF history is now a full decade, not one season. GET /v1/matches?sport=american_football&league=ncaaf now returns 8,180 games spanning 2016 through the 2026 schedule, up from 2025-only. GET /v1/players/:id/stats now returns real per-game box-score stats for NCAAF, live for 2016-2025 and covering about 87% of FBS games played in that span — 3.5M player_stats rows across 7,166 games. The uncovered ~13% is a structural gap, not partial rollout: FBS-vs-FCS "buy games" aren't carried at all today, since FCS opponents are out of scope.

2026-08-04

  • NFL Elo ratings are live and queryable. GET /v1/teams/:id/elo now returns real ratings for all 32 NFL teams, backed by a full historical recompute (3,386 elo_history rows across the 2020-2025 seasons). This was blocked until today by a same-city team-linking bug: the 2025 season had every New York Giants game attributed to the Jets and every Los Angeles Chargers game attributed to the Rams, so an Elo computation would have rated the wrong participants. The bug is fixed (both in the ingest code and in the 34 already-wrong match rows), so ratings now reflect who actually played.
  • The dashboard shows your API key and first call before anything else. The key reveal and the ready-to-run first-call card now render as the first thing you see after signup, ahead of onboarding prompts and account setup. If you've felt like the key was buried under other UI on your first visit, it isn't anymore.
  • Signup now captures which sports you're building for, from more than one signal. The platform reads both an explicit intent you declare and attribution from how you arrived (landing page, referrer), and supports declaring interest in more than one sport. This powers more relevant onboarding and, going forward, more relevant product updates for the sports you actually use.
  • New: an agent-oriented quickstart on the dashboard. Alongside the existing curl-based first call, the dashboard now offers a copy-paste snippet built around the @bigballsdata/sdk npm package, aimed at developers wiring the API into an AI agent or IDE assistant rather than a terminal. Proof-of-concept, feedback welcome.
  • Homepage and landing pages no longer reference the 2026 World Cup as current. With the tournament over, the marketing pages that featured live/upcoming World Cup framing have been updated to reflect that it's concluded.

2026-08-03

  • Gateway errors now return a real 503 for transient database issues, not a raw 500. A momentary connection-pool exhaustion or statement timeout previously surfaced as an unhelpful generic 500. It now returns 503 with retry guidance, which is the honest signal for a transient condition worth retrying.
  • GET /v1/matches?status= accepts the status vocabulary from our other endpoints, not just its own. upcoming, not_started, in_progress, ongoing, final, and ft are now aliased to this endpoint's canonical scheduled/live/finished values, so a term that already works on /v1/wc2026/matches or live-stats no longer 400s here. The error message for a genuinely invalid value now names the exact value you sent.
  • Marketing pages now pull their sport and league counts from GET /v1/coverage, live, instead of a hardcoded number. Pages that advertise "N sports" no longer drift out of sync with what the API actually serves.
  • New: /tools/odds-calculator, a free, no-signup odds converter and payout calculator. Convert American, decimal, and fractional odds, see implied probability, and calculate payout on a stake, entirely client-side. A documented reference implementation of the exact conversion math, not a live odds feed.
  • Background workers no longer silently stall on a Redis hang. The hourly job that closes out stale live matches, and the live-stats and score-update workers, are now resilient to a dead or slow Redis connection. Previously a Redis hang could leave finished games showing as live far longer than expected; both are now isolated so one failing dependency doesn't block the others.

2026-07-27

  • New route_not_found error code, plus sport-prefixed route aliases. A request to a path that doesn't exist now gets a distinct route_not_found (404) instead of being confused with a valid-route-but-missing-resource error. Common URL-guessing mistakes for leagues, matches, and countries now resolve via alias instead of 404ing.
  • GET /v1/sports and GET /v1/coverage no longer require an API key. Both are now public (IP rate-limited), so you can explore what the API covers before you ever sign up.
  • Errors for a mismatched player ID now name the real cause. Passing a bdl_player_id or af_player_id from our own /v1/matches lineups or /v1/teams/{id}/squad responses into /v1/players/{id} now says so explicitly, and points you at /v1/players?name={name} to get the right Big Balls id first. The same clarity now extends to invalid, non-UUID ids passed to team, NBA lineup, and prediction endpoints, with a suggested_fix pointing at the right discovery endpoint.
  • /v1/matches/:id/odds and /v1/matches/:id/events now fall back to stored data instead of returning nothing. Previously these returned null if live odds or events weren't available, even when the data existed in storage. The odds endpoint now falls back to aggregated, vendor-free stored odds; the events endpoint serves stored match events for non-soccer sports before attempting a live fetch.
  • New: activation recovery emails. Signups with no API call in their first 24 hours, or a high error rate across their first several calls, now get an email with a working example tailored to their declared sport and common failure patterns.

2026-07-26

  • Sport entitlements are now enforced, not just modeled. Roughly 119 routes now gate access by your plan: request a sport your plan doesn't include and you get a 403 naming the fix. NBA play-by-play (GET /v1/nba/games/:gameId/plays) and shot coordinates (GET /v1/players/:id/shots) are gated as one Edge-tier unit, and /intelligence/value-finder plus WebSocket connections narrow to your entitled sports rather than blanket-403ing.
  • MLB scores now agree between list and detail views. Scheduled games no longer show a fabricated 0-0 in one view and null in the other; both now correctly show null before kickoff and the real score once the game starts.
  • Finished and live matches derive their score from linescore, not stale match_stats. A match with a complete linescore no longer risks showing 0-0 because a separate stats field hadn't caught up. /v1/matches and /v1/matches/:id now sum the per-period linescore when it's available.
  • /v1/odds/divergences no longer republishes verbatim vendor odds. The raw per-provider price fields are gone from the response, in line with vendor redistribution terms. The computed disagreement signals (category, secondary_categories, magnitude) are unaffected, still our own data.
  • Catalogue endpoints no longer claim confidence: 1 for an unknown source. /v1/sports and /v1/leagues now correctly report source: 'internal-catalog' for our own data; a genuinely unknown source now reports confidence: 0 instead of a misleadingly confident 1.
  • NCAAF coverage claims now match what's actually there, and every sport must declare its data types explicitly. The /ncaaf-api page states real figures (91% of finished games carry multi-book odds, no college football play-by-play, box scores for roughly 13% of games), and the coverage manifest now requires every sport to declare each data type rather than allowing silent gaps. A new ratings data type reports live for NBA and NHL, partial for soccer.
  • Cricket team lookups now search across every league in the sport, not just one. Previously a team lookup was scoped to a single league, which could mint a duplicate team when two data sources filed the same club under different league ids.
  • GET /v1/matches/:id no longer hangs for up to 30 seconds when live enrichment is slow. Each field fetch is now capped at 2 seconds; on a timeout or missing live coverage, the endpoint serves the stored row with an honest coverage note instead of leaving you waiting.
  • /v1/matches/:id/odds and /v1/matches/:id/events no longer require ?sport=. Both now infer the sport from the match id itself.
  • NFL team identity bug fixed at the source. The New York Jets/Giants and Los Angeles Rams/Chargers were being resolved to the same team by a same-city name match with no franchise qualifier. New 2025-season data now links to the correct team; see 2026-08-04 above for the historical data repair.
  • Boxing fighter bios now available: GET /v1/athletes?sport=boxing. Fight cards and bout results are still coming soon and are marked as such; profile data (name, nationality, physicals, weight class) is live today.

2026-07-25

  • MLB games no longer show a fabricated 0-0 before kickoff. A scheduled game's linescore now correctly returns null instead of a placeholder {"home":[0],"away":[0]} that read as a real, in-progress score.
  • Email sign-in accepts the full code length Supabase issues, not just 6 digits. The sign-in form previously hardcoded a 6-digit expectation and truncated or rejected longer codes before they reached Supabase. It now accepts the full 6-10 digit range and passes the code through unmodified, fixing sign-in for anyone who received a longer code.
  • MMA coverage now states what it actually serves: fight cards, bout matchups, and odds, not results. The /mma-api and /ufc-api pages previously implied bout results, fighter profiles, and win-loss records were available. They aren't yet; both pages and the coverage manifest now mark results as coming soon rather than claiming them.
  • Formula 1 coverage now states what it actually serves: the 2026 calendar, nothing else yet. No scores, stats, or historical data exist for F1 today; the /formula-1-api page and coverage manifest no longer imply otherwise.
  • New: a boxing results parser (dry-run only). Parses official commission result documents for bout results, method, round, and judges' scorecards, successfully extracting 13 events and 108 bouts from 2025 test data. No database writes yet; results are scoped to US-sanctioned bouts (Nevada first), not bouts sanctioned outside the US.

2026-07-24

  • TypeScript SDK: install @bigballsdata/sdk (0.1.1). The SDK now ships under its correct name — npm install @bigballsdata/sdk (or pnpm install @bigballsdata/sdk). This release fixes the two bugs that made the earlier @bigballsports/[email protected] unusable: the client now defaults to the correct base URL https://api.bigballsdata.com (0.1.0 defaulted to a domain we don't own, which parked instead of answering), and the published package now resolves on install — its main/types/exports point at the shipped dist/, so import { BigBallSportsClient } from '@bigballsdata/sdk'; works from a clean project. One import, every sport. If you installed @bigballsports/sdk, switch to @bigballsdata/sdk — the old package is not maintained.

  • Sign in from a different device than you requested the link on. Email sign-in now also gives you a 6-digit code. Request the link on your laptop and you can finish signing in on your phone (or the other way round) by typing the code, instead of being forced to open the link on the exact device that requested it. The emailed link still works as before — the code is just an additional path for when the link lands on the wrong device.

  • New: a licence notices page. It carries the exact credit notices our data licences require.

  • The /hockey-api and /nhl-api pages now show real, working examples. Both pages were rebuilt so every endpoint and code sample is copy-paste-runnable against live data — 12 seasons of NHL coverage, xG leaders, and samples that actually return what they show. If a sample on those pages didn't work for you before, it does now.

2026-07-19

  • New: GET /v1/leagues/{id}/xg-leaders, expected-goals leaderboards for the big-five European leagues. One call returns a season xG leaderboard for epl, laliga, serie-a, bundesliga, or ligue-1 — the same league codes /v1/leagues/{id}/top-scorers already takes. Each row carries xg, xa, npxg, xg_chain, xg_buildup, goals, assists, shots, key_passes, matches, minutes, and a derived goals_above_xg (goals minus xG) so you can see finishing over- and under-performance directly. Sort with ?stat=xg|xa|npxg|goals|assists|shots|key_passes (default xg), and narrow with ?season=, ?min_minutes=, ?limit=. History runs back to season 2014, well before our match coverage starts, so this is usable for long-range model training. Refreshed daily.

    Two things to know before you build on it. Rows are keyed by player_name, not by a BigBalls player_id — the entity bridge between our xG source and our players table is not built yet, so we do not offer per-player or per-match xG endpoints and you should not join these rows to /v1/players/{id} on identity. And any season row we cannot attribute to a league is excluded from the board rather than bucketed somewhere convenient, so a leaderboard only ever contains players from the league you asked for.

2026-07-16

  • New: GET /v1/coverage, one call that tells you exactly what we have. A single request now returns a full, DB-derived coverage snapshot: every sport and league with data, the canonical name plus aliases, a copy-paste example_call, the seasons held, freshness (data_through, next_event, last_ingested), and a per-datatype breakdown (scores, player_stats, player_season_stats, odds, player_props, team_stats, and more) with a coverage percentage and a derived status (current, in_season, historical, thin, none) for each. It is generated from the actual rows, not a hand-maintained list, so it cannot drift from reality: a datatype we do not have reports an honest 0% rather than a false badge. Narrow to one sport with ?sport=, which accepts the canonical name or any alias. This is the fastest way to see what is worth integrating before you write a line of code.

  • NCAAF player season stats are now reachable. GET /v1/players/{id}/stats?sport=american_football now returns college football players' season stat lines (passing, rushing, receiving, and more), grouped by season with a level: "season" label so you always know you are reading season aggregates, not a per-game log. This surfaces a large body of NCAAF season data that was in the store but had no discoverable path to it.

  • Consistent sport and league names across the API. Sport and league naming is now resolved through one canonical registry, so the same name and aliases work everywhere. GET /v1/sports now returns all nine covered sports (cricket, MMA, boxing, and Formula 1 are now visible alongside the rest), and unknown values like ?sport=rugby now return a clean 400 instead of an empty result. If you were special-casing name variants on your side, you can rely on the canonical names and aliases now.

2026-07-13

  • verification is now evidence-based — the values changed. On GET /v1/odds/historical and GET /v1/odds/closing-lines, the per-line verification field previously returned two_source_verified whenever we had no divergence on record. That was wrong: no divergence row usually means we never compared that line, not that two providers agreed — so a badge could appear on a line we never actually checked. It now returns one of four honest statuses: two_source_agreed (we compared two providers and they matched — backed by a real comparison record), two_source_diverged (we compared and they disagreed, with divergence_categories; this replaces divergence_flagged), single_source (only one provider carries this book/side, or the book has no cross-provider match — not verified), and unverified (we have not compared this line — not verified). If you were treating two_source_verified as "trusted", note that most of those lines now correctly report single_source or unverified; only two_source_agreed is a positive cross-check. Documented at /docs/odds.

2026-07-09

  • Unknown player ids now return 404, not an empty 200. GET /v1/players/{id} used to answer with a 200 and meta.coverage set to false in two different situations: a player who exists but has no data for the requested sport yet, and an id that matches no player at all. The second case now returns a 404 with error code not_found and a suggested_fix that points you at /v1/players?name= to look the player up by name. A real player who simply has no data for the requested sport still returns the same 200 with meta.coverage false as before, because that is a legitimate answer and not an error. This is a small error-contract change: if you were treating an empty 200 on this endpoint as "not found", you can now rely on the status code instead.

  • World Cup knockout fixtures now consolidated to one row each. GET /v1/wc2026/matches could previously return two rows for a single knockout game, with odds and scores split across them. Each knockout fixture is now a single canonical row carrying its full odds history, correct teams, and final score, so match and bracket reads are complete and consistent from the Round of 16 onward.

  • GET /v1/stored-matches/:id now returns complete current odds. The endpoint reads current lines from a dedicated latest-odds table, removing the previous 500-row cap and reporting the correct bookmaker count. The head-to-head field is also corrected for h2h matchups.

  • Mobile layout fixes on the marketing site. The navbar and hero no longer cause horizontal scrolling on small screens, and code samples now scroll within their own blocks instead of stretching the page.

2026-06-28

  • World Cup bracket odds are faster and no longer time out under load. GET /v1/wc2026/matches reports the current moneyline for all 104 fixtures. It used to compute that from the full append-only odds history (tens of millions of rows), which during a burst of live writes in the Round of 32 could run past two minutes and fail with a 500. Current odds now read from a compact table that holds one row per match, market, book, and side, refreshed as each new line is written. The endpoint returns in well under 100ms and stays up while odds are streaming in fast. The response shape is unchanged, and the line-movement and historical odds endpoints still read the full history, so nothing there changed.
  • Paused API keys now return a clear 403. If we pause a key, which is different from revoking it, requests get a 403 with error code KEY_PAUSED and a plain-language message, so you can tell a temporary pause apart from a revoke or a quota 429. It takes effect within the 60-second key cache. Most keys will never see this; it exists so that on the rare occasion we pause a key, the response says exactly what happened instead of failing in a confusing way.

2026-06-27

  • Golden Boot leaderboard now returns real scorers. GET /v1/wc2026/golden-boot was returning an empty leaders[] even with matches played, because it read a topscorers blob whose ingest job was never scheduled. It now aggregates the top 20 scorers from per-match fixture player stats (the table that is actually refreshed daily). Players tied on goals share a rank (DENSE_RANK), and the list is ordered by goals, then assists, then fewest minutes. Each leader carries country (the national team's full name, for example "Argentina", not an ISO code), team_id/team_name, a nullable player_id, and goals/assists/minutes. See /docs/wc2026.

2026-06-26

  • World Cup knockout rounds now read from the result, not the calendar. GET /v1/wc2026/matches was labeling each knockout match's bracket.round by inferring it from the kickoff date. That broke whenever the schedule shifted. The handler now reads the authoritative FIFA stage stored on the match, so the round value is correct regardless of date. Possible values: R32, R16, QF, SF, THIRD (third-place playoff), FINAL. Same change also filters out a small number of phantom placeholder rows that could appear before the group stage resolves. If you were mapping rounds yourself from dates, you can stop and trust bracket.round.
  • Dashboard quota card v2. The free-tier usage card on your dashboard now shows a circular gauge of today's percentage of the daily cap, with four bands (under 50% is just the number, 50-79% a soft notice, 80-99% a warning, and at/over the cap a clear "throttled until 00:00 UTC" state). Every band shows your real usage against the next tier's real cap and price, so the upgrade math is explicit, not a nag. Dashboard only; no API change.
  • World Cup 2026 player data is flowing reliably. The WC2026 ingest worker (the pipeline behind tournament player and team data) is running stable ahead of the Round of 32. No endpoint or shape change; this is an operational note that the WC2026 player surfaces are kept current as the tournament progresses.

2026-06-21

  • Abuse protection: per-key 4xx circuit breaker. Each API key now has a circuit breaker that watches its client-error (4xx) rate, separate from the request quota. Over a rolling 5-minute window it trips if a key sends 500 client-error responses, or if 80% or more of at least 50 requests are 4xx. On a trip the key gets 429 for a 120-second cooldown, even if it has quota left. This is abuse protection, not a punitive throttle: successful traffic never counts toward it, so normal usage is unaffected, and it clears itself once the broken requests stop. A breaker 429 carries Retry-After, X-RateLimit-4xx-Cooldown, and X-RateLimit-4xx-Reset headers so you can tell it apart from a quota 429 (which carries neither). If you see it, read the error.message and suggested_fix on your 4xx responses and fix the request rather than blindly retrying. Documented at /docs/rate-limits.

2026-06-20

  • Soccer team match statistics. Every finished match now carries a full per-team stat line: possession, shots (on/off target, total, blocked, inside/outside box), fouls, corners, offsides, cards, goalkeeper saves, passes (total, accurate, pass %), and expected goals (xG) where the competition provides it. Fetched from the upstream feed within ~30 minutes of full-time and stored, so reads are fast with no upstream wait. Available on GET /v1/matches/{id}/statistics (Pro plan) for the top-five European leagues, MLS, and the Champions League. The linescore stays authoritative for goals; this fills the team stat line that match previews, recaps, and models need.
  • Odds now tell you how verified they are. Every line on the historical and closing-line endpoints (GET /v1/odds/historical, GET /v1/odds/closing-lines) now carries a verification field. two_source_verified means both of our odds providers priced that book, market, and side and they agree. single_source means only one provider priced it (or the book has no cross-provider match). divergence_flagged means the two providers disagree, and a divergence_categories list tells you why (PRICE_DIFF, LINE_DIFF, MISSING_FROM_ONE, STALE). It is the two-source cross-check made visible on every line, so you can see at a glance which numbers earned two independent confirmations and which to treat with caution. The verification field is live on both endpoints, and GET /v1/odds/divergences (Pro plan) returns the current disagreements directly. Documented at /docs/odds.

2026-06-19

  • Two-source odds verification. We now run a cross-source check every 15 minutes, comparing our two independent odds providers for the same match, market, and book across the verified overlap books (BetMGM, DraftKings, FanDuel, Bovada, BetOnline, Lowvig) and flagging where they disagree. Four categories: PRICE_DIFF (the juice differs), LINE_DIFF (the line itself differs), MISSING_FROM_ONE (only one provider carries it), and STALE (one feed is lagging). Surfaced via GET /v1/odds/divergences (Pro plan). Game lines only. This is the Honesty Architecture principle as a feature: it catches stale lines, upstream errors, and sharp-money asymmetries before they reach you.

2026-06-18

  • Historical odds, opening line to closing line. We are now capturing historical odds snapshots for completed matches: the opening line (about a week before kickoff), the 24-hour line, and the closing line, each stamped with the board's as-of time (snapshot_type and snapshot_at on odds data). Coverage reaches back to 2020 and is rolling out across the major sports (NBA, NHL, MLB, NFL, and the top soccer competitions). This is the raw material for closing-line value, line-movement analysis, and sharp-money signals.
  • Closing lines as games start. Phase 2 captures the closing line for upcoming games as they near kickoff: a 5-minute cron snapshots the current board in the roughly 10-minute window before the start as the closing line (snapshot_type closing, snapshot_at = capture time). Closing-line coverage grows continuously from here, not only through backfill, which is exactly what closing-line value (CLV), line-movement intelligence, and sharp-money detection depend on.
  • Current odds across 17 sports and leagues. Phase 3 polls the live odds board every 30 minutes for NFL, NCAAF, NBA, NCAAB, WNBA, MLB, NHL, the top European soccer leagues (EPL, UEFA Champions League, La Liga, Bundesliga, Serie A, Ligue 1), MLS, MMA, and ATP/WTA tennis. This broadens current-odds coverage well beyond the World Cup, on a roughly 30-minute refresh.
  • Query historical odds and closing lines. Two endpoints are now live: GET /v1/odds/historical (Edge plan) returns every snapshot for a match (opening, 24-hour, closing) for closing-line-value work, and GET /v1/odds/closing-lines (Edge plan) returns closing lines across a sport and date range. Each odds row carries the snapshot type, the as-of time, and the sportsbook.

2026-06-17

  • World Cup matches endpoint stays lean under load. GET /v1/wc2026/matches now loads only the latest line per book and participant when it refreshes (about 3,200 rows instead of the full multi-million-row odds history), so a burst of traffic no longer spikes memory during the recompute. Same response shape, same odds.
  • Clearer errors on bad input. A malformed ?date= on GET /v1/wc2026/matches now returns a clear 400 that names the expected format instead of failing in a confusing way, and every sport-filter validation error across the API now carries a suggested_fix telling you exactly what to change.
  • We measure before we claim. Last week we added a database index to speed up that matches query. Under real production load the planner ignored it, so it carried a write cost without the promised speedup. We removed it and reframed the work as the memory fix above, which is what actually held up. We would rather correct a claim than keep a number that does not survive a real query plan.

2026-06-16

  • World Cup event data is complete. GET /v1/matches/{id}/events now reads across both the soccer and World Cup event tables in a single query, so goals, cards, and substitutions come back for World Cup 2026 matches (including the Stanley Cup and knockout fixtures that previously returned an empty list), not just the league tables it covered before.
  • Champions League and MLS scores route correctly. GET /v1/matches?league=ucl and ?league=mls now resolve to the right upstream instead of returning nothing or Premier League data. UCL and MLS fixtures are queryable through the unified matches endpoint, the same way the European leagues already are.
  • Playoff series state on NHL and NBA matches. Match responses now carry a series block for postseason games (game number in the series, per-side wins, and the leader), so you can render "Game 6, Carolina leads 3-2" straight from the match payload without stitching the series together yourself. Live and upcoming games are accurate now; a fix for finished-game series state on the match-detail route is in progress.
  • Self-service errors on 36 more endpoints. The suggested_fix field, which lands on a 4xx when the fix is unambiguous, now covers 36 additional response sites across the API. More malformed requests now tell you exactly what to change, no support ticket required.
  • No more slow World Cup cold starts. GET /v1/wc2026/matches and GET /v1/wc2026/standings now serve from cache instantly and refresh in the background (stale-while-revalidate). A cache expiry no longer makes one unlucky caller wait several seconds for a recompute, so tail latency on these endpoints is gone.
  • Endpoint response contracts documented. A new reference spells out the exact response shapes for GET /v1/wc2026/matches and GET /v1/predictions: the { total, matches } envelope (not a bare array), the knockout placeholder rows and how to detect them, the Solo-plan requirement on predictions, and the client timeout we recommend. Read it before you parse either response.
  • World Cup per-player tournament stats. GET /v1/wc2026/players/{id}/tournament returns a player's aggregated tournament totals (goals, assists, cards, minutes) with a per-match event breakdown.

2026-06-12

  • Top scorers by league. GET /v1/leagues/{id}/top-scorers returns the scoring leaders for the Premier League, La Liga, Bundesliga, Serie A, and Ligue 1, with goals, assists, minutes, and matches per player. Sort with ?category=goals|assists|minutes|matches. Team names resolve through a direct join, so every row carries a clean club name.
  • Kickoff time on predictions. GET /v1/predictions responses now include kickoff_utc. You no longer need a second call to the schedule endpoint just to get the match time.
  • World Cup matches are cache-hardened. GET /v1/wc2026/matches no longer returns an empty list during a deploy or a brief upstream hiccup. It never caches an empty response, serves the last known-good snapshot when a refresh comes back empty, and warms on boot. The intermittent empty results some callers saw around launch-day deploys are gone.

2026-06-11

  • FIFA World Cup 2026 is live. Every fixture across groups A through L, with the schedule, live group standings, match predictions, value picks, odds, and Elo ratings. Start at /world-cup-2026-api.
  • World Cup scores land at full-time. Final scores now populate within a cron tick of the final whistle, usually a minute or two after the match ends. The scores worker now reads the upstream soccer linescore shape correctly.
  • Consistent post-match data. The post-match pipeline (events, Elo, standings, grading, and group advancement) now reads scores from a single source of truth, matches.linescore. Those surfaces no longer disagree in the minutes right after a result lands.
  • Errors that help you fix them. Every 4xx and 5xx now returns a suggested_fix (when the fix is unambiguous), a docs_url, and a request_id, so you can self-serve most integration mistakes without opening a ticket.

2026-06-10

  • Soccer player club form. GET /v1/players/{id}/club-form returns a player's club season stats (appearances, goals, assists, minutes, rating) across the top 5 European leagues (EPL, La Liga, Bundesliga, Ligue 1, Serie A) for 2025-26. 3,853 players covered, and WC2026 squad players link automatically.
  • Multi-season club form. Add ?season=2024 to scope club form to a single prior season for career context. Omit it for every captured season.
  • NHL 2025-26 season complete. All 1,389 NHL matches this season, including all four Stanley Cup Final games, are now fully linked and queryable end to end.

2026-06-09

  • NBA shot chart API. The shot chart page is live at /nba-shot-chart-api. Pull any player's 2025-26 shots with court X/Y coordinates, shot type, and make/miss via GET /v1/players/{id}/shots (235k+ shots and counting).
  • NFL and soccer API pages enriched. Added query-targeted FAQs to the NFL and soccer API landing pages, answering the integration questions developers search for most.
  • Security hardening. Authentication redirect validation tightened.

2026-06-08

  • WC2026 live group standings. GET /v1/wc2026/groups returns all 12 groups (A-L), 4 teams each, updated within 5 minutes of each matchday result. Group advancement probabilities (Monte Carlo, Elo-driven Poisson model, 10k sims) populate from June 11. The Monte Carlo model is calibrated against 301 WC matches (2002-2022): G=2.73 goals/match, C=186 Elo/goal.
  • Player trophy history. GET /v1/players/{id}/trophies returns a football player's career honours from the upstream football database, with a top-level total_trophies win count. Cristiano Ronaldo has 54 titles, Lionel Messi 65. Football-only; non-soccer players return an empty list.
  • Team playoff records. GET /v1/teams/{id}/playoffs returns an NBA team's playoff record by season (the Boston Celtics went 16-3 in 2023-24).
  • Player transfer history. GET /v1/players/{id}/transfers returns a football player's club moves with fees. Kylian Mbappé: Monaco to PSG for €180M, then to Real Madrid on a free.
  • NBA shot coordinates. GET /v1/players/{id}/shots returns a player's shot log with court coordinate_x/coordinate_y, shot_type, and made/miss. 235k+ shots from 2025-26 including the Finals. Free throws are excluded by default (?include_ft=true to keep them). Build a shot chart or a shooting heat map straight from the JSON.

2026-06-07

  • NBA player game log. GET /v1/players/{id}/gamelog returns flat per-game NBA stat lines, most recent first (last 20 games by default, up to 50). Filter by postseason and season.
  • WC2026 squads. GET /v1/wc2026/teams/{id}/squad returns a nation's full 26-player squad, ordered by position group (goalkeepers, defenders, midfielders, forwards) then shirt number.
  • National team World Cup history. GET /v1/teams/{id}/world-cup-history returns a nation's tournament-by-tournament record from 2002 through 2022: finish, games played, win/draw/loss, and goals for/against.
  • Team Elo history. GET /v1/teams/{id}/elo/history returns a team's Elo trend line, a baseline seed plus one point per settled match, so the rating grows as matches play out. Optional from / to date filters.
  • NBA player career averages. GET /v1/players/{id}/career returns season-by-season career averages (7 NBA seasons, 2019-20 through 2025-26), one row per season and team. NHL players get the hockey equivalent.
  • WC2026 line movement. GET /v1/wc2026/matches/{id}/line-movement returns the chronological odds history per market and book, so you can chart how a line moved.

2026-06-06

  • TypeScript SDK published. @bigballsports/[email protected] is live on npm: pnpm install @bigballsports/sdk or npm install @bigballsports/sdk. One import, every sport. Superseded by @bigballsdata/[email protected] (2026-07-24) — install @bigballsdata/sdk instead. @bigballsports/[email protected] had a wrong default host and a broken install and is no longer maintained.
  • Developer blog live. The WC2026 API launch guide is at /blog/wc2026-api-launch: what's live, what's coming, and 3 example calls to get started.
  • NHL historical standings captured. Past-season NHL standings for 2021 through 2024 (128 rows) now load into the canonical store, the groundwork for season-over-season standings comparisons.
  • NHL season averages from raw box scores. Per-player NHL season averages (4,861 rows) are now computed directly from the captured raw box scores, so the matchup's per-player season numbers stay current without leaning on a stale upstream stats feed.
  • WC2026 prediction calibration and value-finder persistence. A grader now scores the Elo match predictions against actual results to track calibration, and value-finder opportunities are written to storage (57 predictions persisted so far) instead of recomputed on every request.
  • WC2026 knockout bracket fully structured. All 32 knockout fixtures from GET /v1/wc2026/matches now carry a bracket block with round, home_feeder, and away_feeder. Projected team names stay suppressed until the groups resolve; the feeder description shows instead (for example "Winner Group A").
  • WC2026 squad data ingested. All 48 World Cup squads (1,248 players) are loaded, the data behind the upcoming GET /v1/wc2026/teams/{id}/squad endpoint.
  • WC2026 team tournament history computed. 141 tournament records across the 2002 through 2022 World Cups are now in the store (France champions in 2018, Argentina in 2022), for historical context on each nation.
  • Historical season guard. Requesting a past season the API does not hold (for example ?season=2023) now returns an explicit error instead of silently falling back to current-season data.

2026-06-05

  • World Cup 2026 odds are live. GET /v1/odds?tournament=wc2026 returns moneyline, handicap, and totals for all 104 WC2026 fixtures. Mexico vs South Africa (the June 11 opener) has lines now; the remaining 103 matches populate as sportsbooks open their markets.
  • NHL goalie stats split by game type. The matchup now exposes separate regular-season and playoff save percentages for each goalie. Frederik Andersen sits at .931 across 12 Stanley Cup Final games, Carter Hart at .924 across 16 Western Conference Final games.
  • NBA active rosters live. All 30 NBA teams are seeded in roster_memberships (530 players, 2025-26), so the NBA matchup builds lineups from the authoritative active roster instead of the historical player pool.
  • Games endpoint accepts date ranges. GET /v1/games?sport=nba&date=recent returns the last 7 days of finished games in one call, and date=YYYY-MM-DD:YYYY-MM-DD range queries are supported up to 31 days.

2026-06-04

  • NBA starting lineups live. Starters and positions for all 1,291 games of the 2025-26 NBA season, including playoffs, via GET /v1/nba/games/{id}/lineups.
  • NBA playoff player data fixed. Lineup and box score gaps for postseason games (Conference Finals, NBA Finals) are resolved via direct upstream game bridging. All playoff games are now queryable via the lineups and stats endpoints.
  • Matchup endpoints shielded from cache stampede. GET /v1/nba/games/{id}/matchup and GET /v1/nhl/games/{id}/matchup now collapse concurrent cache-miss recomputes into a single database query, so a burst of requests at the cache-expiry boundary no longer piles onto Postgres.
  • Play-by-play groundwork. NBA and NHL play-by-play events (including shot coordinates) now ingest into the canonical store, the foundation for upcoming shot-quality and possession analytics. No public endpoint yet.

2026-06-03

  • World Cup 2026 knockout bracket is queryable. GET /v1/wc2026/matches now returns all 104 fixtures, the 72 group matches plus the full Round of 32 through Final bracket. Knockout matches carry null teams until the group stage assigns them after June 28.
  • NHL matchup rosters come from the roster store. GET /v1/nhl/games/{id}/matchup now builds active rosters from the new roster_memberships table instead of inferring from recent box scores, so full squads render (the Vegas Golden Knights roster is complete again).
  • Player-prop value finder. GET /v1/intelligence/value-props flags player props where the empirical hit rate beats the de-vigged market line. window=upcoming (default) covers the next 24 hours; window=all runs a historical backtest with record, ROI, and average edge. Markets: points, rebounds, assists, points_rebounds_assists, threes.

2026-06-02

  • NBA Finals ingested. New York Knicks vs San Antonio Spurs, with the full series schedule loaded and Game 1 on June 3. Scores and GET /v1/nba/games/{id}/matchup resolve for every game of the series.
  • NHL playoff series object. GET /v1/nhl/games/{id}/matchup now returns match.stage plus a series block: the game number in the series, per-side wins from finished games, and a human-readable series_record (for example "NYR leads 2-1" or "Series tied 1-1"). It is null on regular-season and preseason matchups.
  • Postseason games staged correctly. Every NBA and NHL postseason game now carries the right stage tag (playoffs vs regular), so playoff fixtures are no longer mislabeled as regular season on live pages or in the matchup series block.
  • NHL player props live, with hit rates. 5,168 prop rows loaded across 5 sportsbooks and 6 markets: shots_on_goal, goals, assists, points, blocked_shots, and power_play_points. GET /v1/nhl/players/{id}/props?market=... returns the current line across books (best book per side) plus a rolling last-N-games hit rate for that market. Hit rates resolve across both player-id twins, so they fill in regardless of which id you start from.
  • NBA player props backfilled. 14,373 historical prop rows loaded across 9 sportsbooks, giving GET /v1/nba/players/{id}/props deeper line history behind its hit rates.
  • Player injuries live for NBA and NHL. 127 NBA and 102 NHL players currently on the report, refreshed every 30 minutes. GET /v1/injuries?sport=basketball|ice_hockey returns the full league list (one row per player, deduplicated) and GET /v1/players/{id}/injury returns a single player's status, injury type, expected return date, and note. Both join on the same player_id you use everywhere else.
  • Matchup responses now carry injury status. GET /v1/nba/games/{id}/matchup and GET /v1/nhl/games/{id}/matchup surface each player's current injury status inline, so the preview and the health check arrive in one call.
  • NHL box scores, captured in full. Per-game NHL skater logs keep backfilling into the canonical store (this is what powers the new prop hit rates), and the ingest now captures the complete raw box-score payload (all 37 fields the upstream returns), not just the subset surfaced today. Storage is cheap; missing data is permanent.
  • NBA advanced, general, and clutch stats captured. Per-season NBA general, advanced, and clutch stat splits are now loading into the canonical store, the groundwork for richer NBA player and team analytics.
  • Standings refresh within 60 seconds of a final whistle, across all 11 leagues. An event-driven worker picks up the match:finished signal and re-ingests standings on the next 30-second tick, down from a daily cron that could lag up to 24 hours.
  • World Cup 2026 knockout bracket loaded. All 32 knockout fixtures (Round of 32 through the Final) are in the schedule alongside the 72 group matches, and GET /v1/wc2026/matches now returns the full 104-match bracket. Knockout matches carry null teams until the group stage assigns them.
  • World Cup 2026 tournament leaders. GET /v1/wc2026/leaders returns the top scorers (rank, goals, assists, and games played per player) with an explicit tournament_state: pre_tournament before the June 11 opener, then in_progress, then finished.
  • World Cup 2026 weather live for all 16 stadiums. GET /v1/matches/{id}/weather returns the match-time forecast (temperature, precipitation, wind, condition) for every WC2026 venue.
  • NHL trending players. GET /v1/intelligence/trending?sport=nhl now surfaces hot and cold NHL players by goals, assists, and shots, mirroring the NBA trending signal.
  • Three more sportsbooks. Rebet, BetParx, and Betway are now in the books reference, so their lines surface in odds and props alongside the existing books.
  • Provenance labels corrected. The meta.source field on the weather and golden-boot endpoints now reports accurate provenance.
  • Seven new sport-level API pages. Dedicated landing pages for basketball, hockey, baseball, American football, cricket, boxing, and Formula 1, each documenting the sport's endpoints and cross-linking to the underlying league pages.
  • Data-health monitoring. An hourly job now checks that marquee data is present and correctly tagged: a championship game exists during each title window, no game is mislabeled regular-season inside a postseason window, and high-stakes games are not single-sourced. Gaps surface to us before they reach a live page.
  • Roster history groundwork. A new roster_memberships table records player-to-team membership over time, the foundation for accurate historical rosters and lineups.

2026-06-01

  • NHL Nerd-view reaches NBA parity. /v1/nhl/games/{id}/matchup now surfaces the same depth as the NBA composite: multi-book moneyline odds per sportsbook, team Elo ratings, goalie save percentage, penalty kill percentage, season statistical leaders via the sibling /v1/nhl/leaders endpoint, and a goalie depth chart. The underlying Elo seed runs across every NHL team, so /v1/teams/{id}/elo and the value-finder answer for ice_hockey today.
  • NHL standings fix. Vegas Golden Knights now returns the correct VGK short_name across /v1/standings?league=nhl, team profiles, and every downstream intelligence endpoint that joins on team identity. Corrected at the database so consumers see consistent Vegas data without a client-side patch.
  • NHL standings deduplicated. /v1/standings?league=nhl no longer surfaces duplicate team rows. Ten team rows that carried stale duplicates were corrected at the database; downstream consumers see one row per franchise.
  • Homepage coverage matrix caught up. The Ice Hockey Predictions cell on the homepage matrix flipped from "Coming soon" to "Live" to reflect today's NHL Elo seed shipment. Five other stale cells were audited and filed for a separate accuracy sweep.
  • WC2026 hero countdown. /world-cup-2026-api shows a live days/hours/minutes ticker counting down to the June 11 opener, refreshes every minute, collapses to "Tournament live now" once kickoff lands.
  • WC2026 Golden Boot endpoint. GET /v1/wc2026/golden-boot returns the tournament-wide top-scorers leaderboard with explicit lifecycle state: pre_tournament before June 11 (leaders empty), in_progress during the tournament, finished after the final.
  • Live win probability endpoint. GET /v1/wc2026/matches/{id}/live-probability returns pre-match Elo baseline, live-adjusted probability, and percentage-point delta per outcome. 15-second cache.
  • Per-sport coverage index at /docs/sports. New page listing every supported sport with its coverage level (Live, Historical, Partial), three to five key endpoints, and a link to the per-sport landing page. Useful for picking a starting point when integrating a new sport.
  • NHL Player Props teaser at /nhl-props-api. Endpoint shipping for the Stanley Cup Finals. The page documents the planned shape (five markets: shots_on_goal, goals, assists, points, blocked_shots), the cross-book aggregation contract, and lets agents integrate against the schema before launch.
  • WC2026 Soccer Props teaser (historical). This was announced for the World Cup 2026 opener but did not ship. The review page at /soccer-props-api is retained without a launch date while product decides whether to repurpose or retire the premise in #2326; it is not an active API offering.
  • MLB pitch-level teaser at /mlb-api#pitch-data. 2.1 million pitches loaded across the 2023, 2024, and 2025 seasons. Barrel rate, xwOBA, exit velocity, launch angle, and bat tracking (2024+) live in the database today; sprint speed and OAA pending. Public endpoint in development. (This entry originally pointed at a standalone page of its own. Only the name and the link were changed, on 2026-09-28, when that page was folded into /mlb-api — see the 2026-09-28 entry. Every figure is as first published.)
  • The competitor calculator now reflects real tier pricing. The Compare/calculator page (/compare/api-request-calculator) models a competitor's actual seven-tier plan structure (Free through Max, $0 through $2,499/mo) with per-second rate, monthly data-points quota, real-time vs delay, and WebSocket availability shown as chips on the competitor card. Calculator picks the cheapest fit against both peak rate and monthly cap.
  • NHL landing page now links to the props teaser via a new FAQ entry, and /v1/nhl/leaders joins the staticEndpoints list to match the EndpointExplorer entry.
  • MLB landing page retracts the stale "pitch-by-pitch granularity is not currently exposed" claim. The updated FAQ now references the 2.1M-pitch pitch-level load and points at the pitch-level teaser. Added a pitch-level research card to buildIdeas.
  • NHL EndpointExplorer caught up. Added /v1/nhl/leaders entry (full field tree with rank, player, team, ice time, goals/assists/points/shots, xG, goals above xG). Expanded the matchup field tree with the three new fields shipped today: books[] for multi-book moneyline, season_stats.pk_pct for penalty kill percentage, and players[].season_avg.save_pct for goalie save percentage.
  • This page is live. /whats-new is the granular feed of recent improvements. Bookmark it. New entries land at the top whenever something ships.

2026-05-31

  • UCL + MLS live data shipped. /v1/standings?league=cl|mls and /v1/matches?sport=football&league=cl|mls now return real data via the upstream live-data partner (were misrouting to Premier League or returning empty).
  • Champions League and MLS API pages live. /champions-league-api and /mls-api now surface 6 entries each: DB-backed fixture archive plus live standings plus live matches.
  • Pre-tournament form populated. /v1/wc2026/matches/{id}.pre_match_form is now filled in BEFORE kickoff (was empty until ~2h pre-kickoff). Last 5 fixtures per team computed from the upstream football data partner.
  • /v1/matches season auto-defaults per league family. European-season leagues (cl, epl, laliga, bundesliga, serie_a, ligue1) and calendar-year leagues (mls) get sensible defaults. Override with ?season=YYYY.
  • /v1/matches pagination now works. ?page and ?limit apply as a post-filter slice. Previously silently ignored.
  • /v1/matches/{id}/events shape fix. Response now correctly keyed data.events (was data.stats). 16-second coverage:false latency also resolved.
  • Footer API Status badge. Green health dot plus "API Status" label in the marketing footer, linking to the live UptimeRobot status page.
  • Task #80 closed. CPV vs KSA kickoff time confirmed stable at 2026-06-27T00:00:00.000Z.
  • Auth resilience. Malformed API keys fast-reject before I/O. Legacy bbs_live_<48char> key format accepted alongside the modern <slug>_<hex> format.

2026-05-30

  • NFL EndpointExplorer. /nfl-api now ships an interactive 7-entry explorer covering schedules, plays (with EPA + Win Probability), and weekly player stats.
  • MLB EndpointExplorer. /mlb-api ships a 10-entry interactive explorer.
  • WC2026 group advancement. /v1/wc2026/group/{A-L}/advancement (per-team 1st/2nd/3rd/4th probability) surfaced in the WC2026 explorer.
  • Match weather endpoint covers all 16 WC2026 stadiums. Kickoff temperature, precipitation, wind, and normalised condition.
  • NHL xG loaded. Per-skater season summary loaded (Dataset #36). Goalie GSAx tier branch added.
  • Single-flight recompute prevents Postgres stampede at the cache TTL boundary on /v1/wc2026/standings and /v1/wc2026/matches.

2026-05-29

  • Standalone team Elo endpoint. GET /v1/teams/{id}/elo returns current Elo rating + league rank. Bets Chat Intel tab integration ready.
  • Value-finder shipped. GET /v1/intelligence/value-finder returns model-edge value opportunities across odds markets (Elo-implied probability vs market-implied, configurable edge threshold).
  • Quality tier endpoint. GET /v1/players/{id}/tier (1-6 scale with overall + position percentile). NBA + NHL coverage today.
  • EndpointExplorer rolled out to /nba-api, /nhl-api, and the five European soccer league pages.
  • Soccer xG ingest. Soccer player tiers now wired off xG90.
  • /status redirect. Internal /status now redirects to the public UptimeRobot page.
  • X-Cache header on four composite endpoints so consumers can verify cache hits.

2026-05-28

  • WC2026 group advancement endpoint shipped. GET /v1/wc2026/group/{group}/advancement — per-team finishing-position probability via 10,000-iteration Monte Carlo.
  • NBA leaders. GET /v1/nba/leaders returns statistical leaders with team affiliation.
  • NBA hot/cold movers. GET /v1/intelligence/trending ranks players by last-5 vs season average.
  • NBA player situational splits. GET /v1/nba/players/{id}/splits.
  • Player game-log + rolling-stats. GET /v1/players/{id}/game-log and /v1/players/{id}/rolling-stats.
  • Player season projection. GET /v1/players/{id}/season-projection (82-game pace).
  • Player vs-team head-to-head. GET /v1/players/{id}/vs-team/{team_id}.
  • Player streak. GET /v1/players/{id}/streak (hot/cold form).
  • NHL matchup composite endpoint. GET /v1/nhl/games/{id}/matchup.
  • 17 new league + sport API landing pages.
  • API Request Calculator on the homepage and at /compare/api-request-calculator.
  • WC2026 match weather. GET /v1/matches/{id}/weather.
  • NBA team Elo from box scores. Internal Elo store now driven from box-score totals (Task #28).