GET /history/matches
Completed matches, newest first, with derived winner and tape coverage (BASIC)
Requires BASIC on the live API, or any Historical Data API plan (Starter and up — see https://livetennisapi.com/historical-tennis-data-api). All tours, January 2023 → now (deeper results live in the results archive, 1968–2022, at /history/archive/matches). Filter to a date range with from/to, and by tour, draw (singles/doubles), player (either participant) and country — same vocabulary as /matches. Each item carries a tape object saying what point-by-point data we hold for that match, so a whole page can be qualified in one call instead of one request per match. NOTE ?coverage= is applied AFTER the page is cut, so a filtered page is routinely shorter than limit (and may be empty) while later pages still hold matching matches — a short filtered page is not an end-of-data signal; ?points_complete= filters the same way.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
limit | query | integer | no | Default 50. |
offset | query | integer | no | Default 0. |
from | query | string | no | Earliest play date, YYYY-MM-DD or an ISO-8601 UTC datetime. A bare date covers that whole day. An unparseable value is a 400, never a silently unfiltered 200. |
to | query | string | no | Latest play date, same formats as from (a bare date includes everything played that day). from after to is a 400. |
tour | query | string (atp, wta, challenger, itf, juniors) | no | Restrict results to one tour. Each value covers its singles and doubles draws, so atp includes ATP doubles and juniors covers the boys' and girls' Grand Slam draws. Omit for all tours. An unrecognised value is a 400 rather than a silent pass-through, so a caller never receives a tour it did not ask for. Applied before pagination, so meta.count reflects the filtered set. |
draw | query | string (singles, doubles) | no | Draw filter (added 2026-08-18) — the axis the tour filter deliberately collapses; the two compose (?tour=itf&draw=doubles is the ITF doubles slice). Same vocabulary as the draw field on Match, decided by the same shared definition, so filter and field cannot disagree. A row whose draw is null — a team tie, or no stated event type and no doubles-team participant — matches NEITHER value: null is an answer, not a wildcard. Two honesty notes: on /tournaments the answer comes from the event type alone (a tournament row has no participants to supply the doubles-team evidence matches have), and draw=doubles alone also returns mixed and exhibition doubles that no tour value reaches. An unknown value is a 400 bad_draw with the allowed values. |
player | query | array of integer | no | Filter to matches where this player id is EITHER participant. Repeatable (max 50 ids); multiple values return the deduplicated union. An unknown id returns an honest empty list, not an error; a non-integer value is a 400 bad_request. Before 2026-08-03 this parameter was accepted and silently ignored — treat any integration written against that behaviour as unfiltered. |
country | query | string | no | Filter to matches where EITHER participant's player.country equals this lowercase 3-letter code — the same vocabulary the Player object returns (IOC-style codes, e.g. ned, sui, gre; NOT ISO-3166). Players with no recorded country never match, so a country filter excludes unknown-nationality matches rather than guessing. A value that is not 3 letters is a 400 bad_country. |
tournament_id | query | string | no | Filter to one tournament — exact match on the stable numeric id every match row publishes as tournament_id and /tournaments publishes as id, so the value round-trips straight off any response. Works on /matches (every status) and /history/matches, and composes with every other filter: ?tour=atp&draw=singles&tournament_id=1217 is one slam's men's singles, where ?tour= alone still mixes concurrent events during a slam fortnight. There is no separate edition or occurrence id: tournament_id is stable across seasons, so one EDITION is tournament_id plus a from/to window around its dates. Matches whose tournament_id is null (a tournament not yet in the catalogue) never match this filter; a date-window listing without it still returns them. Unknown-but-numeric ids return an empty list, not an error; a non-numeric value is a 400 bad_tournament_id. |
coverage | query | string (from_start, partial, reconstructed, reconstructed_partial, none) | no | Keep only matches whose tape has this coverage. An unknown value is a 400 bad_coverage listing the accepted values in allowed. |
points_complete | query | string (true, false) | no | Keep only matches whose measured point-completeness ledger verdict is this value — best-basis (the served tape OR an on-disk reconstruction measured point-complete; fetch the latter with ?points=complete on the per-match tape). The ledger is a per-match cache reconverged nightly. A match not yet measured matches NEITHER value; anything but true/false is a 400 bad_points_complete. Applied AFTER the page is cut, exactly like ?coverage=. |
Responses
| Status | Meaning |
|---|---|
200 | Completed matches (winner = 1|2|null, from final sets), each with its tape coverage |
400 | Bad query parameter |
401 | Missing, unknown, or disabled credentials |
403 | Your tier doesn't unlock this endpoint |
429 | Rate limit exceeded (Retry-After header present). Three body shapes, told apart by error and scope: the per-MINUTE limit (rate_limited, with upgrade_url, tier and price naming the next tier up); the per-DAY quota (rate_limited with scope: "day", limit_per_day, and resets_at — the absolute ISO instant the daily window resets); and abuse_throttled with retry_at_epoch — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it. |
Response fields
| Field | Type | Description |
|---|---|---|
data | array of object | |
meta | object |
Example
curl https://api.livetennisapi.com/api/public/v1/history/matches \
-H "Authorization: Bearer twjp_..."
GET /history/coverage
Measured completeness rollup per tour × draw bucket (BASIC)
Requires BASIC on the live API, or any Historical Data API plan (Starter and up — see https://livetennisapi.com/historical-tennis-data-api). The numbers to read BEFORE choosing what to backtest, in one call instead of paging the archive. A PREBUILT snapshot rebuilt nightly right after the completeness ledger reconverges — never computed at read time — so as_of (= built_at) dates every number, and ledger_max_computed_at is the newest underlying per-match measurement. Buckets are atp/wta/challenger/itf/juniors × singles/doubles plus other (team ties, mixed, exhibitions, and matches with no stated event type — counted, never dropped, so the totals cannot lie), derived from the same registries as the tour and draw fields. method states the full measurement rule in one paragraph, so every number carries its own definition. As of 2026-08-18 the headline spread it exposes: 51.1% of ITF singles matches are point-complete on the best basis against 3.5% of ITF doubles — do not extrapolate a completeness rate across a tour group.
Responses
| Status | Meaning |
|---|---|
200 | The rollup artifact, dated by its own as_of |
401 | Missing, unknown, or disabled credentials |
403 | Your tier doesn't unlock this endpoint |
429 | Rate limit exceeded (Retry-After header present). Three body shapes, told apart by error and scope: the per-MINUTE limit (rate_limited, with upgrade_url, tier and price naming the next tier up); the per-DAY quota (rate_limited with scope: "day", limit_per_day, and resets_at — the absolute ISO instant the daily window resets); and abuse_throttled with retry_at_epoch — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it. |
503 | coverage_unavailable — the artifact has not been built yet (or is unreadable). Honest and temporary; retry after the nightly build. The rollup is never computed inline. |
Response fields
| Field | Type | Description |
|---|---|---|
as_of | string | Equal to built_at, verbatim — the artifact's own clock, and the date to quote with every number in this response. |
built_at | string | When the rollup was built (UTC). |
ledger_max_computed_at | string or null | The newest underlying per-match measurement in the completeness ledger. |
method | string | The full measurement rule for point_complete, in one paragraph — every number carries its own definition. |
buckets | object | One CoverageBucket per tour × draw bucket (atp_singles … juniors_doubles, plus other). A bucket with zero completed matches is OMITTED rather than emitted as zeros — read a missing key as "nothing to count", not an error. |
totals | object | The five verifiable numbers for one bucket. |
Example
curl https://api.livetennisapi.com/api/public/v1/history/coverage \
-H "Authorization: Bearer twjp_..."
GET /history/matches/{matchId}
Per-match tape — point-by-point score + per-point model probabilities (BASIC)
The tape is the point-by-point score sequence we hold for this match — every recorded score row including the model fields win_probability_p1 and danger at that point — plus match metadata and the model profiles produced during the match. The model fields here are part of the paid History product by design, distinct from the ULTRA-gated LIVE model fields. One match per request. Requires BASIC on the live API, or the Historical Data API Starter plan and up.
The tape is NOT guaranteed to cover the whole match — check meta.coverage and meta.point_source before backtesting. Rows expanded after the fact from a finished-match point-by-point record carry a null timestamp and null model fields; nothing is ever synthesised.
WORKS ON A LIVE MATCH, not only a completed one. The tape is assembled from whatever has been committed so far, so it is how you read the point-by-point history of a match in progress — including games played before you started watching, where we were already watching them. The LIST endpoint is completed-only; get live ids from /matches?status=live. /matches/{matchId}/score is one state; this is the sequence of states.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
matchId | path | integer | yes | The match id. Every match route shares ONE id space, so the same value works everywhere matchId appears.
Get it from any match list, where it is the id field of each match object:
GET /matches?status=live (in progress), GET /matches?status=upcoming or GET /fixtures (scheduled), GET /matches?status=completed or GET /history/matches (finished).
Ids are stable for the life of a match, so one captured before it starts still resolves after it finishes.
|
sequence | query | string (raw, clean) | no | raw (default) is every row we committed, in commit order — deliberately non-monotonic, since independent sources race and a higher-trust one may correct a lower-trust one backwards. clean returns one row per distinct score state, keeping the last assertion of each, ordered by MATCH PROGRESS (the same ranking our arbiter uses), not by wall clock — so array order is the authoritative point order, and timestamp values may locally disagree with it by a few seconds where a stale re-assertion carried a later clock. Within a game, deuce states legitimately cycle (40-40 ↔ AD) and rank equally, so consecutive clean rows are not guaranteed point-monotonic there; point_winner is only set where the transition is a single attributable point. An unknown value is a 400 bad_sequence. Default raw. |
points | query | string (default, complete) | no | default serves observed rows first — what our own pipeline committed, a SAMPLED record of the match. complete is the explicit opt-out of that precedence for consumers who want every point: where a whole-match reconstruction exists it is served WHOLE, in its own point order, with point_winner on every row and null timestamps/model fields per the reconstruction contract. Where none exists, the response is exactly the default read plus meta.points (whose available_complete tells the cases apart) — no error. Cannot combine with sequence=clean (400 bad_combination — the state-key collapse would delete the repeated deuce states a complete point sequence contains). An unknown value is a 400 bad_points; where not yet enabled, complete answers 400 points_read_disabled rather than silently serving the default. coverage and meta.points are orthogonal axes: coverage says how the rows were OBTAINED, points says how COMPLETE the sequence is — completeness is only ever claimed per match, as measured. Default default. |
Responses
| Status | Meaning |
|---|---|
200 | The full tape (match + tape + profiles + coverage meta) |
400 | Bad query parameter |
401 | Missing, unknown, or disabled credentials |
403 | Your tier doesn't unlock this endpoint |
404 | No such resource, or no data yet |
410 | The match id existed and was merged into another match record (since 2026-09-05). The body is MatchMerged, not Error: it carries a forwarding address, which is the whole point of answering 410 rather than 404. |
429 | Rate limit exceeded (Retry-After header present). Three body shapes, told apart by error and scope: the per-MINUTE limit (rate_limited, with upgrade_url, tier and price naming the next tier up); the per-DAY quota (rate_limited with scope: "day", limit_per_day, and resets_at — the absolute ISO instant the daily window resets); and abuse_throttled with retry_at_epoch — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it. |
Response fields
| Field | Type | Description |
|---|---|---|
match | object | Match metadata with the final score embedded. |
tape | array of object | The score sequence — see HistoryTapeRow. Array order is the authoritative order: commit order on raw, match-progress order on ?sequence=clean (see the sequence parameter). |
tiebreaks | array or null | Per-set tiebreak final scores from OBSERVED states only, aligned to the sets of the final scoreline: {"p1", "p2"} for a 7-6 set whose observed maximum tiebreak state is a valid terminal shape (max >= 7, margin >= 2), null per set otherwise — a breaker whose closing point the feed skipped reads null rather than an under-report. Null when the match has no 7-6 set. Present on raw and clean alike. |
profiles | array of object | Model profiles produced during the match, oldest first. |
meta | object |
Example
curl https://api.livetennisapi.com/api/public/v1/history/matches/18953 \
-H "Authorization: Bearer twjp_..."
GET /history/packages
List the pre-built monthly bulk history packages (PRO)
Bulk downloads are a heavier product than single-match tape reads. Requires PRO on the live API, or the Historical Data API Pro plan and up, or a one-off package access pass. A key that can read the tape but is not package-entitled receives 403 upgrade_required.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
kind | query | string (tape, rankings, rally, archive, elo, archive_tape) | no | Package family. tape (default) = monthly point-by-point match tapes; rankings = as-of ranking records (ULTRA); rally = the charted rally corpus (shot-by-shot) as YEARLY exports (ULTRA); archive = the results archive (1968–2022) as YEARLY exports, same entitlement as the tape packages; elo = the as-of Elo tape as YEARLY exports (ULTRA); archive_tape = the RECONSTRUCTED 2013–2022 point-by-point tape as YEARLY exports — ten periods, 2013 through 2022, each with a JSONL and a CSV file, all ready, needing ULTRA or a History Pro/Business subscription or an active one-off package window (a History Starter grant reads a tape per call and does not download years of them). The yearly kinds' period is YYYY, one file per year, because a fixed historical corpus is not an accruing monthly stream. The default means a tape-only client never sees a new kind of row appear. Default tape. |
year | query | string | no | Year archive listing — every published month of the year (History Business, a 1-year package, or ULTRA). |
Responses
| Status | Meaning |
|---|---|
200 | Ready packages, newest period first |
400 | Bad query parameter |
401 | Missing, unknown, or disabled credentials |
403 | Your tier doesn't unlock this endpoint |
429 | Rate limit exceeded (Retry-After header present). Three body shapes, told apart by error and scope: the per-MINUTE limit (rate_limited, with upgrade_url, tier and price naming the next tier up); the per-DAY quota (rate_limited with scope: "day", limit_per_day, and resets_at — the absolute ISO instant the daily window resets); and abuse_throttled with retry_at_epoch — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it. |
Response fields
| Field | Type | Description |
|---|---|---|
data | array of object | |
meta | object |
Example
curl https://api.livetennisapi.com/api/public/v1/history/packages \
-H "Authorization: Bearer twjp_..."
GET /history/packages/{period}
One monthly package — manifest, or the bulk file itself (PRO)
Without format returns the package manifest (file set, counts, sha256). With format=jsonl or format=csv streams that file as an attachment. Same entitlement as /history/packages. 404 when the month has not been built yet — list available months first.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
period | path | string | yes | Calendar month, YYYY-MM — except for the yearly kinds (rally, archive, elo, archive_tape), where it is the bare year YYYY (400 bad_period otherwise). |
kind | query | string (tape, rankings, rally, archive, elo, archive_tape) | no | Package family; rankings, rally, elo and archive_tape require ULTRA, a History Pro/Business subscription, or an active one-off package window (changed 2026-08-19). rally = the yearly charted rally corpus exports; archive = the yearly results archive (1968–2022) exports, same entitlement as the tape packages; elo = the yearly as-of Elo tape exports; archive_tape = the yearly reconstructed 2013–2022 point-by-point tape exports (period 2013 through 2022 — no other year exists). Default tape. |
format | query | string (jsonl, csv, corrections) | no | Omit for the JSON manifest; set to download the file. corrections (added 2026-09-10) streams the package's correction CSV — match_id, field, before, after, tournament_key, source, corrected_at — when stored data was repaired after the package was published; the manifest's files lists it with format: corrections, and a package without one answers 404 no corrections file for that package. First use: the 2023-02 → 2024-12 tape packages carry surface corrections for 19,439 Challenger matches (repaired 2026-09-10; the packages themselves were rebuilt the same day). |
Responses
| Status | Meaning |
|---|---|
200 | The manifest (no format), or the bulk file as an attachment (format=jsonl streams NDJSON, format=csv streams CSV, format=corrections streams the correction CSV). A gzipped file (see the manifest's compression) is served as application/gzip, never with Content-Encoding: gzip — the manifest's sha256 covers the exact bytes you receive. |
400 | Bad query parameter |
401 | Missing, unknown, or disabled credentials |
403 | Your tier doesn't unlock this endpoint |
404 | No such resource, or no data yet |
429 | Rate limit exceeded (Retry-After header present). Three body shapes, told apart by error and scope: the per-MINUTE limit (rate_limited, with upgrade_url, tier and price naming the next tier up); the per-DAY quota (rate_limited with scope: "day", limit_per_day, and resets_at — the absolute ISO instant the daily window resets); and abuse_throttled with retry_at_epoch — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it. |
Response fields
| Field | Type | Description |
|---|---|---|
period | string | Calendar month, YYYY-MM — or the bare year YYYY on the yearly rally/archive/elo/archive_tape kinds. |
status | string (ready) | Only built months are listed or served. |
match_count | integer or null | |
row_count | integer or null | |
files | array of object | One entry per downloadable format. |
built_at | string or null | |
kind | string (tape, rankings, rally, archive, elo, archive_tape) | Present only on non-tape packages, so the shape a tape client already parses is unchanged. On a rankings package match_count is the number of players covered and row_count the number of ranking records; on a rally package the counts are charted matches and points; on an archive package the counts are archive results; on an elo package row_count is the number of rating records; on an archive_tape package match_count is the reconstructed tapes in that year and row_count their score rows (97,901 and 14,340,663 across the ten years). |
Example
curl https://api.livetennisapi.com/api/public/v1/history/packages/{period} \
-H "Authorization: Bearer twjp_..."