List matches by lifecycle status (FREE)
status=live and status=upcoming are the FREE current-state picture. status=completed pages historical results and is part of the paid History product — it requires BASIC (the same rule as /history/matches). 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". The player, country, from/to, tour, draw, has_analysis and has_market filters are optional, AND-composed, applied inside the query (before pagination), and work on every status — omitting them returns exactly what the endpoint returned before they existed. has_analysis and has_market are the two availability flags every row already carries: filter the slate with them and call /matches/{matchId}/analysis and /matches/{matchId}/prices only for the ids that have something, rather than probing per match for a 404.
Request
- Base URL: https://api.livetennisapi.com/api/public/v1
- URL: https://api.livetennisapi.com/api/public/v1/matches
- Auth: one of:
- HTTP bearer
- API key in header X-API-Key
Query parameters
live (default) and upcoming are the FREE current-state picture. completed and cancelled are terminal LISTINGS, part of the history product (BASIC, or any History plan on a free key; a FREE key's 20 free history calls each month are served here too). cancelled covers feed-cancelled, walkover-with-no-stated-winner and postponed-never-played matches; a walkover that named its winner is completed. cancelled pages with limit/offset (optionally from/to) and does NOT accept updated_since (400 bad_request). Any other value is a 400 bad_status carrying the accepted list in allowed.
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.
Return only matches that DO (true) or do NOT (false) have model analysis, matching the has_analysis flag each row carries (filter added 2026-09-14, documented here 2026-09-20). Omit for both. Anything other than true/false is a 400 bad_has_analysis, never a silently ignored filter. These are the matches /matches/{matchId}/analysis can serve; every other id answers 404 no_analysis. The misses are structural rather than transient — analysis is produced only for a match carrying a mapped market — so a caller polling the unanalysed half is asking a question whose answer can never become yes. Filter the slate with this instead.
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.
CHANGE FEED. A UTC date (YYYY-MM-DD) or ISO-8601 datetime. Returns the rows whose updated_at is at or after it, OLDEST FIRST, with meta.next_cursor and meta.watermark. Poll from meta.watermark, not from your own clock — it trails the server by 5s on purpose, because a row committed while your request was being served can carry a timestamp below your now and still not be in your result. Overlap ~60s and dedupe by id; the feed is at-least-once by design. CANNOT be combined with from/to (400 bad_updated_since): those filter when a match was PLAYED and sort by play time, this filters when its row CHANGED, and a change cursor cannot resume a play-time ordering. Not accepted with status=cancelled (400 bad_request). Anything unparseable is a 400, never accepted and ignored. A physically DELETED row can never appear in the feed; cancellations, walkovers and withdrawals are status CHANGES and do appear.
ULTRA. A DIFFERENT LISTING, not a filter on the match page: the match ids with a WITHDRAWN score state at or after this instant, newest first, one row per match as { match_id, withdrawals, last_withdrawn_at } with an exact meta.total and limit / offset paging. A UTC date (YYYY-MM-DD) or an ISO-8601 UTC datetime. This is how a stream consumer that was disconnected finds which matches to ask GET /matches/{matchId}/withdrawals about. It takes no other parameter beside limit and offset — status, updated_since, the date range and every match filter are 400 bad_withdrawn_since, as is an unparseable instant. Records are kept 90 days from the withdrawal.
Echo back meta.next_cursor to fetch the next page of a change feed. Opaque — do not parse it, its contents will change. Supplies both the floor and the tie-break position, so it replaces updated_since on follow-up pages rather than accompanying it.
Response
Matches with latest score
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.