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). A FREE key may spend its 20 free history calls per calendar month here; past that the answer is 403 upgrade_required carrying free_history_taste: "used". 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. ?tier= (added 2026-09-22) selects by the level the tournament was played at in the match's season, and ?restated_since= (added 2026-09-22) keeps only the matches whose published result was corrected after an instant — the poll to run after each settlement pass.
Request
- Base URL: https://api.livetennisapi.com/api/public/v1
- URL: https://api.livetennisapi.com/api/public/v1/history/matches
- Auth: one of:
- HTTP bearer
- API key in header X-API-Key
Query parameters
Return only matches that DO (true) or do NOT (false) have a mapped match-winner market, matching the has_market flag each row carries (added 2026-09-15). Omit for both. Anything other than true/false is a 400 bad_has_market, never a silently ignored filter. These are the matches /history/matches/{id}/prices and /matches/{id}/prices can serve; every other id answers 404 no_market. Measured 2026-09-15: 3,718 of the 9,741 matches completed since 2026-08-16 (38%) had a price tape.
Return only matches in the QUALIFYING draw (true) or only those the source states are not (false), matching the is_qualifying flag each row carries (field added 2026-08-12, filter added 2026-10-02). Omit for both. Anything other than true/false is a 400 bad_is_qualifying, never a silently ignored filter. THIS IS THE ONLY WAY TO SPLIT THE TWO DRAWS, and the reason is in the is_qualifying field description: the two are routinely carried under ONE tournament name with ONE round vocabulary, so tournament, tournament_id, round and round_code all read main-draw for a qualifying match. Do not filter on the round text. THREE-VALUED, so this filter is too: a row whose flag is null (no source has ever stated it) matches NEITHER value and is returned only when the filter is omitted. null is an answer, not a false — returning it under is_qualifying=false would assert "main draw" on the strength of nothing. Measured 2026-10-02 over completed matches: the flag is stated on 98.9% of the last 30 days and 98.3% of 2026 outside the UTR circuit, where it is mostly unstated (1,695 of the 1,831 unstated rows in the last 30 days are UTR).
Return only men's or only women's matches, matching the gender field each row carries (added 2026-09-09, documented here 2026-10-02). Omit for both. Anything other than men/women is a 400 bad_gender, never a silently ignored filter. It exists because ?tour= deliberately collapses: challenger is ATP Challenger AND WTA 125, and itf and juniors are both sexes — so without this there is no way to ask for men's Challenger at all. A row whose gender is null (mixed doubles, a team tie) matches NEITHER value, the same contract as ?draw= and ?is_qualifying=. bad_points_complete, bad_combination, points_read_disabled, bad_after_seq, bad_has_market, no_market, points_disabled,
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.
Latest play date, same formats as from (a bare date includes everything played that day). from after to is a 400.
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 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.
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.
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.
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.
Tier filter (added 2026-09-22): comma-separated exact values from the closed tier vocabulary (?tier=wta_125, ?tier=itf_w35,itf_w50,itf_w75, ?tier=challenger_75,challenger_100). Selects matches whose tier field carries one of the values — the level the tournament was played at in the MATCH'S season, read from the same per-season table, so filter and field cannot disagree. A match whose tier is null matches no value. Composes with every other filter (?tour=challenger&gender=women&tier=wta_125 and ?tier=wta_125 select the same rows; ?tier=challenger_125&draw=doubles is a slice tour= alone cannot make). Available on /matches (every status) and /history/matches. /tournaments takes ?tier= as well, reading the CURRENT season rather than a match's, and states that difference on its own parameter. Unknown value → 400 bad_tier with the offending values in bad and the full vocabulary in allowed.
How a tape came to exist, and specifically whether we WATCHED it. from_start — we watched this match live from 0-0; every row was committed by our own pipeline while the point was being played, so every row carries a real timestamp. It is a statement about how the rows were obtained, NOT a synonym for "complete". partial — we watched it, but recording began after play had started and no reconstruction is available to repair the opening. reconstructed — the tape opens with rows expanded after the fact from a finished-match point-by-point record: either the whole tape (we never watched the match) or its unwatched opening followed by what we did watch. True about the score, silent about the clock. reconstructed_partial — as reconstructed, AND the reconstruction is known not to cover the whole match. That happens two ways: it OPENS AFTER 0-0 (the upstream record begins mid-match), or it STOPS SHORT of the official final score (the record truncated, usually by the deciding game). Every row is still a point somebody recorded — nothing is synthesised to close either gap — but the match is NOT complete and must not be backtested as if it were. none — no rows; the match was on the calendar and was neither watched nor reconstructable. This vocabulary describes the 2023+ tape. The 2013–2022 archive tape (ArchiveTape) reuses it restricted to reconstructed and reconstructed_partial, and derives the second DIFFERENTLY: there a per-GAME tape carries that label however complete it is — see ArchiveTape.meta.coverage before reading it the same way.
Keep only matches whose tape has this coverage. An unknown value is a 400 bad_coverage listing the accepted values in allowed.
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=.
Keep only matches whose published result was CORRECTED after this instant (added 2026-09-22) — a restatement row on their status history later than it. The poll to run after recording results: anything it returns has a result_restated_at past your cursor and a result_version above the one you hold, and its status history says what changed. An ISO-8601 instant with a Z or an offset (a naive value is read as UTC); a bare date is a 400 bad_restated_since — a day is not an instant, exactly as for changed_since. Composes with every other filter here. A match whose correction was a reopen is live again and reappears here once re-completed, with a further restatement row. Rows with this basis exist from 2026-09-22; earlier corrections were not signalled and nothing is reconstructed for them.
Response
Completed matches (winner = 1|2|null, from final sets), each with its tape coverage
Example response
{
"data": [
{
"score": {
"win_probability_meta": {
"model_version": "markov-population-2026-08-23"
}
}
}
]
}Changes
No recorded changes to this operation across all 1 revision of this API.