The endpoints
Each one below lists its plan tier, every parameter, the response shape and an example. They are the same entries as in the full reference, which carries all 48 operations of the API on one page.
GET /history/matches/{matchId}/prices
Per-point price history — the match-winner quote in force at every played point (PRO)
One row per played point of a match with a mapped market, ascending by seq (added 2026-09-15). Each row carries the score state the BASIC tape publishes and, per neutral side ("1" = players.p1's outcome, "2" = p2's), the newest quote observed at or before the point's capture time — or, when none precedes it, the first quote after it within five minutes (negative lag). A side with no usable quote is null, never invented. resolution is an honest label of the join: tick (|lag| <= 15 s), minute (<= 90 s), coarse (older); matches played from 2026-09-15 align against the full-resolution in-play tape, earlier ones against the thinned tape and read minute/coarse. The ULTRA-only per-point fields (server, winner, serve, outcome) are not on this surface. Works on a live match too. 404 no_market when the match has no mapped market — enumerate with GET /history/matches?has_market=true.
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.
|
after_seq | query | integer | no | Resume cursor — return points with seq greater than this (meta.last_seq of the previous page). Anything but a non-negative integer is a 400 bad_after_seq. Default 0. |
limit | query | integer | no | Default 100. |
Responses
| Status | Meaning |
|---|---|
200 | Per-point price rows, ascending by seq |
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 |
|---|---|---|
data | array of object | |
meta | object |
Example
curl https://api.livetennisapi.com/api/public/v1/history/matches/18953/prices \
-H "Authorization: Bearer twjp_..."
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). 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.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
has_market | query | boolean | no | 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. |
is_qualifying | query | boolean | no | 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). |
gender | query | string (men, women) | no | 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, |
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. |
tier | query | string | no | 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. |
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=. |
restated_since | query | string | no | 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. |
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). 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 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. winner (since 2026-09-22) rides every row of EITHER sequence with the same verdict: on the raw sequence it is judged against the previous raw row, so a re-send reads null there and collapses away here. 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 (and winner, the same verdict) 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 | |
tape | array of object | Chronological score sequence — see HistoryTapeRow |
tiebreaks | array or null | Per-set tiebreak final scores, aligned to the sets of the final scoreline: {"p1", "p2"} for each 7-6 set, null per set otherwise; null when the match has no 7-6 set. Each entry comes from the tape's own terminal breaker state (max >= 7, margin >= 2 — always wins), a source's finished-match summary, or the point-by-point reconstruction — named per set in meta.tiebreaks_source. Nothing is inferred from a partial state: a breaker whose closing point no source states reads null rather than an under-report. Before 2026-09-16 only the tape kind existed and the array was null on ~99% of 7-6 sets. TIMING. Only the tape kind is available the instant a match ends. summary and reconstruction entries are written by a finals pass that runs ONCE A DAY, so a match that finished earlier the same day commonly reads null for its 7-6 sets and carries them from the next pass onward. Measured 2026-09-17: of 7-6 sets in matches completed on the five previous days, 72-100% were populated; of those completed the same day, 8%. Re-read a fresh match the following day before treating a null as final. |
profiles | array of object | Model profiles, oldest first (Analysis profile shape) |
meta | object |
Example
curl https://api.livetennisapi.com/api/public/v1/history/matches/18953 \
-H "Authorization: Bearer twjp_..."
GET /history/incidents
Published data-quality incidents (BASIC)
Every data-quality incident we have published, oldest window first. This is the register to reconcile against when a published value changed after you read it: each record states the window it applies to, the rule before and after, what the defect was, which surfaces it touched, how to tell an affected row from a clean one, and whether corrections and a per-match list are available. Nothing here is inferred at read time. The register is part of a release, so a record appears only when we have decided what it says, and it does not change between releases.
Responses
| Status | Meaning |
|---|---|
200 | The incident register. |
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/incidents \
-H "Authorization: Bearer twjp_..."
GET /history/incidents/{incidentId}/matches
The matches one incident affected, as JSONL or CSV (BASIC)
One row per affected match, streamed as JSONL by default or CSV with ?format=csv. Columns are incident_id, match_id, tournament, tour, round, draw_stage, scheduled_time, affected_field, served and expected. served and expected carry that incident's own vocabulary: for the deciding-set rule match_tiebreak against full_set, for the price side side_reversed against side_players, and for a field repair the before and after values. Rows are derived from stored data by the same code every consumer of that field uses. A match whose derivation would be a guess is left out, and the incident's matches.note says which those are, so this export is exact rather than complete.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
incidentId | path | string | yes | |
format | query | string (jsonl, csv) | no | Default jsonl. |
Responses
| Status | Meaning |
|---|---|
200 | One row per affected match, as a file attachment. |
400 | bad_format — format was neither jsonl nor csv. |
401 | Missing, unknown, or disabled credentials |
403 | Your tier doesn't unlock this endpoint |
404 | not_found for an unknown incident id. not_derivable for an incident with no exact per-match derivation, carrying that incident's window_utc, affected_surfaces and the note saying why. |
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. |
Example
curl https://api.livetennisapi.com/api/public/v1/history/incidents/{incidentId}/matches \
-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 | |
schema_version | integer or null (1, 2, null) | Which shape this month's files carry (added 2026-09-22), so you can tell without opening them. 2 = a package row is the API tape row (origin, serve, outcome, winner, and the full meta), and the CSV carries the trailing columns. 1 = the shape every month built before 22 September 2026 carries: tape rows hold the score columns only (no origin, serve, outcome or winner; no meta.enrichment, reconstructed_at or observed_span), and the CSV ends at danger. Months built before 22 September 2026 keep this shape UNTIL THEY ARE REBUILT; the rebuild runs newest month first, then backwards, and a month's schema_version flips to 2 (and its built_at and sha256 values move) when its turn comes. Check the manifest rather than assuming a month is one or the other: a consumer that needs the enriched fields should require schema_version >= 2 and, for a month still at 1, read the per-match endpoint for the matches it needs. null = not versioned: the non-tape kinds (rankings, elo, …). |
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_..."