diff --git a/docs/changelog.atom b/docs/changelog.atom new file mode 100644 index 0000000..143a1d7 --- /dev/null +++ b/docs/changelog.atom @@ -0,0 +1,600 @@ + + + https://docs.livetennisapi.com/changelog.atom + Live Tennis API changelog + Every change to the Live Tennis API specification, dated and versioned. + 2026-10-29T00:00:00Z + + + Live Tennis APIhttps://livetennisapi.com + https://docs.livetennisapi.com/icon-256.png + + https://docs.livetennisapi.com/changelog.html#v1.13.78 + Live Tennis API 1.13.78 + 2026-10-29T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong>Shot-level winners and errors: where they are and when they are withheld.</strong> The family is present where the statistics source counts it: the 2026 Grand Slam main draws (Wimbledon and the US Open near complete, the Australian Open and Roland Garros filling as the completed-match statistics fetch walks back) and the odd regular-tour match. The earlier note that it had not been available since 2026-07-12 described live payloads of the regular tour and is replaced. A family the source filled with zeros is now withheld: a side that won 20 points or more with every by-shot winner and error at 0 is not measured, and the whole family is omitted, totals included. 33 of 33 Roland Garros 2026 qualifying payloads carried such zeros.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.77 + Live Tennis API 1.13.77 + 2026-10-29T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong><code>/fixtures</code> and <code>/matches</code> tell one story.</strong> A fixture bound to a match (<code>match_id</code> set) is served with the match&#x27;s <code>status</code>, in the fixture vocabulary, and the match&#x27;s <code>start_time</code>, read at request time. The match row moves within minutes of a schedule change; the fixture row followed the 6-hourly refresher, so the two endpoints could disagree on the same match for hours. A customer found a Vienna match <code>live</code> at 09:00Z on one and <code>upcoming</code> at 12:30Z on the other. A vendor-stated <code>cancelled</code> or <code>postponed</code> on the fixture is kept. A null <code>is_qualifying</code> on the fixture takes the match&#x27;s value when the match states one.</li> +<li><strong>A bare digit in the schedule&#x27;s status field is no longer a match in play.</strong> The day-ahead listing carries <code>1</code> on matches that have not started. It was read as a set in play, so 298 fixtures for 29 October were published as <code>live</code>. The digit is <code>live</code> only when the schedule&#x27;s own in-play flag says so. The stored rows were restated the same day.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.76 + Live Tennis API 1.13.76 + 2026-10-28T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong><code>X-LTAPI-Signature-V1</code> on every webhook delivery.</strong> The value is <code>t=&lt;unix seconds&gt;,v1=&lt;hex&gt;</code>, and the hex is HMAC-SHA256 of <code>&lt;t&gt;.&lt;raw request body&gt;</code> with your webhook secret. The timestamp is inside the signed string, so a captured delivery re-sent later under a new time no longer verifies. Each body also carries a top-level <code>delivery_id</code>, unique per delivery, to dedupe on. The <code>POST /webhooks</code> description lists the receiver checks: a 5-minute timestamp window, a constant-time compare and a dedupe on <code>delivery_id</code>. It also lists <code>score_withdrawn</code> among the <code>X-LTAPI-Event</code> values, which it had left out.</li> +</ul> +<h3>Fixed</h3> +<ul> +<li><strong>The webhook <code>events</code> enum matches what registration accepts.</strong> <code>POST /webhooks</code> listed <code>stoppages</code>, which registration refuses with a 400, and the <code>Webhook</code> schema listed only <code>score</code> and <code>break_point</code>. Both now read <code>score</code>, <code>break_point</code>, <code>point</code>, <code>status</code>.</li> +</ul> +<h3>Deprecated</h3> +<ul> +<li><strong><code>X-LTAPI-Signature</code>, the body-only webhook signature.</strong> It does not cover the timestamp. We keep sending it unchanged until 2027-02-01, and deliveries sent from 00:00 UTC on that date do not carry it. Both headers use the same secret, so moving to the new header needs no re-registration.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.75 + Live Tennis API 1.13.75 + 2026-10-28T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong>Fields are described by what they hold.</strong> Rounds, stoppages, per-point tags, statistics and surfaces now say what each value means and when it is null. Earlier entries in this changelog were reworded the same way. The 2013 to 2022 archive tape is described as reconstructed point sequences with no capture timestamps. Its <code>point_source_detail</code> value is unchanged. The charting endpoints describe the charted corpus by its coverage. Nothing on the wire changed.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.74 + Live Tennis API 1.13.74 + 2026-10-26T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong>The point-by-point page describes the tape by what it holds.</strong> The tape is the ordered score states we hold for a completed match, with coverage declared per match. The model&#x27;s win probability sits only on rows that were observed live.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.73 + Live Tennis API 1.13.73 + 2026-10-26T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong>Source links point at GitHub again.</strong> The github.com/livetennisapi organisation is public again, so every page, the README and llms.txt link each package&#x27;s repository on GitHub beside its registry page. The Organization&#x27;s sameAs names the GitHub org instead of the GitLab mirror.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.72 + Live Tennis API 1.13.72 + 2026-10-26T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong><code>duplicates_collapsed</code> on <code>GET /matches/{matchId}/points</code>.</strong> How many rows of the page repeated a point the match had already published and were left out. The writer no longer stores such repeats; the count covers rows written before that. <code>last_seq</code> still advances past them.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.71 + Live Tennis API 1.13.71 + 2026-10-26T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong>Every docs page head carries the X (Twitter) card tag.</strong> The 2026-10-25 Ahrefs crawl listed twelve docs pages under &quot;X card missing&quot;. The heads already carried the Open Graph tags, so the card tag names the card type and the rest falls back to Open Graph.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.70 + Live Tennis API 1.13.70 + 2026-10-23T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong>index.html&#x27;s JSON-LD description is its meta description.</strong> The hub page&#x27;s <code>TechArticle</code> carried a different sentence from its meta tag. The meta text is the one that fits the result-page budget, so the JSON-LD now repeats it. The generator&#x27;s JSON-LD check covers index.html as well as the generated pages.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.69 + Live Tennis API 1.13.69 + 2026-10-23T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong>JSON-LD carries raw text, not HTML entities.</strong> The players page&#x27;s <code>TechArticle</code> description read <code>player&amp;#x27;s</code> inside its JSON-LD while the meta tag decoded to <code>player&#x27;s</code>: the topic, reference and changelog templates formatted the block with the HTML escaper, and a script element&#x27;s text is never decoded. Every JSON-LD block is now built as a dict and serialized once with <code>json.dumps</code>, <code>&lt;/</code> escaped as <code>&lt;\/</code>. The reference and changelog descriptions in JSON-LD are the same text as their meta descriptions. The generator refuses a page whose JSON-LD does not parse, contains an entity, or whose <code>TechArticle</code> description differs from the decoded meta description.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.68 + Live Tennis API 1.13.68 + 2026-10-23T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong>The docs&#x27; Organization node matches the apex page&#x27;s.</strong> Every docs page publishes the same <code>Organization</code> as livetennisapi.com: <code>alternateName</code> and the ten <code>sameAs</code> profiles (RapidAPI, Apify, API.market, Postman, GitLab, PyPI, the two npm packages, the docs site and X). Ahrefs flagged the docs node as incomplete while it carried only the name, url and logo.</li> +<li><strong>Titles fit 60 characters and descriptions 110 to 155.</strong> Three topic pages carry a shorter head title than their h1, the authentication page&#x27;s description says what <code>GET /usage</code> is for, and a description the budget cuts no longer ends on a connective. The generator refuses a page outside these budgets, index.html included.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.67 + Live Tennis API 1.13.67 + 2026-10-22T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong><code>/players/{playerId}/stats</code> keeps a UTC instant as given.</strong> <code>from</code> and <code>to</code> are compared as instants, and <code>window</code> echoes what was applied. An instant inside a repeated daylight-saving hour was read through a local wall clock. <code>to=2025-10-25T22:30:00Z</code> came back as 23:30Z with <code>from</code> an hour late, <code>from=22:30Z&amp;to=23:15Z</code> was refused and the reversed window was accepted. The same ordering applies to <code>from</code> and <code>to</code> on <code>GET /matches</code> and <code>GET /history/matches</code>.</li> +<li><strong>An omitted <code>to</code> is the request instant.</strong> It used to be rounded down to the minute, so a <code>from</code> a few seconds earlier answered 400 <code>bad_date</code>. Polls without <code>to</code> inside one minute still share a cached response, and its <code>window.to</code> is the first poll&#x27;s instant.</li> +<li><strong><code>freshness.measured.complete</code> and <code>serve_return.matches_with_stats</code> are one judgement.</strong> A deciding match tiebreak written as its points in the stamp and as one game in the served final, or the other way round, is the same finished set. The statistics endpoint compared the totals first and answered <code>null</code> one way and <code>false</code> the other, while the player aggregate counted the match complete. The field text now says what the judgement is.</li> +<li><strong><code>PlayerRecentMatch.score.games</code> are the games the match object publishes.</strong> The recent scores go through the same projection as <code>GET /matches/{matchId}</code>, so a deciding match tiebreak the row keeps in <code>points</code> is rendered into the deciding-set games slot. Before this, the stored lists were read raw and a match that ended in the breaker showed one set fewer.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.66 + Live Tennis API 1.13.66 + 2026-10-22T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong><code>Player.tour</code> describes a doubles team the way the API decides it.</strong> A team filed under a circuit of its own carries that circuit. Only a team filed under a level word or nothing falls back to the circuit both its members share, and it is <code>null</code> when they disagree or either is unknown. The 1.13.63 text put the fallback first. The field for singles or doubles is the match&#x27;s <code>draw</code>, not its <code>gender</code>.</li> +<li><strong>Parameter tables show numeric bounds.</strong> Every parameter with a <code>minimum</code> or <code>maximum</code> now says so next to its default. <code>/players/{playerId}/stats?last=</code> reads as 1 to 50, where it used to show only the default of 20.</li> +<li><strong>The AI SDK guide says how <code>check_api_status</code> detects the plan.</strong> livetennisapi-ai 1.3.0 reads <code>/usage</code> and reports the key&#x27;s own tier, so a FREE key with History access reports as FREE. The BASIC estimate applies only to the fallback probe, used when <code>/usage</code> cannot be read.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.65 + Live Tennis API 1.13.65 + 2026-10-22T00:00:00Z + + <h3>Changed</h3> +<ul> +<li>The AI SDK guide now pins livetennisapi-ai 1.3.0, which adds <code>player_stats</code> (PRO, with <code>serve_return</code> on ULTRA). The guide and llms.txt now say 25 tools, the same set as livetennisapi-mcp 1.6.1.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.64 + Live Tennis API 1.13.64 + 2026-10-22T00:00:00Z + + <h3>Changed</h3> +<ul> +<li>The AI SDK guide now pins livetennisapi-ai 1.2.1. That release fixes <code>check_api_status</code> for FREE keys, so the known-issue note and its workaround are gone.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.63 + Live Tennis API 1.13.63 + 2026-10-22T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong><code>serve_return.matches_incomplete</code> is documented.</strong> The API already returns it. It counts matches whose measured statistics stop short of the final. Those matches are left out of every sum.</li> +<li><strong><code>serve_return.matches_with_stats</code> says what it counts.</strong> It counts only matches whose measured statistics reach the final, and only those are summed.</li> +<li><strong><code>GET /players</code> documents its 400 response.</strong> A <code>restated_since</code> that is not an ISO-8601 instant answers <code>bad_restated_since</code> with the <code>Error</code> body.</li> +<li><strong><code>Player.tour</code> lists the values the API sends.</strong> They are <code>atp</code>, <code>wta</code> and <code>null</code>. The old text described capitalised and level values that have not been served since 2026-09-24.</li> +<li><strong>Field tables show nested fields.</strong> Inline objects are spelled out as dotted paths, such as <code>sets.tiebreaks</code> and <code>serve.first_serve_in</code>. A field typed by a named schema links to that schema&#x27;s table.</li> +<li><strong>Changelog list items keep their continuation lines.</strong> An unindented line under a bullet rendered as a paragraph inside the list. That is invalid HTML.</li> +<li><strong>The AI SDK guide states a known issue with FREE keys.</strong> In livetennisapi-ai 1.1.0, <code>check_api_status</code> reports a FREE key as unreachable. The guide names the workaround until 1.2.1 is on npm.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.62 + Live Tennis API 1.13.62 + 2026-10-22T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong><code>PlayerRecentMatch.opponent</code> is nullable.</strong> The endpoint returns <code>null</code> when the match row holds no opponent id. The schema said it was always an object.</li> +<li><strong><code>PlayerRecentMatch.score.games</code> values are nullable integers.</strong> The per-set lists are passed through from the stored row as <code>GET /matches/{matchId}</code> serves them, and nothing on that path coerces the values.</li> +<li><strong><code>records.last_12_months</code> now says what window it covers.</strong> It is the 365 days ending at <code>to</code>, whatever <code>from</code> says. With the default window it equals <code>overall</code>.</li> +<li><strong>The <code>/players/{playerId}/stats</code> 400 response references the <code>Error</code> schema.</strong> It names the five codes and says that <code>bad_surface</code> and <code>bad_level</code> list the accepted values in <code>allowed</code>.</li> +<li><strong>The <code>Player</code> schema gains seven fields the API already returns.</strong> They are <code>ranking_status</code>, <code>ranking_checked_at</code>, <code>itf_ranking</code>, <code>profile_restated_at</code>, <code>profile_version</code>, <code>archive_player_id</code> and <code>itf_id</code> (ULTRA only). <code>GET /players</code> gains the <code>restated_since</code> parameter that <code>profile_restated_at</code> points to. Nothing changed on the wire.</li> +<li><strong><code>llms.txt</code> and the plans table name the new endpoint.</strong> <code>/players/{playerId}/stats</code> is listed under PRO and its <code>serve_return</code> block under ULTRA.</li> +<li><strong>The players topic page no longer says <code>/rankings</code> per-player as-of records are PRO.</strong> The rank-ordered listing is PRO, and the as-of records are ULTRA.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.61 + Live Tennis API 1.13.61 + 2026-10-22T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong><code>GET /players/{playerId}/stats</code> (PRO; <code>serve_return</code> ULTRA).</strong> One player&#x27;s record over our own completed singles matches, 2023 onward. The default window is the 365 days ending now, and <code>?from=</code>, <code>?to=</code>, <code>?surface=</code>, <code>?level=</code> and <code>?last=</code> narrow it. PRO gets <code>records</code>: W/L overall, over the last 12 months, and by surface, venue, level and season. It also carries the set-based record (tiebreaks, deciding sets, after winning or losing the first set, comebacks, sets won and lost), withdrawals given and received, and the last N matches with the score turned round to the player. Every count carries its sample size, and a percentage on an empty sample is <code>null</code>, not 0. Winners and scores are read the same way the match object publishes them, so this endpoint cannot disagree with <code>GET /matches/{matchId}</code>. ULTRA adds <code>serve_return</code>, the per-match statistics of <code>/matches/{matchId}/statistics</code> summed over the window. Each ratio is published as numerator, denominator and pct, with the number of matches behind it. Below ULTRA the key is absent. A doubles-team id answers 400 <code>doubles_not_supported</code>, and a merged or retired id answers 410 as <code>/players/{playerId}</code> does. For 1968–2022, <code>/history/archive/career</code> remains the endpoint.</li> +<li><strong><code>height_cm</code> and <code>doubles_ranking</code> on <code>GET /players/{playerId}</code>.</strong> Both are on the single-player read only, on paid tiers, and null on a FREE key. <code>height_cm</code> comes from the results archive&#x27;s record of the same person and is never inferred. <code>doubles_ranking</code> is <code>{rank, points, observed_on}</code>, the current position on the official ATP/WTA doubles table. It is null when the player is off the table or was last seen there more than 21 days ago. It is never copied into <code>ranking</code>, which stays the singles position. Nothing existing changed type or meaning.</li> +</ul> +<h3>Changed</h3> +<ul> +<li><strong>The licence is named by its SPDX identifier.</strong> <code>info.license</code> now carries <code>identifier: MIT</code> in place of a URL. The old URL pointed at a GitHub page that no longer loads. No endpoint, field or type changed.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.60 + Live Tennis API 1.13.60 + 2026-10-03T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong>The hosted MCP endpoint, which this reference had never mentioned.</strong> A remote Streamable HTTP server has run at <code>https://mcp.livetennisapi.com/mcp</code> for weeks. The only MCP line here named the self-run package, <code>npx livetennisapi-mcp</code>. A reader who wanted an LLM agent over this API was therefore told to install and host one. The string <code>mcp.livetennisapi.com</code> appeared zero times in the reference, zero times in <code>llms.txt</code> and zero times in the README. An engineer evaluating the API found the endpoint by going looking for it, which is what prompted this. A new <strong>MCP server</strong> section now sets the hosted and self-run routes side by side. The client-libraries table, <code>llms.txt</code> and the README name the endpoint too. Everything in the section was read off the live server on 2026-10-03, not taken from a release note. <code>initialize</code> reports <code>serverInfo</code> version <strong>1.5.0</strong>. <code>tools/list</code> returns <strong>24</strong> tools, set-identical to the 24 the section lists by name. Both documented auth headers were called and both work: <code>Authorization: Bearer twjp_...</code> and <code>X-API-Key: twjp_...</code>. The rate headers read <code>limit=300</code> on a key and <code>limit=60</code> without one. The section states both, and states that the caller&#x27;s own plan limit still applies underneath. That second sentence is load-bearing. The hosted cap sits above a free key&#x27;s 30 a minute, so publishing it alone would read as a quota raise. With no key at all only <code>check_api_status</code> answers, and it returns the free-key URL. <code>GET /mcp</code> answers 405 <code>&quot;use POST&quot;</code>, so the section says POST only. <code>GET /health</code> reports the running version. Every plan reaches MCP, free keys included, and each tool reaches exactly what that plan reaches. No price is quoted anywhere, because the endpoint takes the key a customer already has. No field, parameter or type changed, and no endpoint of the REST API changed.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.59 + Live Tennis API 1.13.59 + 2026-10-02T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong><code>?is_qualifying=</code> on <code>/matches</code>, <code>/history/matches</code> and <code>/fixtures</code>, and <code>?gender=</code> documented on the same three.</strong> The <code>is_qualifying</code> field has been published on every match and fixture since 2026-08-12 and this reference already called it &quot;the only field that separates a qualifying draw from the main draw&quot; and told callers to &quot;filter accordingly&quot; — while the parameter they would reach for did not exist and was accepted and ignored. Measured on prod before the change: <code>/matches?status=completed&amp;limit=50</code> returned the same fifty ids under <code>is_qualifying=true</code>, <code>is_qualifying=false</code> and no filter at all, every one of the fifty rows carrying <code>is_qualifying: false</code>, while the <code>?draw=</code> control changed the page. The filter is applied in SQL before the page limit on <code>status=live</code>, <code>upcoming</code>, <code>completed</code> and <code>cancelled</code> and on the archive listing, so a deep crawl can page a single draw rather than filtering a full corpus client-side. It is <strong>three-valued like the field</strong>: a row whose flag is <code>null</code> matches neither <code>true</code> nor <code>false</code> and is returned only when the filter is omitted, because returning it under <code>false</code> would assert &quot;main draw&quot; on the strength of nothing. Coverage, measured 2026-10-02 over completed matches: the flag is stated on <strong>98.9%</strong> of the last thirty days and <strong>98.3%</strong> of 2026 outside the UTR circuit, which is where almost all unstated rows are (1,695 of the 1,831 in the last thirty days). Anything other than true/false is a 400 <code>bad_is_qualifying</code>. Asked for by a customer splitting qualifying from main draw for a match-prediction model. <code>?gender=men|women</code> has worked on the match and archive listings since 2026-09-09 and was never documented here; it is now, with the same null contract — a mixed-doubles or team-tie row matches neither value. No field or type changed.</li> +</ul> +<h3>Fixed</h3> +<ul> +<li><strong><code>?gender=</code> was accepted and ignored on <code>/matches?status=live</code> and <code>?status=upcoming</code>.</strong> Those two statuses are served from a short-lived slate cache keyed only on the status, and the gate that decides whether a request may use it never learned about <code>gender</code>, so a filtered request was answered with the whole slate under a 200. Measured on prod before the fix: <code>status=live</code> returned the same twenty-five rows for <code>gender=men</code>, <code>gender=women</code> and no filter — six of them women&#x27;s matches and seven with no gender stated — and <code>status=upcoming</code> the same sixty rows of a hundred and forty-three under every value. <code>status=completed</code> and <code>status=cancelled</code> are not cached and answered correctly throughout (sixty of sixty, pure), so the filter itself was never wrong: the request did not reach it. The live listing is polled continuously, so the cache was effectively always warm and the filter was dropped on every call rather than occasionally. Present from 2026-09-09 to 2026-10-02. Callers who filtered the live or upcoming slate by gender and trusted the result were served the unfiltered slate and should re-read anything derived from it; every row carried a correct <code>gender</code> field throughout, so a client that filtered on the field rather than the parameter was unaffected.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.58 + Live Tennis API 1.13.58 + 2026-10-01T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong>The archived tape&#x27;s accept clock now begins 2026-08-11, six weeks earlier than 1.13.57 published, because the missing values were recovered rather than written off.</strong> 1.13.57 measured the boundary honestly at 2026-09-23, the day <code>history_archived_scores.content_changed_at</code> landed as a catalogue-only column with no backfill. What that measurement did not say is that the values were still reachable: for every archive row copied before the column existed, the live row it was copied from may still be inside the 90-day retention, and that row holds the value the archiver would itself have written. A backfill carried them across on 2026-10-01 in six bounded tranches, <strong>1,413,471 accept clocks and 1,393,458 accept counters, inside 1,538,176 rows over 14,491 matches</strong>, and left <code>fillable=0</code>. Nothing is derived, rounded or taken from a neighbouring row, and a row whose live row had already been trimmed stays null for good. Measured after the run: the archive carries the clock on <strong>100.0% of every day from 2026-08-11 to 2026-10-01</strong>, 92.5% of 2026-08-10 (the live column&#x27;s own first instant is <code>2026-08-10T08:07:42Z</code>) and 0.0% of every day before it, which is <strong>1,721,102 of 4,663,181</strong> archived observed rows against 307,631 before. <code>HistoryTapeRow.sequence</code>, the accept counter, has its own later floor because <code>live_scores.match_seq</code> landed after the clock: nothing to 2026-08-13, 68.2% of 08-14, 99.3% of 08-15, then 95.9% or more of every day from <strong>2026-08-16</strong> and 100% from 2026-09-01. Two boundaries, not one. <code>received_at</code> could not move at all and the reference now says so: its live column landed 2026-09-24, a day AFTER the archive column, so every live row carrying an arrival clock was archived with it and the recoverable count is zero. Wire-verified both sides: match 2168 (26 April 2026), before the boundary, still answers 131 observed rows with <code>timestamp</code> on all 131 and <code>accepted_at</code> on none; match 189679&#x27;s archived copy carried the clock on 0 of its 171 observed rows before the run and on 171 of 171 after it. The practical consequence for a timing study is that the clock no longer disappears when a match ages out of live retention. No field, endpoint or type changed.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.57 + Live Tennis API 1.13.57 + 2026-10-01T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong><code>HistoryTapeRow.accepted_at</code> was documented as covering the whole observed corpus, and on an archived tape it begins 2026-09-23.</strong> The field is the one this reference tells a timing study to use, and its description read &quot;It rides the whole observed corpus, so a study that spans 2026-09-24 must use this one and not <code>received_at</code>&quot;, while <code>received_at</code> said &quot;THIS CLOCK HAS A START DATE AND THE OTHER TWO DO NOT&quot;. Both sentences were wrong about <code>accepted_at</code>. Measured over the whole archived observed corpus: <strong>307,512 of 4,659,024 rows carry it</strong>, which is 0.0% of every month to August 2026 (286 rows in all of August), 27.3% of September and 100% of October. Wire-verified on match 2168 (26 April 2026), answered with <code>meta.from_archive: true</code>: 131 observed rows, <code>timestamp</code> on all 131 and <code>accepted_at</code> on none. The measurement quoted in 1.13.5x — match 189679, <code>accepted_at</code> on all 171 observed rows — is correct and does not generalise, because that match is still inside live retention and is answered from its live rows (<code>from_archive: false</code>); its own archived copy carries the clock on <strong>0 of 367</strong> rows. So coverage depends on which copy answers the read, <code>meta.from_archive</code> is how you tell, and a null here is a boundary of the record rather than a missing value. <code>timestamp</code> is the only one of the three clocks with no start date. Found while answering a customer who asked for &quot;a timestamp precise enough to line up with exchange quotes&quot; — the exact question the wrong sentence was there to answer. No field, endpoint or type changed.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.56 + Live Tennis API 1.13.56 + 2026-10-01T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong>An exact player name that sat inside a longer name could not be asked about on any of the three name endpoints.</strong> Every name lookup is a substring match, so there was no way to say &quot;exactly this one&quot;, and a person whose name is a prefix of somebody else&#x27;s was unreachable: <code>GET /h2h?p1=Carlos%20Alcaraz</code> answered <code>400 ambiguous_name</code> listing <code>Carlos Alcaraz</code> and <code>Carlos Alcaraz Gonzalez</code>, with no string that could pick between them. Measured on the live roster and corpus: <strong>14 of the 1,177 ranked players</strong> on <code>/h2h</code>, <strong>21 of 1,500</strong> distinct archive names on <code>/history/archive/career</code> and <strong>4 of 1,739</strong> charted names on <code>/charting/players</code>, the men&#x27;s world #2 and #3 among them. Passing a name exactly now selects that person on all three. A name given in part is still a fragment and still refuses with the candidate list, so <code>Sinner</code> continues to list Jannik and Martin rather than guessing, and a single name charted under both genders on <code>/charting/players</code> still needs <code>gender</code>. The refusal also reports the candidates it would have shown before the exact match was applied, so a hint never gets shorter than it was. Verified on the wire: Alcaraz against Sinner returns 18 meetings 11-7 with the surface split and the ULTRA aggregates. No field, endpoint, parameter or error code changed.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.55 + Live Tennis API 1.13.55 + 2026-09-29T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong>ITF World Tennis Tour doubles began stating <code>outcome</code>, and the reference still said no doubles draw carries either field.</strong> The 1.13.42 note measured 454 completed doubles matches and found no tagged row. That was true when it was written. The first doubles row carried a tag on 2026-09-23, and by 2026-09-29 <code>GET /matches/{id}/points</code> published <code>outcome</code> on <strong>104 of 596</strong> completed ITF doubles matches, 17%, against <strong>0 of 371</strong> for the ATP, WTA and Challenger doubles draws in the same window. The vocabulary is ace and double fault, as on the ITF singles draws. <code>serve</code> is still unstated on every doubles draw, measured 0, and that sentence is unchanged. The cause sat before the tagger: a dedup change let a doubles fixture be adopted and oriented by its member sets instead of being refused, so the ITF per-point join reached rows it had never been able to match. 17% falls between &quot;almost none&quot; and &quot;a clear majority&quot;, so the description states it as a measured fraction and sends the reader to <code>enrichment</code> per match instead of to the draw. WTA main-tour qualifying has carried that treatment since 1.13.42. Found by the every-sweep tag-coverage check. No field, endpoint or behaviour changed.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.54 + Live Tennis API 1.13.54 + 2026-09-28T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong>The two broadcast surfaces, live since 2026-09-24 and documented nowhere a customer could read.</strong> <code>GET /broadcast/match/{matchId}</code> and <code>GET /broadcast/live</code> have served every ULTRA key for four days and appeared in no published reference: the write-up went into the tennis repository, which nothing serves. Both are now specified, with the 54-field <code>BroadcastObject</code> schema in the same order the API returns it, verified field by field against a live response rather than transcribed from the source. The object exists because a graphics template binds each field to a fixed path, so three properties are contractual rather than incidental and are documented as such: it is FLAT, every key is PRESENT on every read (<code>null</code>, never missing — a missing key breaks a binding), and the score is already in DISPLAY form (<code>p1_points</code> reads <code>&quot;40&quot;</code> or <code>&quot;AD&quot;</code>, <code>set_line</code> is one string, each set has its own column). It is a projection of what the API already publishes plus the break-, set- and match-point flags the scoring pipeline derives; nothing in it is a new fact and nothing is guessed.</li> +<li><strong>The 1 Hz polling contract, which is the part a live scoreboard gets wrong.</strong> Every response carries a strong <code>ETag</code> and <code>Cache-Control: no-cache, max-age=1</code>, so <code>If-None-Match</code> turns an unchanged state into a 304 with no body; ULTRA&#x27;s 600-a-minute burst window means ten single-match pollers at 1 Hz fit on one key, and <code>/broadcast/live</code> stays one request however many matches are live. A new state is detected on <code>sequence</code>, never on <code>served_at</code>, which changes on every read — stated on both routes because polling on <code>served_at</code> is the mistake that looks like it works.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.53 + Live Tennis API 1.13.53 + 2026-09-26T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong>The tape now publishes the accept counter the withdrawal ledger points at.</strong> <code>GET /matches/{matchId}/withdrawals</code> tells you a <code>sequence</code> was withdrawn and to resume from <code>replaced_by_sequence</code>, and until today no served history-tape row carried that number, so the pointer named a row nothing identified. <code>HistoryTapeRow.sequence</code> is the same per-match accept counter as <code>Score.sequence</code> on a live read. Because a withdrawn state never enters the record at all, a withdrawal now reads as a GAP in the tape&#x27;s run of counters, which is what makes &quot;resume from the predecessor&quot; resolvable against the served history. Reported by an Ultra customer who verified three withdrawal records against the tape and wrote that &quot;the history rows lack sequence IDs, so these responses alone cannot reconstruct exactly what a live client received&quot;. Null, never fabricated, on a row archived before 2026-09-26 (the archive did not keep the counter, so it died with the live row at retention) and on the pre-2023 archive tape, which has no such counter. Unlike <code>accepted_at</code> and <code>received_at</code> it is NOT null on a reconstructed row: a counter is a fact about our own accept order.</li> +</ul> +<h3>Fixed</h3> +<ul> +<li><strong><code>Score.sequence</code> said the tape does not carry it, and the stated reason was the gap.</strong> The description read &quot;absent on history-tape rows, which are already served in order&quot;. Being served in order does not resolve a <code>replaced_by_sequence</code>, which is a pointer at a counter rather than a position in a list. Corrected, and it now points at <code>HistoryTapeRow.sequence</code>.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.52 + Live Tennis API 1.13.52 + 2026-09-25T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong>The tape&#x27;s two accept clocks are published.</strong> <code>HistoryTapeRow.accepted_at</code> and <code>HistoryTapeRow.received_at</code> have ridden every observed row of <code>GET /history/matches/{matchId}</code> since 2026-09-24 and were documented nowhere, so a customer doing timing research on a tape had no published field to read. <code>accepted_at</code> is the instant our arbiter accepted the state, stamped once and never refreshed, which is the clock to difference against: <code>timestamp</code> starts equal to it and is then refreshed while the owning source keeps re-asserting the unchanged state, so on most raw rows <code>timestamp</code> is a later re-assertion instant (on match 189679, 164 of its 171 observed rows carry a <code>timestamp</code> more than a second after <code>accepted_at</code>). <code>received_at</code> is the instant the state reached our edge before arbitration, so <code>accepted_at</code> minus <code>received_at</code> is our own arbitration step. Neither figure is a claim about speed before our edge and neither is the court&#x27;s clock. Both are null on every reconstructed row, which never had a clock.</li> +<li><strong>The arrival clock&#x27;s start date, which is the part that bites.</strong> <code>received_at</code> is stamped only as a state arrives, so it can never be filled in afterwards. The earliest one we hold is <code>2026-09-24T02:00:26Z</code>; on every observed row accepted before that instant it is null while <code>timestamp</code> and <code>accepted_at</code> are populated. A study that spans the boundary has to read <code>accepted_at</code>, which rides the whole observed corpus. Measured on match 189679 (12 September) after a Basic customer reported it: 171 observed rows, <code>accepted_at</code> on all 171, <code>received_at</code> on none.</li> +<li><strong>What <code>server</code> guarantees on a tiebreak row, and what it does not.</strong> Tiebreak rows are never settled on either sequence. The server genuinely changes during a breaker, so there is no constant to take a majority of, and the field is the source&#x27;s own assertion carried through. Where a breaker&#x27;s rows skip points the rotation cannot be reconstructed from what is served: you have the server of a state, not of a numbered point, and <code>meta.points.transitions_legal</code> against <code>transitions_total</code> tells you whether that happened. On the set roll-up row carrying <code>tiebreak_final</code>, <code>server</code> is the value of that committed set-end state and is NOT a preserved copy of the final breaker point&#x27;s server. Reported by a Basic customer who could not reconcile 21 tiebreak transitions against the standard rotation and was right not to.</li> +</ul> +<h3>Fixed</h3> +<ul> +<li><strong><code>Score.accepted_at</code> said &quot;absent on history-tape rows&quot;.</strong> It has been present on them since 2026-09-24, so the description had been wrong for a day, on the one field a timing study is told to use. Corrected, and the tape row now documents its own copy.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.51 + Live Tennis API 1.13.51 + 2026-09-25T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong>The <code>score_withdrawn</code> frame is documented on the push feed page, where a streaming consumer looks for it.</strong> 1.13.50 put the record and the frame&#x27;s fields on the score read and in the schema list, and the frame is a STREAM artifact: it arrives on the same channel as the <code>score</code> frame it retracts. <code>GET /ws-token</code> now carries the frame itself, the reason it exists (the feed publishes every accepted state and runs no trust deference, while the score read runs the legality gate and can serve an older state), what each <code>kind</code> asks you to do, and the three reads that recover a judgement you were offline for. No spec surface changed.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.50 + Live Tennis API 1.13.50 + 2026-09-25T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong>A withdrawn score state is now recoverable, and <code>GET /matches/{matchId}/withdrawals</code> serves the record.</strong> The <code>verdict</code> object has told a stream consumer since 2026-09-23 that a state it holds was taken back, but it is recomputed on every read over the newest state, so it returned null again as soon as the next state was accepted. In a live match that is seconds. A consumer that disconnected and missed the <code>score_withdrawn</code> frame had no way back to it: the publisher&#x27;s duplicate ledgers live inside the publishing process, and nothing else recorded the judgement. Every withdrawal is now written down when the judgement is made, before the frame goes out. Three reads use it. <code>GET /matches/{matchId}/withdrawals</code> (ULTRA, the tier that receives the frame) returns every refused <code>sequence</code> on a match, oldest first, each with <code>kind</code>, <code>reason</code>, <code>withdrawn_at</code>, <code>published_at</code>, the <code>sequence</code> served in its place, and the frame exactly as the feed sent it. <code>GET /matches?withdrawn_since=&lt;instant&gt;</code> lists the match ids with a withdrawal since then, with a count per match, so a consumer coming back after a gap knows which matches to ask about. And <code>?sequence=N</code> on <code>GET /matches/{matchId}/score</code> makes <code>verdict</code> answer for the one sequence you hold, even after later states have landed. Records are kept 90 days from the withdrawal.</li> +<li><strong>The <code>score_withdrawn</code> frame&#x27;s own fields are published for the first time.</strong> <code>type</code>, <code>match_id</code>, <code>kind</code>, <code>superseded_sequence</code>, <code>reason</code>, <code>safe_to_resume</code>, <code>ts</code> and <code>published_at</code>. It carries NO replacement score and no new sequence, which is the question a customer had to ask us: the state to resume from is the one on the next <code>score</code> frame, or on a fresh <code>GET /matches/{matchId}/score</code>, and the sequence served in the refused state&#x27;s place is on the withdrawal record rather than on the frame. <code>ts</code> is when the judgement was made, <code>published_at</code> when the frame was handed to the feed.</li> +</ul> +<h3>Changed</h3> +<ul> +<li><strong>Only a discarded withdrawal is re-asserted by <code>?sequence=</code>.</strong> A <code>deferred</code> state may since have been corroborated and a <code>withheld</code> read described that moment&#x27;s scan, so neither answers from the record; a verdict the read produces itself always wins, because it describes the state actually being served. <code>?sequence=</code> changes <code>verdict</code> and nothing else on the object. A non-integer is <code>400 bad_sequence</code> rather than an accepted-and-ignored filter, because a silently dropped lookup would read as &quot;not withdrawn&quot;. <code>?withdrawn_since=</code> likewise takes only <code>limit</code> and <code>offset</code> beside it: any match filter, <code>status</code> or <code>updated_since</code> alongside it is <code>400 bad_withdrawn_since</code>.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.49 + Live Tennis API 1.13.49 + 2026-09-24T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong>The tournament catalogue takes <code>?tier=</code>, which it had been accepting and ignoring.</strong> <code>GET /tournaments</code> publishes each row&#x27;s <code>tier</code> and <code>tier_source</code>, and the two match listings have filtered on the same vocabulary since 1.13.28. The catalogue did not: <code>?tier=atp_500</code>, <code>?tier=laver_cup</code> and an outright invalid value each returned the entire 10,302-row catalogue under a 200, byte-identical to no filter at all, because the level was attached to a page after it had already been selected. It now selects. The one difference from the match listings is the season, and it is stated on the parameter: a catalogue row has no season of its own, so <code>?tier=</code> here reads the CURRENT season, the same season the row&#x27;s own <code>tier</code> field reports, while on <code>/matches</code> and <code>/history/matches</code> it reads each match&#x27;s own season. A tournament with no row for the current season matches no value. An invalid value is now a 400 <code>bad_tier</code> carrying the vocabulary, as it always was elsewhere.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.48 + Live Tennis API 1.13.48 + 2026-09-24T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong><code>tiebreak_final</code> on the history tape row is documented for the first time, and the page-first rule the reference stated is no longer true.</strong> A PRO customer wired a bot to the closing score and reported that match 195134 served a <code>tiebreaks</code> array of <code>{p1: 2, p2: 7}</code> while no tape row carried <code>tiebreak_final</code>, and that it stayed that way on every later read. Two things were wrong here. The field had never appeared in this reference on <code>HistoryTapeRow</code> at all, though it is served on <code>GET /history/matches/{matchId}</code> and a customer had been pointed at it in writing; it is now described beside <code>is_tiebreak</code>. And the <code>MatchPoints</code> text said a page that *starts* on a roll-up row &quot;has no previous row to read and uses the recorded finals alone&quot;, which was true when written and is not true now: since 2026-09-24 such a page reads the one row before it, so the value does not depend on where the page was cut. Both endpoints resolve the score the same way and agree with the response&#x27;s own <code>tiebreaks</code> array.</li> +<li><strong>The row that closes a tiebreak set carries the score whichever row the set counter moves on.</strong> The tape&#x27;s set counter lags the games array by a row: the row that took 195134&#x27;s first set to 6-7 still read <code>sets</code> <code>[0, 0]</code>, and the counter reached <code>[0, 1]</code> on the row after it. The stamp only fired when the counter grew on that same row, so a set whose counter lagged was never stamped, and nothing revisits a tape. The games on the row now decide it, judged by the tournament&#x27;s own set test so the Next Gen first-to-4 format is read by its own rule. Measured on the live endpoint before the change: of 9 live matches whose <code>tiebreaks</code> array stated a final, 2 carried it on no row, both first-set breakers.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.47 + Live Tennis API 1.13.47 + 2026-09-24T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong>A set completion that does not end the match is assigned a <code>sequence</code> only once confirmed.</strong> Live since 2026-09-24 and stated here for the first time. A live scoring display can render the set-ending frame ON set point, before the point is decided, and step back when the point is not won; a customer measured exactly that twice. A rise in <code>sets</code> that does not end the match, arriving while the state we hold is set point for the side that would gain the set, is now held until the same source keeps presenting it for 10 s or more, a source at least as trusted presents it, or the same source shows play continuing inside the new set. The unconfirmed frame is never stored, so no <code>sequence</code> ever carries it and nothing is published as provisional — a <code>sequence</code> that carries a set completion is one that met the rule. Until then <code>GET /matches/{matchId}/score</code>, the push feed and the WebSocket all keep serving the set-point state; a frame nobody confirms within 120 s is discarded. A set completion that <strong>ends</strong> the match is exempt and follows the completion rules instead. Measured over the 30 days to 2026-09-24: a median 10 s added between the point and the published set-end, and on a feed that does not re-present states the set-end lands with the first point of the next set (90th percentile 133 s). It does not cover a set genuinely won and then corrected on court, which can still be published and rewound — read <code>corroborated</code> beside it, and <code>verdict</code> says so when a state is taken back.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.46 + Live Tennis API 1.13.46 + 2026-09-24T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong><code>corroborated</code> and <code>corroborated_age_seconds</code> on the Score: has an independent source vouched for THIS EXACT state?</strong> Raised by a customer who acted on a completed set that was taken back seconds later. <code>corroborated</code> is <code>true</code> when a source other than the one that wrote the state has since presented us the same state, <code>false</code> when none has — including a brand-new state nobody has had a chance to agree with yet — and <code>null</code> on the archive fallback of a retention-trimmed completed match, where <code>age_seconds</code> and <code>sources_count</code> are null too. <code>corroborated_age_seconds</code> is the seconds since that vouch, null when there is none. <strong>This is the field to gate a set-end on.</strong> <code>sources_count</code> and <code>changing_sources_count</code> are window measures over the match and say nothing about the state in front of you, so a set completion that only one source has announced can sit beside a perfectly healthy count — and a set-end is the one state whose correction changes a result rather than a number. It is evidence, not proof: a vouched state can still be corrected, so compare <code>corroborated_age_seconds</code> with <code>age_seconds</code> and treat a vouch younger than the state it vouches for as the weaker signal it is. When a state is taken back the <code>sequence</code> goes down and <code>verdict</code> says so. Present wherever <code>sources_count</code> is; a yes/no, never a name.</li> +<li><strong><code>changing_sources_count</code> on the Score is documented.</strong> Live since 2026-09-24 and absent from this reference. How many distinct sources CHANGED the score in the last 300 seconds, counted on the content clock alone, so a source re-asserting an unchanged state is not counted — the difference from <code>sources_count</code>, which a feed stuck re-sending its last state holds at 1 for as long as the echo lasts while this reads 0. The window is wider on purpose: a point lands every 30-60 s and a changeover is about 90 s. A cross-confirmation is agreement, not a change, so this can read below <code>sources_count</code> and never above it.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.45 + Live Tennis API 1.13.45 + 2026-09-24T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong><code>GET /history/incidents</code> and <code>GET /history/incidents/{incidentId}/matches</code> are documented for the first time.</strong> Both have been live and neither appeared anywhere in this reference, so the register a customer is meant to reconcile against could not be found from the documentation. <code>/history/incidents</code> returns the published data-quality incidents, oldest window first, each with its <code>window_utc</code> (start, end and the <code>basis</code> it was read from), <code>rule_before</code>, <code>rule_after</code>, <code>defect</code>, <code>affected_surfaces</code>, <code>how_to_tell</code>, <code>corrections</code> and <code>matches</code>. The register is part of a release and is not inferred at read time, so a record does not change between releases. <code>/history/incidents/{incidentId}/matches</code> streams one row per affected match as JSONL, or CSV with <code>?format=csv</code>, with the columns <code>incident_id</code>, <code>match_id</code>, <code>tournament</code>, <code>tour</code>, <code>round</code>, <code>draw_stage</code>, <code>scheduled_time</code>, <code>affected_field</code>, <code>served</code> and <code>expected</code>. A match whose derivation would be a guess is left out and the incident&#x27;s <code>matches.note</code> says which, so the export is exact rather than complete. Measured on the live API 2026-09-24: five incidents across the kinds <code>pricing_rule</code>, <code>version_stamp</code>, <code>field_orientation</code> and <code>field_repair</code>.</li> +<li><strong>The <code>verdict</code> object on the Score is documented.</strong> Live since 2026-09-23 and absent from this reference. It is null on an ordinary read. When the legality gate refuses a state a stream has already published, it carries <code>kind</code> (<code>withdrawn</code>, <code>deferred</code> or <code>withheld</code>), <code>superseded_sequence</code> (the <code>sequence</code> a stream consumer is holding), <code>reason</code> and <code>safe_to_resume</code>, which is true exactly when <code>kind</code> is <code>withdrawn</code>. The push feed, the WebSocket and webhooks announce the same judgement as a <code>score_withdrawn</code> frame naming the same sequence, so a stream consumer learns a state was taken back without polling. <code>GET /matches/{matchId}/score</code> now names the field and points at the incident register.</li> +</ul> +<p>No field, endpoint or behaviour changed by this release; both were already served.</p> + + + https://docs.livetennisapi.com/changelog.html#v1.13.44 + Live Tennis API 1.13.44 + 2026-09-23T00:00:00Z + + <ul> +<li><strong><code>GET /matches/{matchId}/points</code>: the row that closes a tiebreak set now carries <code>tiebreak_final</code>, and the reference did not say so.</strong> Raised by a Pro customer running trading bots, who reported that the tape stops one point short of every tiebreak and that a field filling once a day was no use to a decision taken while the match is live. A tiebreak&#x27;s last point is carried, like every set-winning point, by the set roll-up row (<code>tiebreak</code> false, the set banked 7-6, <code>score</code> 0-0), and that row never stated the breaker&#x27;s score at closure. It now carries <code>tiebreak_final: [p1, p2]</code> in our player order, e.g. <code>[7, 5]</code>. Exact when the previous row is the decided score or one point from it, so it is derived from the stream itself and is present as soon as the set closes; otherwise taken from the match&#x27;s recorded tiebreak finals, the same finals <code>GET /history/matches/{matchId}</code> publishes as <code>tiebreaks</code>; <code>null</code> when neither can state it, never guessed. Absent on every other row, including the breaker rows. A page that *starts* on a roll-up row has no previous row to read and uses the recorded finals alone. Measured at the endpoint over the 30 hours to 2026-09-23 20:30Z: 60 completed matches held 67 tiebreak sets and 36 of them served a closing score, only 7 of those matches having a recorded final at the time. Documentation of an already-served field; no behaviour changed by this release.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.43 + Live Tennis API 1.13.43 + 2026-09-23T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong>The reference said a FREE key is refused the history endpoints outright. It is not, and has not been since 2026-08-07: a FREE key is served 20 history calls per calendar month.</strong> Three separate statements carried the wrong rule — the FREE tier summary (&quot;No historical results&quot;), the <code>/matches</code> description (&quot;requires BASIC ... and returns <code>403 upgrade_required</code> on a FREE key&quot;), and the <code>status</code> parameter. Measured on the live API 2026-09-23 with a fresh FREE key: <code>GET /history/matches</code> answered <code>200</code> and kept answering until the monthly allowance was spent, after which it answered <code>403 upgrade_required</code> carrying <code>free_history_taste: &quot;used&quot;</code> and the detail &quot;your 20 free history calls this month are used&quot;. A reader planning against the old text would have concluded the endpoints were closed to them and either bought a tier they did not yet need or abandoned the evaluation; a reader who tried anyway got a <code>200</code> the reference said was impossible and could not tell an allowance from a private grant. <code>/history/matches</code>, <code>/history/coverage</code>, <code>status=completed</code> on <code>/matches</code> and the FREE tier summary now all state the allowance and the refusal that follows it.</li> +</ul> +<p>No field, endpoint or behaviour changed; every correction is to a description.</p> + + + https://docs.livetennisapi.com/changelog.html#v1.13.42 + Live Tennis API 1.13.42 + 2026-09-23T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong>Every per-point coverage count in this reference was measured in our own tables and published as though it described the API. It did not, and the gap is large.</strong> <code>GET /matches/{id}/points</code> serves a COMPLETED match&#x27;s stored per-point stream only while that stream passes our quality bar. Where it does not, the endpoint serves a measured-complete RECONSTRUCTION instead, and a reconstruction carries no clock and no <code>serve</code> / <code>outcome</code> tags at all. So the share of matches whose tags a reader can fetch is lower, sometimes far lower, than the share of matches a source tagged. Re-measured 2026-09-23 on what the endpoint publishes, beside what the previous text claimed: <strong>Davis Cup World Group <code>serve</code> 2 of 22 (published as 22 of 22), World Group I 1 of 42 (published as 41 of 42)</strong>; WTA qualifying <code>serve</code> <strong>18 of 48</strong> (published as 48 of 48); ATP Challenger qualifying <code>serve</code> <strong>137 of 197</strong> (195 of 197) and <code>outcome</code> <strong>134 of 197</strong> (191 of 197); WTA 125 qualifying <code>serve</code> <strong>47 of 63</strong> (61 of 63) and <code>outcome</code> <strong>30 of 63</strong> (24 of 63); ITF qualifying <code>outcome</code> <strong>181 of 215</strong> men&#x27;s and <strong>142 of 176</strong> women&#x27;s (189 of 209, 143 of 168); WTA main-tour qualifying <code>outcome</code> <strong>5 of 48</strong>, against 47 of 88 in the same events&#x27; main draws (published as 3 of 48 against 82 of 88). Both descriptions now state which layer they count and name <code>basis</code> as the per-response answer.</li> +<li><code>enrichment</code> per match was correct throughout, on every one of these matches, and <code>basis</code> has always named which sequence you were given. Only the prose was wrong.</li> +</ul> +<p>No field, endpoint or behaviour changed; every correction is to a description.</p> + + + https://docs.livetennisapi.com/changelog.html#v1.13.41 + Live Tennis API 1.13.41 + 2026-09-22T00:00:00Z + + <h3>Fixed</h3> +<ul> +<li><strong>The <code>outcome</code> description named the wrong tour as the qualifying gap, and it was wrong in both directions.</strong> It read &quot;Qualifying draws follow their own tour, with one gap: ... Challenger (men) qualifying states no outcome at all.&quot; Measured 2026-09-22 over completed matches carrying a per-point stream since 2026-09-12, <strong>ATP Challenger qualifying states <code>outcome</code> on 191 of 197 matches, with the full five-value vocabulary</strong> (6,881 unforced errors, 4,275 winners, 4,267 forced errors, 1,026 aces, 774 double faults) — a reader who segregated a corpus on that sentence discarded the single richest qualifying population in the product. The gap is on the <strong>WTA main tour</strong>, which the old sentence implicitly cleared: WTA qualifying states <code>outcome</code> on <strong>3 of 48</strong>, against 82 of 88 in the same events&#x27; main draws. ITF qualifying is unchanged in direction (189 of 209 men&#x27;s, 143 of 168 women&#x27;s) and WTA 125 qualifying is partial (24 of 63). <code>enrichment</code> per match was correct on every one of these matches throughout — only the prose was wrong, and the prose is what a corpus is planned from.</li> +<li><strong><code>tour: challenger</code> pools two populations with different <code>outcome</code> coverage, and the reference described only one of them.</strong> It read &quot;Challenger: all five&quot;. That is true of ATP Challenger (<code>tier: challenger_50</code> … <code>challenger_175</code>): 252 of 255 completed main-draw matches with a stream carry the full five. It is not true of the WTA 125 events, which are also served as <code>tour: challenger</code> (<code>tier: wta_125</code>) and state <strong>ace and double fault only</strong> — 137 of 141 main-draw matches carry <code>outcome</code>, none of them a winner, forced error or unforced error. Read <code>tier</code>, not <code>tour</code>, to tell the two apart. Both descriptions now say so.</li> +<li><strong>The <code>serve</code> qualifying denominators are re-measured</strong> (WTA qualifying 48 of 48, ATP Challenger qualifying 195 of 197, WTA 125 qualifying 61 of 63) and name the populations by <code>tier</code> rather than by &quot;Challenger (men)&quot; / &quot;Challenger (women)&quot;, which the served fields do not distinguish.</li> +</ul> +<p>No field, endpoint or behaviour changed; every correction is to a description.</p> + + + https://docs.livetennisapi.com/changelog.html#v1.13.40 + Live Tennis API 1.13.40 + 2026-09-22T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong><code>tier</code> and <code>tier_source</code> on every match and tournament object, with <code>?tier=</code> on <code>GET /matches</code> and <code>GET /history/matches</code>.</strong> <code>tier</code> is the level the tournament was played at in the season of the match — the official category as the tour publishes it — and answers the question <code>category</code> cannot: is this a WTA 125, an ITF W35 or a Challenger 75? The vocabulary is closed: <code>grand_slam</code>; the ATP levels (<code>atp_finals</code>, <code>atp_1000</code>, <code>atp_500</code>, <code>atp_250</code>, <code>next_gen_finals</code>); the WTA levels (<code>wta_finals</code>, <code>wta_elite_trophy</code>, <code>wta_1000</code>, <code>wta_500</code>, <code>wta_250</code>, <code>wta_125</code>); <code>challenger_175</code> … <code>challenger_50</code>; the ITF World Tennis Tour categories (<code>itf_m15</code>, <code>itf_m25</code>, <code>itf_w15</code> … <code>itf_w100</code>, with the 2023 women&#x27;s categories kept as printed that season); the team events (<code>united_cup</code>, <code>davis_cup</code>, <code>bjk_cup</code>, <code>laver_cup</code>, <code>olympics</code>); <code>juniors</code>; <code>utr</code>; <code>exhibition</code>; or <code>null</code> — nothing we hold names the level, never guessed. The tier is per season and it moves under one <code>tournament_id</code>: Dallas was <code>atp_250</code> in 2023 and 2024 and <code>atp_500</code> from 2025, so a 2024 Dallas match reads <code>atp_250</code> and a 2025 one <code>atp_500</code> under the same id. A match&#x27;s tier is resolved by the calendar year of its <code>scheduled_time</code>, never copied from the tournament row; <code>/tournaments</code> rows carry the current season&#x27;s, null when this season&#x27;s calendar does not list the event. <code>tier_source</code> says how the level was established — <code>calendar</code> (the official per-season tour calendar), <code>name</code> (an unambiguous name rule: the ITF category is in the event&#x27;s official name, team and UTR events are named as such), <code>wikipedia</code> (the season&#x27;s schedule page, only where the official calendar could not state the level for that season), <code>resolver</code> (resolved after the seed dataset by the same rules) — and is null exactly when <code>tier</code> is null. The 2023–2026 seasons were verified event by event against the official calendars (99.1% of tournament-seasons resolved; the rest deliberately null). <code>?tier=</code> takes comma-separated exact values, read from the same per-season table so filter and field cannot disagree; a null tier matches no value; an unknown value is a <code>400 bad_tier</code> with the offending values in <code>bad</code> and the full vocabulary in <code>allowed</code>. <code>category</code> is unchanged and remains the coarse class.</li> +<li><strong>A correction to a published result is signalled, never a silent edit: <code>basis: restatement</code> on the status history, <code>result_restated_at</code> / <code>result_version</code> on every match object, and <code>?restated_since=</code> on <code>GET /history/matches</code>.</strong> Raised by a licensed results customer settling off our results. Once a match has been published as <code>completed</code> (or as a cancelled walkover with a winner), any later change to <code>status</code>, <code>event_status</code>, <code>winner</code> or the final score is appended to <code>GET /matches/{matchId}/status-history</code> as a new row with <code>basis: restatement</code>, at the instant we made the change: <code>status</code> / <code>event_status</code> carry before and after as usual (the same value twice when only the winner or the score moved) and <code>score</code> is the result after the correction. This holds for every path that can change a result — a source&#x27;s late final, a second authority&#x27;s correction, an operator&#x27;s repair, a completion reopened and re-closed — because it is enforced where the result is written, not by each path remembering to say so; a re-assertion of the same result, or a change to points alone, writes nothing. <code>result_restated_at</code> is the instant of the newest such row (null while the result stands as first published); <code>result_version</code> is <code>1</code> plus the number of corrections — record it with the result you settle on, and a higher number on re-read means the one you hold was superseded. <code>?restated_since=&lt;ISO instant&gt;</code> keeps only the matches corrected after that instant — the poll to run after each settlement pass; a <code>Z</code> or an offset, a naive value read as UTC, and a bare date refused as <code>400 bad_restated_since</code>, because a day is not an instant. Rows with this basis exist from 2026-09-22; earlier corrections were not signalled and nothing is reconstructed for them.</li> +<li><strong><code>winner</code> on every row of every history tape: who won the point that produced the row.</strong> Raised by a modelling customer initialising serve states from the tape — <code>server</code> and <code>serve</code> were stated per row, and the third fact a model needs was only implicit in the score step. <code>winner</code> (<code>1</code> | <code>2</code> | null) is judged from the score step between the row and the previous served row (the previous raw row on the raw sequence, the previous clean row under <code>?sequence=clean</code>), by the same rule the completeness ledger counts a legal transition with — and from nothing else: never the serve, the server, the outcome tag or the pattern of play. Null on the first row and wherever the step is not one attributable point: a re-sent row, a set opener, a backward correction, a multi-game jump. A step whose games total rises by exactly one attributes to the side whose count rose — the game point was theirs — whatever in-game points the step skipped, a missed poll or a game withheld whole (<code>meta.points.games_withheld</code>). Present on the raw and clean sequences, on <code>?points=complete</code> reads and on the pre-2023 archive tape; equal to <code>point_winner</code> wherever that older key is present, which is kept unchanged for existing readers. Derived once per response, never stored.</li> +<li><strong><code>schema_version</code> on the packages listing, and bulk tape rows carry the enriched tape.</strong> Under <code>schema_version</code> 2 a <code>kind=tape</code> JSONL line is exactly what <code>GET /history/matches/{matchId}</code> returns for that match on its default basis, produced by the same code — the same rows with <code>origin</code>, <code>serve</code>, <code>outcome</code> and <code>winner</code> beside the score columns, and the full <code>meta</code> (<code>enrichment</code>, <code>reconstructed_at</code>, <code>observed_span</code>, <code>meta.points</code> where served) — so a model reads its serve states from the bulk file just as it would from the endpoint; the CSV appends <code>origin</code>, <code>serve</code>, <code>outcome</code>, <code>winner</code> after <code>danger</code>, in that order, and its column order grows only at the end. <code>1</code> is the shape every month built before 22 September 2026 carries: score columns only, no <code>meta.enrichment</code>, and a CSV that ends at <code>danger</code>. Months built before that date keep shape <code>1</code> until they are rebuilt — newest month first, then backwards, <code>built_at</code> and <code>sha256</code> moving as each flips — so check the manifest rather than assuming; a consumer that needs the enriched fields should require <code>schema_version &gt;= 2</code> and, for a month still at <code>1</code>, read the per-match endpoint for the matches it needs. <code>null</code> on the non-tape kinds.</li> +</ul> +<h3>Changed</h3> +<ul> +<li><strong>A completed match on the tours with a complete per-point record is re-joined after the match against that complete sequence.</strong> ATP 250/500/1000, ATP Challenger and WTA 1000/500/250/125: the live join paired <code>serve</code> / <code>outcome</code> onto points as they were captured, so a point tagged after our capture went untagged. A completed match is now re-joined against the complete sequence by the same exact-state rule, and its serve/outcome coverage exceeds what was captured live. The late tags arrive as <code>point_update</code> frames and <code>tagged_at</code> revisions like any other — collect them with <code>?changed_since=</code>. Nothing is inferred; a point that is never stated stays null.</li> +<li><strong>The pre-2023 archive tape no longer claims <code>point_winner</code> is null throughout a per-game tape.</strong> On a per-game tape (556 matches, 555 of them 2013) consecutive rows differ by a whole game, and the reference said no point was attributable there. Each row in fact reads the side whose game count rose — the game point was theirs — and says nothing about the game&#x27;s other points; <code>winner</code> reads the same. No field changed shape or value.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.39 + Live Tennis API 1.13.39 + 2026-09-22T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong><code>is_qualifying</code> states the one case where a round label DOES decide the draw.</strong> The field said it is &quot;never inferred from the round label&quot;, which is the right instinct and slightly too strong: main-draw vocabulary (<code>Semi-finals</code>, <code>Final</code>) is indeed never read that way, but a round label that itself reads <code>Qualification Round 1</code> is STATING the draw, and it reads <code>true</code> — a <code>false</code> beside such a round is a payload contradicting itself rather than asserting main draw. Three matches (Chengdu, 22 Sep) were briefly served <code>is_qualifying: false</code> with <code>round_code: &quot;Q1&quot;</code> beside them and were repaired the same morning. Readers segregating a corpus by draw need to know which way that conflict resolves. No field changed shape or type.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.38 + Live Tennis API 1.13.38 + 2026-09-22T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong><code>GET /markets</code> states that the scope is MATCH-WINNER ONLY, and returns at most one market.</strong> A prospect asked whether the odds endpoints carry game spreads / handicaps (<code>A -3.5 @ 1.90</code>) in addition to match-winner prices. Every description in this reference already said &quot;match-winner market&quot;, but nowhere said what that EXCLUDES, and the endpoint&#x27;s own summary said &quot;market(s)&quot;. Both are now explicit: no handicaps or game spreads, no totals, no set-winner books, no per-game or per-set derivative; <code>data</code> holds at most one object and <code>meta.count</code> is 0 when nothing is mapped. Venues do list tennis derivatives beside the match-winner book and this API publishes none of them — a derivative is refused at the mapping step. No field changed shape or value.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.37 + Live Tennis API 1.13.37 + 2026-09-21T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong><code>is_qualifying</code> is documented on the match object.</strong> The field has been served on <code>GET /matches</code>, <code>GET /matches/{matchId}</code> and <code>GET /history/matches</code> for some time and was absent from this reference entirely, which left the one field that separates a qualifying draw from the main draw invisible to anyone reading the docs. Three-valued: <code>true</code>/<code>false</code> are the source&#x27;s own assertion, <code>null</code> means no source has ever stated it, and <code>null</code> is not <code>false</code>.</li> +</ul> +<h3>Changed</h3> +<ul> +<li><strong><code>round</code> and <code>round_code</code> now state that they are DRAW-RELATIVE.</strong> Both describe the round *within* the draw the match belongs to, and the draw is named by <code>is_qualifying</code> — not by the round. A qualifying semi-final carries <code>round: &quot;... - Semi-finals&quot;</code> and <code>round_code: SF</code>, exactly as a main-draw semi-final does; <code>Q</code>/<code>Q1</code>..<code>Q4</code> appear only where the label itself names the round as qualifying, which most labels do not. <code>round_code</code> previously said &quot;this is the field to branch on&quot; without that limit, and two customers independently read a qualifying match as a main-draw one and reported it as a data fault. The data was correct in every case; the documentation was not. No field changed shape or value.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.36 + Live Tennis API 1.13.36 + 2026-09-21T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong><code>GET /charting/players</code> states a sample of 11,803 charted matches, not 11,646.</strong> The charted corpus was refreshed on 2026-09-19 and 184 matches — 133 of them played after 2026-05-24, including the US Open women&#x27;s draw to the quarter-finals — were loaded into the product on 2026-09-21. <code>matches_charted</code> on every response has always been the true per-player denominator and was never affected; the curated-coverage note beside it was quoting an older total and understating the sample. <code>GET /rally/matches</code> grew in the same load: 11,822 charted matches and 1,875,132 shot-by-shot points, with the newest women&#x27;s chart now 2026-09-09. The men&#x27;s corpus is unchanged because no men&#x27;s match has been charted since 2026-05-21. No field changed shape.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.35 + Live Tennis API 1.13.35 + 2026-09-21T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong><code>meta.points.games_short</code> on <code>GET /history/matches/{matchId}</code> counts the completed games the tape holds too few points for.</strong> Raised by a prospect evaluating per-point histories. A game the tape skips whole is a legal <code>0-0 → 0-0</code> boundary with the games counter up by one, and the transition test alone cannot see it — so a tape could read <code>complete: true</code> with a whole game missing. The measurement now walks the sequence by game boundary and compares what is held for each completed game with the fewest points that game can have contained given the last state it shows (from 0-0 at least 4; from 15-30 at least 5; from 40-40 at least 8; a tiebreak at least 7) — a lower bound, never an estimate. A game the tape skips whole counts; so does a game that ended with no further rows after its last stored point. <code>&gt; 0</code> forces <code>complete</code> to <code>false</code>; nothing is inferred or repaired, and the game&#x27;s boundary rows are still served exactly as stored. <code>null</code> = nothing measured (an empty sequence).</li> +<li><strong><code>basis_reason</code> on <code>GET /matches/{matchId}/points</code> says why a reconstruction was served instead of the stream.</strong> Present on the <code>reconstruction</code> basis only: <code>stream_absent</code> (no stored stream rows), <code>stream_incomplete</code> (the stream is legal but does not measure complete — it joined mid-match or stopped short — and carries no tags) or <code>stream_illegal</code> (at least one transition is not attributable to one point: a gap or a torn row). A stream that is complete, or tagged and legal end to end, is always served on the <code>live</code> basis and the key is absent. Additive; nothing else in the response changes shape.</li> +</ul> +<h3>Changed</h3> +<ul> +<li><strong><code>meta.points.complete</code> now means the served sequence holds at least every point the score requires — not merely that consecutive rows are consistent with each other.</strong> True only when the sequence opens at 0-0, every transition is a legal single-point step, it carries at least as many point transitions as the final scoreline implies the match contained, every completed game holds at least as many point transitions as it must have contained (<code>games_short</code> is <code>0</code>), it reaches a finished final scoreline at love (or the match ended early — retirement/walkover), it is not known-truncated, and no game was withheld (<code>games_withheld</code>). <code>ends_at_final</code> on the same object now also requires that the last row shows no game in progress — points at love or null, not a tiebreak in progress: a row carrying a finished spine with a game still underway (<code>[[6,6],[3,4]]</code> at 15-0) reads <code>false</code>, where before the spine alone decided. Some tapes that read <code>complete: true</code> until now will read <code>false</code>; no row is changed.</li> +<li><strong>A recurring score state on the history tape pairs its <code>serve</code>/<code>outcome</code> by nearest clock, instead of reading null.</strong> Tags on <code>GET /history/matches/{matchId}</code> rows are joined from the match&#x27;s per-point stream by exact score state, and a state the stream asserts more than once (a deuce cycle revisiting deuce) cannot be told apart by the score, so until now it read <code>null</code> outright. Where a state recurs the join now pairs by nearest clock within 90 seconds — the tape row&#x27;s observed clock against the stream point&#x27;s, each stream point used at most once — and reads <code>null</code> otherwise (no candidate within the window, or two at the same distance). Every tag is an observed pairing, state and where needed clock, never an inference from the pattern of play. Reconstructed rows and rows no stream point matches still read <code>null</code>.</li> +</ul> +<h3>Fixed</h3> +<ul> +<li><strong>A stored live stream that is complete, or tagged and legal end to end, is served on a completed match; a reconstruction is projected only when the stream falls short.</strong> Until now a measured-complete recorded sequence displaced the stream unconditionally, so a completed match could lose its <code>serve</code>/<code>outcome</code> tags and every per-point clock the moment a reconstruction landed — a projection carries neither, so a complete tagged stream is strictly more information than any reconstruction of the same match. <code>GET /matches/{matchId}/points</code> now keeps the <code>live</code> basis whenever the stream is itself measured complete (match-closing point included) or carries tags and is legal end to end (every transition one attributable point, judged in playing order), and serves the projection only when the stream has no rows, is incomplete and untagged, or holds a transition nobody can attribute — <code>basis_reason</code> says which. When the reconstruction serves it still serves wholesale; the two sequences are never interleaved. If a completed match reads <code>reconstruction</code>, re-read from <code>after_seq=0</code> rather than resuming a live cursor into it.</li> +<li><strong>A double fault stated on a first serve is a contradiction; the point is served untagged.</strong> A source point whose serve number and outcome contradict each other — a double fault on serve 1 — was carried through as stated. <code>serve</code> and <code>outcome</code> on such a point now both read <code>null</code> on <code>GET /matches/{matchId}/points</code> and on the WebSocket frames rather than tagged wrongly; nothing is corrected to a second serve. Stored tags that carried the contradiction have been cleared.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.34 + Live Tennis API 1.13.34 + 2026-09-21T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong>The reference said Davis Cup and the Grand Slams &quot;read null throughout&quot; for the per-point tags; <code>serve</code> is in fact stated on the top Davis Cup tiers.</strong> <code>serve</code> and <code>outcome</code> are stated separately, and the old sentence excluded both on the strength of one of them: team ties and Slams carry no per-point outcome, so <code>outcome</code> is genuinely <code>null</code> there — but the serve number is stated on them. Measured over completed matches with a per-point stream since 2026-09-12: Davis Cup World Group states <code>serve</code> on 22 of 22 matches and World Group I on 41 of 42, while World Group II states it on 0 of 38; <code>outcome</code> is null on all of them, on every one of the 14,132 rows. The machine-readable authority was right throughout and is unchanged — a World Group I match answers <code>enrichment: {&quot;serve&quot;: &quot;stated&quot;, &quot;outcome&quot;: &quot;none&quot;}</code> and a World Group II match answers <code>{&quot;serve&quot;: &quot;none&quot;, &quot;outcome&quot;: &quot;none&quot;}</code> — so this corrects the prose to match what the API already returns, in the direction of more coverage, not less. The <code>serve</code> description now names the Davis Cup tiers in its coverage list with the measurement; the <code>outcome</code> description now treats the two fields separately instead of excluding a competition from both. No field, endpoint or behaviour change.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.33 + Live Tennis API 1.13.33 + 2026-09-21T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong><code>serve</code> and <code>outcome</code> said nothing about qualifying or doubles draws, and the internal reference called both blanket-<code>null</code>.</strong> The per-point tags (added 2026-09-12) were documented tour by tour for main draws only. Measured over completed matches carrying a per-point stream since the source went live on 2026-09-12: WTA qualifying states <code>serve</code> on 48 of 48, Challenger (men) qualifying on 153 of 155, Challenger (women) on 49 of 51; ITF qualifying states <code>outcome</code> (aces and double faults) on 95 of 107 men&#x27;s and 62 of 74 women&#x27;s. Qualifying draws are covered like their own main draw, with exactly one real gap — <code>outcome</code> on Challenger (men) qualifying, which no source states at all. The doubles half of the old claim is confirmed and now carries its denominator: across 454 completed doubles matches in the same window, not one row carries either field. Both descriptions now state where each field is stated, including the qualifying and doubles positions; <code>enrichment</code> per match remains the authority and none of it is a promise about a match not yet played. Found by the every-sweep tag-coverage check, which reads 85 of 86 Challenger qualifying matches carrying <code>serve</code> against a documented <code>null</code>. No field, endpoint or behaviour change.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.32 + Live Tennis API 1.13.32 + 2026-09-21T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong>A match that ends in a tiebreak closes with the set roll-up row after the decisive tiebreak score.</strong> The match-closing row on <code>GET /matches/{matchId}/points</code> was described as if the last point were always a game point one point short of the final, and a match decided in a tiebreak did not fit that shape. The docs now say what the stream actually holds: when a completed match ends in a tiebreak, the stream&#x27;s last stored row is the decisive tiebreak score itself (7-3, or 8-6) and the closing row is the set roll-up after it — the next game number, <code>number</code> 0, <code>tiebreak</code> false, <code>sets</code> incremented for the tiebreak winner, the set banked 7-6 in <code>games</code>, <code>score</code> 0-0, <code>server</code> null, <code>winner</code> the tiebreak winner — the same row the stream stores after every other set-ending tiebreak, so <code>ends_at_final</code> reads <code>true</code> there. A 10-point match tiebreak still cannot be stated as one point and is refused as before (<code>ends_at_final: false</code>), as is a tape that never reached the final.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.31 + Live Tennis API 1.13.31 + 2026-09-20T00:00:00Z + + <h3>Added</h3> +<ul> +<li><strong><code>GET /matches/{matchId}/points</code> now closes a completed match with the match-closing point, and <code>ends_at_final</code> says whether it did.</strong> Raised by a prospect evaluating per-point tapes, who asked where the point that wins the match is. Every row is the state *after* a point, so a game-winning point is carried by the next game&#x27;s <code>number: 0</code> opener — and the match-winning point had no next row to be carried by: the live stream never held it, and a serve statistic built off the stream was missing every match&#x27;s last point. On a completed match served on the <code>live</code> basis the page now ends with one terminal row: <code>seq</code> = last + 1, <code>number</code> 0, <code>sets</code> / <code>games</code> the final score, <code>score</code> 0-0, <code>tiebreak</code> false, <code>server</code> null (nobody serves next), <code>winner</code> the match winner, <code>ts</code> the instant the final score was observed. It is built at read time from the stream&#x27;s last row and the observed final score, and only when the two are one point apart; nothing is fabricated otherwise. <code>ends_at_final</code> (boolean) on the response says whether the sequence served ends on that row: <code>false</code> on a completed match whose stream stops short of it (a retirement or walkover, a capture that stopped two or more points short, a closer from deuce or from a match tiebreak that cannot be stated as one point), always <code>false</code> on a live match, and on the <code>reconstruction</code> basis judged from the projected sequence&#x27;s last frame. <code>serve</code> / <code>outcome</code> on the terminal row are null: no source&#x27;s tag for a match&#x27;s last point is stored yet. WebSocket and push frames are unchanged.</li> +<li><strong><code>tagged_at</code> on every point row, and <code>?changed_since=</code> on the same endpoint, so late serve/outcome tags are collectable without a socket.</strong> The same prospect asked how to pick up tags that land after a row was read. <code>after_seq</code> is a cursor by <code>seq</code>, so it can never return a row already held — and a <code>serve</code> / <code>outcome</code> tag that lands late lands on exactly such a row; until now the only way to see it was the <code>point_update</code> frame on a WebSocket. Every row now carries <code>tagged_at</code>, the UTC instant its tags landed (null while none has, and on every <code>reconstruction</code>-basis frame), and <code>?changed_since=&lt;ISO-8601 instant&gt;</code> (<code>2026-09-20T00:35:18Z</code>; <code>Z</code> or an offset, a naive value is read as UTC, a date alone is refused) returns only the rows whose <code>ts</code> <strong>or</strong> <code>tagged_at</code> is later than that instant, in <code>seq</code> order, paged as usual and composable with <code>after_seq</code>. The post-match recipe: read the match, keep the instant, re-read with <code>changed_since=&lt;that instant&gt;</code> and replace held rows by <code>seq</code>. Anything that is not an ISO-8601 timestamp is a <code>400 bad_changed_since</code>. On the <code>reconstruction</code> basis no row carries a clock or a tag, so a <code>changed_since</code> read of it is an empty page: that sequence is final at first read.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.30 + Live Tennis API 1.13.30 + 2026-09-20T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong>The ATP per-point outcome row said &quot;full&quot; as a projection, not a measurement.</strong> In the 30 days since ATP per-point outcomes went live (2026-09-12), no ATP main-tour event was actually played — the only tour tennis in that window was a Grand Slam and Davis Cup, neither of which carries per-point outcomes — so every ATP main-tour match carried no outcome tags at all, while the docs asserted <code>full</code> (ace, double fault, winner, forced error, unforced error) unconditionally. The row now names the tours covered (ATP 250 / 500 / 1000), states plainly that Grand Slams and Davis Cup are not on it and read null throughout (check <code>enrichment</code> per match), and gives the first date the claim was actually measured against a played tour event: the week of 2026-09-22.</li> +</ul> + + + https://docs.livetennisapi.com/changelog.html#v1.13.29 + Live Tennis API 1.13.29 + 2026-09-20T00:00:00Z + + <h3>Changed</h3> +<ul> +<li><strong>The <code>GET /history/matches</code> listing&#x27;s point-completeness ledger now judges the same raw sequence the per-match read serves, re-sent rows dropped — not the <code>clean</code> collapse.</strong> The ledger&#x27;s previous default basis, <code>clean</code>, collapses the tape to one row per distinct score state; that collapse can delete a real deuce point — two rows sitting at 40-40 with no advantage row between them is a legitimate rally, not a duplicate — and call the resulting, shorter tape point-complete when the tape actually served is not. The ledger now judges the raw sequence with provable re-sends dropped, the exact judgement the per-match read makes and publishes as <code>meta.points.complete</code>, so <code>tape.points_complete_default</code> on the listing and <code>meta.points.complete</code> on a fetched match agree by construction instead of disagreeing on a sequence the read never served. Measured on 7,717 completed matches scored since 1 Sep: the <code>clean</code> collapse called 70 of them point-complete whose served (raw) tape was not, and missed 810 whose served tape was. <code>clean</code> remains available only as a request option (<code>?sequence=clean</code>); rows scored before 2026-09-20 keep <code>points_basis: clean</code> until the nightly re-score reaches them. <code>tape.points_basis</code>, <code>tape.points_complete_default</code> and <code>tape.points_complete</code> descriptions updated to match.</li> +</ul> +<h3>Fixed</h3> +<ul> +<li><strong><code>tape.computed_at</code> on the <code>GET /history/matches</code> listing carried the serving host&#x27;s UTC offset (<code>+03:00</code> on prod) instead of a trailing <code>Z</code>.</strong> Same instant, valid ISO-8601, but every other timestamp in the payload — <code>meta.generated_at</code>, each row&#x27;s <code>timestamp</code> — ends in <code>Z</code>, so a consumer diffing <code>computed_at</code> against either had to normalise it by hand first. Now projected to UTC with a trailing <code>Z</code> like the rest of the surface.</li> +</ul> + + diff --git a/docs/changelog.html b/docs/changelog.html index 1425d1e..89e1fe7 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -13,6 +13,8 @@ + + @@ -205,6 +207,7 @@

Live Tennis API · docs

Changelog

Every change to the specification, dated and versioned. The API surface is v1; changes within it are additive only. Current version 1.13.78, last change . Also as the full reference, OpenAPI YAML / JSON, and the dated facts file.

+

Subscribe in a feed reader: Atom or JSON Feed.

1.13.78

Changed

diff --git a/docs/changelog.json b/docs/changelog.json new file mode 100644 index 0000000..96212dd --- /dev/null +++ b/docs/changelog.json @@ -0,0 +1,367 @@ +{ + "version": "https://jsonfeed.org/version/1.1", + "title": "Live Tennis API changelog", + "home_page_url": "https://docs.livetennisapi.com/changelog.html", + "feed_url": "https://docs.livetennisapi.com/changelog.json", + "description": "Every change to the Live Tennis API specification, dated and versioned.", + "icon": "https://docs.livetennisapi.com/icon-256.png", + "authors": [ + { + "name": "Live Tennis API", + "url": "https://livetennisapi.com" + } + ], + "language": "en", + "items": [ + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.78", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.78", + "title": "Live Tennis API 1.13.78", + "content_html": "

Changed

\n", + "date_published": "2026-10-29T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.77", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.77", + "title": "Live Tennis API 1.13.77", + "content_html": "

Fixed

\n", + "date_published": "2026-10-29T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.76", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.76", + "title": "Live Tennis API 1.13.76", + "content_html": "

Added

\n\n

Fixed

\n\n

Deprecated

\n", + "date_published": "2026-10-28T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.75", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.75", + "title": "Live Tennis API 1.13.75", + "content_html": "

Changed

\n", + "date_published": "2026-10-28T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.74", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.74", + "title": "Live Tennis API 1.13.74", + "content_html": "

Changed

\n", + "date_published": "2026-10-26T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.73", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.73", + "title": "Live Tennis API 1.13.73", + "content_html": "

Changed

\n", + "date_published": "2026-10-26T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.72", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.72", + "title": "Live Tennis API 1.13.72", + "content_html": "

Added

\n", + "date_published": "2026-10-26T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.71", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.71", + "title": "Live Tennis API 1.13.71", + "content_html": "

Fixed

\n", + "date_published": "2026-10-26T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.70", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.70", + "title": "Live Tennis API 1.13.70", + "content_html": "

Fixed

\n", + "date_published": "2026-10-23T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.69", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.69", + "title": "Live Tennis API 1.13.69", + "content_html": "

Fixed

\n", + "date_published": "2026-10-23T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.68", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.68", + "title": "Live Tennis API 1.13.68", + "content_html": "

Changed

\n", + "date_published": "2026-10-23T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.67", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.67", + "title": "Live Tennis API 1.13.67", + "content_html": "

Fixed

\n", + "date_published": "2026-10-22T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.66", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.66", + "title": "Live Tennis API 1.13.66", + "content_html": "

Fixed

\n", + "date_published": "2026-10-22T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.65", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.65", + "title": "Live Tennis API 1.13.65", + "content_html": "

Changed

\n", + "date_published": "2026-10-22T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.64", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.64", + "title": "Live Tennis API 1.13.64", + "content_html": "

Changed

\n", + "date_published": "2026-10-22T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.63", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.63", + "title": "Live Tennis API 1.13.63", + "content_html": "

Fixed

\n", + "date_published": "2026-10-22T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.62", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.62", + "title": "Live Tennis API 1.13.62", + "content_html": "

Fixed

\n", + "date_published": "2026-10-22T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.61", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.61", + "title": "Live Tennis API 1.13.61", + "content_html": "

Added

\n\n

Changed

\n", + "date_published": "2026-10-22T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.60", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.60", + "title": "Live Tennis API 1.13.60", + "content_html": "

Added

\n", + "date_published": "2026-10-03T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.59", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.59", + "title": "Live Tennis API 1.13.59", + "content_html": "

Added

\n\n

Fixed

\n", + "date_published": "2026-10-02T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.58", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.58", + "title": "Live Tennis API 1.13.58", + "content_html": "

Changed

\n", + "date_published": "2026-10-01T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.57", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.57", + "title": "Live Tennis API 1.13.57", + "content_html": "

Fixed

\n", + "date_published": "2026-10-01T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.56", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.56", + "title": "Live Tennis API 1.13.56", + "content_html": "

Fixed

\n", + "date_published": "2026-10-01T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.55", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.55", + "title": "Live Tennis API 1.13.55", + "content_html": "

Fixed

\n", + "date_published": "2026-09-29T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.54", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.54", + "title": "Live Tennis API 1.13.54", + "content_html": "

Added

\n", + "date_published": "2026-09-28T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.53", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.53", + "title": "Live Tennis API 1.13.53", + "content_html": "

Added

\n\n

Fixed

\n", + "date_published": "2026-09-26T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.52", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.52", + "title": "Live Tennis API 1.13.52", + "content_html": "

Added

\n\n

Fixed

\n", + "date_published": "2026-09-25T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.51", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.51", + "title": "Live Tennis API 1.13.51", + "content_html": "

Added

\n", + "date_published": "2026-09-25T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.50", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.50", + "title": "Live Tennis API 1.13.50", + "content_html": "

Added

\n\n

Changed

\n", + "date_published": "2026-09-25T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.49", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.49", + "title": "Live Tennis API 1.13.49", + "content_html": "

Fixed

\n", + "date_published": "2026-09-24T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.48", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.48", + "title": "Live Tennis API 1.13.48", + "content_html": "

Fixed

\n", + "date_published": "2026-09-24T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.47", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.47", + "title": "Live Tennis API 1.13.47", + "content_html": "

Changed

\n", + "date_published": "2026-09-24T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.46", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.46", + "title": "Live Tennis API 1.13.46", + "content_html": "

Added

\n", + "date_published": "2026-09-24T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.45", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.45", + "title": "Live Tennis API 1.13.45", + "content_html": "

Added

\n\n

No field, endpoint or behaviour changed by this release; both were already served.

", + "date_published": "2026-09-24T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.44", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.44", + "title": "Live Tennis API 1.13.44", + "content_html": "", + "date_published": "2026-09-23T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.43", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.43", + "title": "Live Tennis API 1.13.43", + "content_html": "

Fixed

\n\n

No field, endpoint or behaviour changed; every correction is to a description.

", + "date_published": "2026-09-23T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.42", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.42", + "title": "Live Tennis API 1.13.42", + "content_html": "

Fixed

\n\n

No field, endpoint or behaviour changed; every correction is to a description.

", + "date_published": "2026-09-23T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.41", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.41", + "title": "Live Tennis API 1.13.41", + "content_html": "

Fixed

\n\n

No field, endpoint or behaviour changed; every correction is to a description.

", + "date_published": "2026-09-22T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.40", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.40", + "title": "Live Tennis API 1.13.40", + "content_html": "

Added

\n\n

Changed

\n", + "date_published": "2026-09-22T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.39", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.39", + "title": "Live Tennis API 1.13.39", + "content_html": "

Changed

\n", + "date_published": "2026-09-22T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.38", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.38", + "title": "Live Tennis API 1.13.38", + "content_html": "

Changed

\n", + "date_published": "2026-09-22T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.37", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.37", + "title": "Live Tennis API 1.13.37", + "content_html": "

Added

\n\n

Changed

\n", + "date_published": "2026-09-21T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.36", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.36", + "title": "Live Tennis API 1.13.36", + "content_html": "

Changed

\n", + "date_published": "2026-09-21T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.35", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.35", + "title": "Live Tennis API 1.13.35", + "content_html": "

Added

\n\n

Changed

\n\n

Fixed

\n", + "date_published": "2026-09-21T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.34", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.34", + "title": "Live Tennis API 1.13.34", + "content_html": "

Changed

\n", + "date_published": "2026-09-21T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.33", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.33", + "title": "Live Tennis API 1.13.33", + "content_html": "

Changed

\n", + "date_published": "2026-09-21T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.32", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.32", + "title": "Live Tennis API 1.13.32", + "content_html": "

Changed

\n", + "date_published": "2026-09-21T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.31", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.31", + "title": "Live Tennis API 1.13.31", + "content_html": "

Added

\n", + "date_published": "2026-09-20T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.30", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.30", + "title": "Live Tennis API 1.13.30", + "content_html": "

Changed

\n", + "date_published": "2026-09-20T00:00:00Z" + }, + { + "id": "https://docs.livetennisapi.com/changelog.html#v1.13.29", + "url": "https://docs.livetennisapi.com/changelog.html#v1.13.29", + "title": "Live Tennis API 1.13.29", + "content_html": "

Changed

\n\n

Fixed

\n", + "date_published": "2026-09-20T00:00:00Z" + } + ] +} diff --git a/docs/llms.txt b/docs/llms.txt index 93942c3..98a0826 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -9,6 +9,7 @@ Base URL: https://api.livetennisapi.com/api/public/v1 Full text reference: https://docs.livetennisapi.com/reference.html Changelog (dated, versioned): https://docs.livetennisapi.com/changelog.html +Changelog feeds: https://docs.livetennisapi.com/changelog.atom (Atom), https://docs.livetennisapi.com/changelog.json (JSON Feed) OpenAPI spec: https://docs.livetennisapi.com/openapi.yaml OpenAPI spec (JSON): https://docs.livetennisapi.com/openapi.json Website: https://livetennisapi.com diff --git a/scripts/build_reference.py b/scripts/build_reference.py index 4ccd883..81834e9 100644 --- a/scripts/build_reference.py +++ b/scripts/build_reference.py @@ -1207,6 +1207,7 @@ def build_llms_txt(spec: dict[str, Any]) -> str: f"Base URL: {base}", f"Full text reference: {DOCS_URL}/reference.html", f"Changelog (dated, versioned): {DOCS_URL}/changelog.html", + f"Changelog feeds: {DOCS_URL}/{FEED_ATOM} (Atom), {DOCS_URL}/{FEED_JSON} (JSON Feed)", f"OpenAPI spec: {DOCS_URL}/openapi.yaml", f"OpenAPI spec (JSON): {DOCS_URL}/openapi.json", f"Website: {SITE}", @@ -1453,6 +1454,76 @@ def flush_para() -> None: return "\n".join(html_parts) +# ------------------------------------------------------------------ changelog feeds ------------ +# The changelog as Atom and as JSON Feed 1.1, so a developer can subscribe to API changes in a feed +# reader instead of re-reading the page. Both are written from the same parsed entries as +# changelog.html, newest first, and --check keeps them current. +FEED_ATOM = "changelog.atom" +FEED_JSON = "changelog.json" +FEED_LIMIT = 50 + + +def _feed_entries() -> list[dict[str, Any]]: + return changelog_entries(CHANGELOG.read_text(encoding="utf-8"))[:FEED_LIMIT] + + +def build_changelog_atom(spec: dict[str, Any]) -> str: + entries = _feed_entries() + updated = f"{entries[0]['date']}T00:00:00Z" if entries else "1970-01-01T00:00:00Z" + title = f"{spec['info']['title']} changelog" + items = [] + for e in entries: + url = f"{DOCS_URL}/changelog.html#v{e['version']}" + items.append( + " \n" + f" {E(url)}\n" + f" {E(spec['info']['title'])} {E(e['version'])}\n" + f" {e['date']}T00:00:00Z\n" + f' \n' + f' {E(_md_block(e["body_md"]))}\n' + " " + ) + return ( + '\n' + '\n' + f" {DOCS_URL}/{FEED_ATOM}\n" + f" {E(title)}\n" + " Every change to the Live Tennis API specification, dated and versioned.\n" + f" {updated}\n" + f' \n' + f' \n' + " Live Tennis APIhttps://livetennisapi.com\n" + f" {DOCS_URL}/icon-256.png\n" + + "\n".join(items) + + "\n\n" + ) + + +def build_changelog_jsonfeed(spec: dict[str, Any]) -> str: + entries = _feed_entries() + feed = { + "version": "https://jsonfeed.org/version/1.1", + "title": f"{spec['info']['title']} changelog", + "home_page_url": f"{DOCS_URL}/changelog.html", + "feed_url": f"{DOCS_URL}/{FEED_JSON}", + "description": "Every change to the Live Tennis API specification, dated and versioned.", + "icon": f"{DOCS_URL}/icon-256.png", + "authors": [{"name": "Live Tennis API", "url": SITE}], + "language": "en", + "items": [ + { + "id": f"{DOCS_URL}/changelog.html#v{e['version']}", + "url": f"{DOCS_URL}/changelog.html#v{e['version']}", + "title": f"{spec['info']['title']} {e['version']}", + "content_html": _md_block(e["body_md"]), + "date_published": f"{e['date']}T00:00:00Z", + } + for e in entries + ], + } + return json.dumps(feed, ensure_ascii=False, indent=1) + "\n" + + def build_changelog(spec: dict[str, Any], reference_html: str) -> tuple[str, str]: """(html, latest_date). Same head shape and stylesheet as the reference so it reads as one site; each entry is a dated
so an engine can quote 'as of' from the page itself. @@ -1484,6 +1555,8 @@ def build_changelog(spec: dict[str, Any], reference_html: str) -> tuple[str, str + + @@ -1498,6 +1571,7 @@ def build_changelog(spec: dict[str, Any], reference_html: str) -> tuple[str, str

Live Tennis API · docs

Changelog

Every change to the specification, dated and versioned. The API surface is v1; changes within it are additive only. Current version {E(info['version'])}, last change . Also as the full reference, OpenAPI YAML / JSON, and the dated facts file.

+

Subscribe in a feed reader: Atom or JSON Feed.

{articles} @@ -2174,6 +2248,9 @@ def main() -> int: # The same spec as JSON, for the agents and tools that only read JSON (GPT Actions, # Postman import, most MCP scaffolds). Both URLs are stable and both are advertised. DOCS / "openapi.json": json.dumps(spec, ensure_ascii=False, indent=1) + "\n", + # The changelog as feeds, from the same entries as changelog.html. + DOCS / FEED_ATOM: build_changelog_atom(spec), + DOCS / FEED_JSON: build_changelog_jsonfeed(spec), } # Heading levels must not skip. h1 -> h3 is a level a screen reader reports as missing and diff --git a/scripts/truthcheck.sh b/scripts/truthcheck.sh index 6640dd7..2596d3c 100755 --- a/scripts/truthcheck.sh +++ b/scripts/truthcheck.sh @@ -6,11 +6,12 @@ # to be present. CHANGELOG.md is exempt: its dated entries legitimately quote # the grid as it stood at the time — and since 2026-09-14 docs/changelog.html is # that same file rendered, so it inherits the exemption or the rule contradicts -# itself. Nothing else generated from the spec is exempt. +# itself. The Atom and JSON feeds (docs/changelog.atom, docs/changelog.json) are +# the same entries again and share the exemption. Nothing else generated from the spec is exempt. set -eu cd "$(dirname "$0")/.." -files=$(git ls-files | grep -vE '^CHANGELOG\.md$|^docs/changelog\.html$|^scripts/truthcheck\.sh$|\.(png|jpg|jpeg|ico|svg|woff2?)$') +files=$(git ls-files | grep -vE '^CHANGELOG\.md$|^docs/changelog\.(html|atom|json)$|^scripts/truthcheck\.sh$|\.(png|jpg|jpeg|ico|svg|woff2?)$') fail=0