Properties
| Name | Type | Description |
|---|---|---|
| match | object | |
| tape | array | Chronological score sequence — see HistoryTapeRow |
| tiebreaks | arraynull | 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 o |
| profiles | array | Model profiles, oldest first (Analysis `profile` shape) |
| meta | object |
JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://artifacts.apis.io/livetennisapi-com/json-schema/livetennisapi-com-history-tape-schema.json",
"title": "HistoryTape",
"x-generated": "2026-10-28",
"x-method": "derived",
"x-generator": "derive-json-schema.py",
"x-source": "openapi/livetennisapi-com-openapi.yaml#/components/schemas/HistoryTape",
"type": "object",
"properties": {
"match": {
"$ref": "#/$defs/Match"
},
"tape": {
"type": "array",
"description": "Chronological score sequence — see HistoryTapeRow",
"items": {
"$ref": "#/$defs/HistoryTapeRow"
}
},
"tiebreaks": {
"type": [
"array",
"null"
],
"description": "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.",
"items": {
"type": [
"object",
"null"
],
"properties": {
"p1": {
"type": "integer"
},
"p2": {
"type": "integer"
}
}
}
},
"profiles": {
"type": "array",
"description": "Model profiles, oldest first (Analysis `profile` shape)",
"items": {
"type": "object"
}
},
"meta": {
"type": "object",
"properties": {
"match_id": {
"type": "integer"
},
"rows": {
"type": "integer",
"description": "Rows RETURNED — after any `sequence=clean` collapse."
},
"coverage": {
"$ref": "#/$defs/Coverage"
},
"point_source": {
"type": [
"string",
"null"
],
"enum": [
"observed",
"reconstructed",
"mixed",
null
],
"description": "Where the rows came from — `observed` (every row watched live), `reconstructed` (every row expanded after the fact), `mixed` (a reconstructed opening followed by what we watched), or null on an empty tape. Reported once here and never per row, so the row shape cannot carry data-source provenance."
},
"raw_rows": {
"type": "integer",
"description": "Rows BEFORE any collapse — equals `rows` when `sequence=raw`."
},
"unique_states": {
"type": "integer",
"description": "Distinct score states in the raw tape. `raw_rows` minus this is pure repetition."
},
"sequence": {
"type": "string",
"enum": [
"raw",
"clean"
],
"description": "Echoes the requested `?sequence=`."
},
"from_archive": {
"type": "boolean",
"description": "The rows were served from the immutable archive rather than the live table. Informational — the content contract is identical."
},
"points": {
"$ref": "#/$defs/PointsMeta"
},
"tiebreaks_source": {
"type": [
"array",
"null"
],
"description": "Aligned to the top-level `tiebreaks`: per set, where that set's breaker final came from — `tape` (the served tape reached a terminal breaker state), `summary` (a source's finished-match summary) or `reconstruction` (the point-by-point reconstruction) — or null where the entry is null. Null when `tiebreaks` is null. `summary` and `reconstruction` are written by the once-a-day finals pass (see `tiebreaks`), not at the moment a match ends.",
"items": {
"type": [
"string",
"null"
],
"enum": [
"tape",
"summary",
"reconstruction",
null
]
}
},
"reconstructed_at": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "When the newest RECONSTRUCTED row in this response was written (UTC) — when the reconstruction you are holding landed. Null when no reconstructed row is served. Measured on the rows returned, after any sequence=clean collapse. Added 2026-09-20."
},
"observed_span": {
"type": [
"object",
"null"
],
"description": "The `timestamp` of the first and last OBSERVED rows in this response, in served order. Null when no observed row is served. Added 2026-09-20.",
"properties": {
"first": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"last": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
},
"enrichment": {
"type": "object",
"description": "What serve/outcome tags actually landed on the rows of THIS response — the per-point stream's vocabulary. `none`/`none` on a wholly reconstructed tape. Added 2026-09-20.",
"properties": {
"serve": {
"type": "string",
"enum": [
"stated",
"none"
]
},
"outcome": {
"type": "string",
"enum": [
"full",
"ace_double_fault",
"none"
]
}
}
},
"generated_at": {
"type": "string",
"format": "date-time"
}
}
}
},
"$defs": {
"Coverage": {
"type": "string",
"enum": [
"from_start",
"partial",
"reconstructed",
"reconstructed_partial",
"none"
],
"description": "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 recorded sequence 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."
},
"HistoryTapeRow": {
"type": "object",
"description": "One row of the score sequence. Rows we watched live carry a real `timestamp`. Rows expanded after the fact from a finished-match point-by-point record carry a null `timestamp` AND null model fields, because neither a wall clock nor a model output ever existed for them — nothing is synthesised. `origin` (since 2026-09-20) says explicitly which kind each row is, read from the row's stored provenance — never inferred from its clock; a null `timestamp` remains true of every reconstructed row. The model fields alone are not a marker, since they are stamped best-effort and an observed row may lack them. `meta.point_source` / `meta.coverage` say which kind of tape you are holding. `origin`, `serve` and `outcome` are present on `GET /history/matches/{matchId}` rows; the pre-2023 ArchiveTape (every row reconstructed) does not carry them. `winner` (since 2026-09-22) is present on EVERY row of every sequence, on both — who won the point that produced the row, judged from the score step and from nothing else; `server`, `serve` and `winner` together are the three per-point facts a model needs, and the third was only implicit in the score step before. Reconstructed rows are never repaired or reordered: a game whose recorded points are internally inconsistent is withheld whole, not fitted or patched into a legal sequence — see `meta.points.games_withheld` (added 2026-09-20).",
"properties": {
"origin": {
"type": "string",
"enum": [
"observed",
"reconstructed"
],
"description": "`observed` — a row our own pipeline committed while the point was being played; `reconstructed` — a row expanded after the fact from a finished-match point-by-point record (null timestamp, null model fields). From the stored provenance, never inferred from a null timestamp. Added 2026-09-20."
},
"serve": {
"type": [
"integer",
"null"
],
"enum": [
1,
2,
null
],
"description": "The serve the point was played on (1 first, 2 second), joined from this match's per-point stream: paired by EXACT score state — set count, every set's games, in-game points and tiebreak flag all equal — and, where a state recurs (a deuce cycle), by nearest clock within 90 seconds (the tape row's observed clock against the stream point's), each stream point used at most once; otherwise null. Observed rows only: null on every reconstructed row, on a row no stream point matches, and on a recurring state with no clock within the window or two at the same distance. Never inferred from the score or the pattern of play. Added 2026-09-20; clock pairing 2026-09-21."
},
"outcome": {
"type": [
"string",
"null"
],
"enum": [
"ace",
"double_fault",
"winner",
"forced_error",
"unforced_error",
null
],
"description": "How the point ended, as stated for the point — same join rule and same nulls as `serve`. `meta.enrichment` says what landed on this tape. Added 2026-09-20."
},
"sets": {
"type": "array",
"items": {
"type": "integer"
}
},
"games": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
}
}
},
"points": {
"type": "array",
"description": "In-game points as tennis strings (\"0\", \"15\", \"30\", \"40\", \"AD\") — EXCEPT during a tiebreak, where they are the running tiebreak count as plain integer strings (\"0\", \"1\", \"2\", …); `is_tiebreak` tells the two notations apart.",
"items": {
"type": "string"
}
},
"server": {
"type": [
"integer",
"null"
],
"enum": [
1,
2,
null
],
"description": "Who is serving — null where it was never stated. On `?sequence=clean` the server is SETTLED per non-tiebreak game (the server is physically constant within a game, so the game's majority assertion fills nulls and corrects transient disagreements; a tie settles nothing). The raw sequence carries every row exactly as committed, so a transient wrong-server read can appear there and be corrected by a later row. 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's own assertion carried through. Two consequences, both reported by a Basic customer on 2026-09-25 and confirmed in the code. The rotation cannot be reconstructed from the rows served wherever the tape is sampled — a breaker whose rows skip points gives you the server of a state, not of a numbered point, and `meta.points.transitions_legal` against `transitions_total` tells you whether that happened. And on the set roll-up row that carries `tiebreak_final`, `server` is the value of that committed set-end state: it is NOT a preserved copy of the final breaker point's server, and we make no claim that it is. Withhold server-dependent tiebreak analysis unless the encoding reconciles on the match in front of you."
},
"is_tiebreak": {
"type": "boolean"
},
"tiebreak_final": {
"type": [
"array",
"null"
],
"description": "Added 2026-09-24. The breaker's closing score in our player order, e.g. `[2, 7]`, on the row that CLOSES a tiebreak set. Absent on every other row, including the breaker rows themselves, which stay exactly as they were observed. Same value, from the same resolver, as this set's entry in the response's `tiebreaks` array, so the two never disagree. The row that closes a set carries the decided games before the set counter catches up, and since 2026-09-24 the games on the row decide it, so the field does not depend on which row the counter moves on. Before that a set whose counter lagged by a row was never stamped and stayed empty on every later read, while `tiebreaks` stated the final.",
"items": {
"type": "integer"
}
},
"win_probability_p1": {
"type": [
"number",
"null"
],
"description": "The model read stamped on this row while the match was live. The tape carries NO model-regime stamp — there is no `model_version` or `generated_at` column here, nor in the bulk `tape` packages — so segregate a recorded tape corpus by this row's `timestamp` against the two regime boundaries documented on `Score.win_probability_meta`: 2026-08-18T08:29:28Z and 2026-09-16T07:59:54Z."
},
"danger": {
"type": [
"number",
"null"
]
},
"timestamp": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"accepted_at": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "On tape rows since 2026-09-24. The instant our arbiter accepted this state (UTC), stamped once at insert and never refreshed by a re-assertion. This is the clock to difference a timing study against: `timestamp` starts equal to it and is then refreshed while the owning source keeps re-asserting the unchanged state, so on most raw rows `timestamp` is a later re-assertion instant (measured on match 189679: 164 of its 171 observed rows carry a `timestamp` more than a second after `accepted_at`). Null on every reconstructed row, which never had a clock. IT ALSO HAS A START DATE ON THE ARCHIVED TAPE, and this description claimed the opposite (\"it rides the whole observed corpus\") until 2026-10-01. THAT DATE MOVED BACK SIX WEEKS ON 2026-10-01. A tape answered from the archive (`meta.from_archive: true`) now carries the clock on every observed row from 2026-08-11: 100.0% of every day from 2026-08-11 to 2026-10-01, 92.5% of 2026-08-10 (the live column's own first instant is `2026-08-10T08:07:42Z`), and 0.0% of every day before that. Over the whole archived observed corpus, 1,721,102 of 4,663,181 rows carry it (36.9%, against 307,631 before the backfill): 0.0% of every month to July 2026, 68.8% of August, 100.0% of September and of October. The boundary was 2026-09-23, the day the archive column landed, until a backfill took each missing value from the live row that archived row had been copied from: 1,413,471 clocks and 1,393,458 counters, inside 1,538,176 rows over 14,491 matches. Nothing is derived, rounded or taken from a neighbouring row, and a row whose live row was already trimmed at the 90-day retention stays null for good. Wire-verified on match 2168 (26 April 2026), before the boundary: 131 observed rows, `timestamp` on all 131 and `accepted_at` on none. Match 189679's own archived copy carried the clock on 0 of its 171 observed rows before that run and on 171 of 171 after it, which is why the 189679 measurement above holds either way: that match is served with `from_archive: false`. So read `meta.from_archive` before planning a timing study, and treat a null here as a boundary of the record rather than as a missing value."
},
"received_at": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "On tape rows since 2026-09-24. The instant this state reached our edge, before the arbiter ran, stamped once. `accepted_at` minus `received_at` is our own arbitration step; neither figure is a claim about how fast anything before our edge is, and neither is the court's clock. `timestamp` is the only one of the three with no start date: this clock begins 2026-09-24 and `accepted_at` begins 2026-08-11 on an archived tape (this description said the other two had no start date until 2026-10-01, which was wrong about `accepted_at` — see `HistoryTapeRow.accepted_at`). This is also the one clock the 2026-10-01 archive backfill could not move: the 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. It is stamped only as a state arrives, so it can never be filled in afterwards: the earliest one we hold is `2026-09-24T02:00:26Z`, and on every observed row accepted before that instant it is null while `timestamp` is populated. Measured on match 189679 (12 September) after a Basic customer reported it on 2026-09-25: 171 observed rows, `accepted_at` on all 171, `received_at` on none — a match served from its live rows, so it says nothing about the archived tape. Null on every reconstructed row."
},
"sequence": {
"type": [
"integer",
"null"
],
"description": "On tape rows since 2026-09-26. This state's per-match ACCEPT COUNTER — the same number `GET /matches/{matchId}/withdrawals` publishes as `sequence` and `replaced_by_sequence`, and the same counter as `Score.sequence` on a live read. It is what makes the withdrawal ledger's pointer resolvable: told \"sequence 104 was withdrawn, resume from 103\", you can now find 103 on the tape, and because a withdrawn state never enters the record at all the withdrawal shows up here as a GAP in the run of counters. Before this the tape published no such number, so the pointer named a row nothing identified — reported by an Ultra customer who wrote that \"the history rows lack sequence IDs, so these responses alone cannot reconstruct exactly what a live client received\". NULL, NEVER FABRICATED, IN TWO CASES, and both are honest gaps rather than errors: a row whose archived copy could not be filled from its own live row, and the pre-2023 archive tape, which has no such counter because no live accept ever happened. The 2026-10-01 backfill moved the first of those from 2026-09-26 back to 2026-08-14, so the archived counter is on 68.2% of 2026-08-14, 99.3% of 08-15, 95.9% or more of every day from 2026-08-16 and 100% from 2026-09-01; before that the counter died with the live row at the 90-day retention. Unlike the two clocks it is NOT null on a reconstructed row: a counter is a fact about our own accept order, so wherever one is present a live reader really did receive that state at that position."
},
"winner": {
"type": [
"integer",
"null"
],
"enum": [
1,
2,
null
],
"description": "Who won the point that produced this row — `1` | `2`, judged from the score step between this row and the previous served row (the previous raw row on the raw sequence, the previous clean row under `?sequence=clean`), 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 (an identical state — null on raw, collapsed away on clean), a set opener (same games totals, the arrays extended by `[0,0]`), 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 the strict expander withheld whole; `meta.points.games_withheld` says when the latter happened. Present on every row of every sequence, observed and reconstructed, and on the pre-2023 ArchiveTape; equal to `point_winner` wherever that older key is present. Derived once per response, never stored. Added 2026-09-22."
},
"point_winner": {
"type": [
"integer",
"null"
],
"enum": [
1,
2,
null
],
"description": "The same verdict as `winner` under the older key, kept unchanged for existing readers. Who won the point this row records — PRESENT ONLY on `?sequence=clean` and `?points=complete` rows, and only where the transition from the previous row of the served sequence is a single attributable point; null on gaps, torn rows and the first row. Never on the plain raw sequence (raw is deliberately non-monotonic: consecutive raw rows are corrections, not points — a `points=complete` read is the exception, because there the served order IS point order). Derived at read time, never stored or guessed. Since 2026-09-22 `winner` carries the same verdict on every row of every sequence."
}
}
},
"Match": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"tournament": {
"type": "string"
},
"tour": {
"type": [
"string",
"null"
],
"enum": [
"atp",
"wta",
"challenger",
"itf",
"juniors",
null
],
"description": "The tour, in the SAME vocabulary the `tour` query filter accepts — both are derived from one registry, so a match selected by `?tour=X` always carries that value in `tour`. Null when no tour is stated for the match or the event type has no public tour name (exhibitions, team and mixed events) — never guessed. Safe to group and filter on; never parse the tournament name for this."
},
"tournament_id": {
"type": [
"string",
"null"
],
"description": "Stable tournament identity — one id per tournament × event type, stable across seasons. Joins `GET /tournaments/{tournamentId}`. Null when the tournament is not in our tournament list at all (UTR events), and on the small residue of matches whose tournament name could not be matched to exactly one edition in that list — never guessed. Measured over August 2026 terminal matches: ATP 1 of 531 and WTA 3 of 513 null; ITF about 2.5% after the 2026-09-04 backfill (was ~25%); UTR 100%. (Edge case — a match mislabelled as qualifying by the feed has its id re-pointed to the main-draw tournament when detected, so the id can change once, early, in that direction only.)"
},
"tier": {
"type": [
"string",
"null"
],
"enum": [
"grand_slam",
"atp_finals",
"atp_1000",
"atp_500",
"atp_250",
"next_gen_finals",
"united_cup",
"davis_cup",
"olympics",
"laver_cup",
"wta_finals",
"wta_elite_trophy",
"wta_1000",
"wta_500",
"wta_250",
"wta_125",
"bjk_cup",
"challenger_175",
"challenger_125",
"challenger_100",
"challenger_75",
"challenger_50",
"itf_m15",
"itf_m25",
"itf_w15",
"itf_w25",
"itf_w35",
"itf_w40",
"itf_w50",
"itf_w60",
"itf_w75",
"itf_w80",
"itf_w100",
"juniors",
"utr",
"exhibition",
null
],
"description": "The level the tournament was played at IN THIS MATCH'S SEASON (added 2026-09-22) — the official category as the tour publishes it, in a closed vocabulary: `grand_slam`; the ATP levels (`atp_finals`, `atp_1000`, `atp_500`, `atp_250`, `next_gen_finals`); the WTA levels (`wta_finals`, `wta_elite_trophy` — last held 2023 — `wta_1000`, `wta_500`, `wta_250`, `wta_125`); the ATP Challenger levels (`challenger_175` … `challenger_50`); the ITF World Tennis Tour categories (`itf_m15`, `itf_m25`, `itf_w15` … `itf_w100` — the 2023 women's categories `itf_w25`/`itf_w40`/`itf_w60`/`itf_w80` are kept as printed that season, not mapped onto the 2024 ladder, because the level a match was played at is a fact about that season); the team events (`united_cup`, `davis_cup` and `bjk_cup` in every group and stage, `laver_cup`, `olympics`); `juniors`; `utr`; and `exhibition` (no ranking points — Hopman Cup class). Same vocabulary as the `?tier=` filter, read from the same per-season table, so filter and field cannot disagree. Resolved by the MATCH'S season (the calendar year of `scheduled_time`), never copied from the tournament row: a tournament keeps one `tournament_id` across seasons while its level moves — Dallas was `atp_250` in 2023 and 2024 and `atp_500` from 2025, so a 2024 Dallas match reads `atp_250` and a 2025 one `atp_500` under the same id. Coverage of the 2023–2026 seasons was verified event by event against the official calendars (99.1% of tournament-seasons resolved; the rest are deliberately null — the Asian Games tennis event, which no tour lists). A new event may read null for a short while after its first match until its tier is resolved. Null when nothing we hold names the level — never guessed; null is \"we do not know\", never a level. `category` on `/tournaments` is unchanged and remains the coarse class (`challenger`, `itf`, `atp_250` …); read this field for the level."
},
"tier_source": {
"type": [
"string",
"null"
],
"enum": [
"calendar",
"name",
"wikipedia",
"resolver",
null
],
"description": "How `tier` was established (added 2026-09-22): `calendar` = the official per-season tour calendar named the level (ATP tour and Challenger calendars, the WTA tournament list); `name` = an unambiguous name rule (the ITF category is in the event's official name — `M25 …`, `W35 …` — team and UTR events are named as such, and the Slam junior draws and exhibitions are their own event type); `wikipedia` = a published season schedule, used only where the official calendar could not state the level for that season (Eastbourne 2023–2025); `resolver` = resolved after the seed dataset, by the same rules, from the event's name, tour and dates in that season. Null exactly when `tier` is null."
},
"surface": {
"type": [
"string",
"null"
],
"enum": [
"hard",
"clay",
"grass",
null
],
"description": "Court surface. Null where no source states one, never inferred from a tournament's usual surface in other seasons. TEAM COMPETITIONS (Davis Cup, Billie Jean King Cup, Laver Cup) have no surface at tournament level — each tie is played on the surface its host nation chose — so the match carries its TIE's surface, taken from the ITF's published tie record (venue, indoor/outdoor, surface) and the same for every rubber of that tie; `tournament.surface` stays null for them. A team-event match is null only when the tie's venue is not yet published or the match cannot be matched to a single tie (competition, date window and the nations on court must agree). The other structural null is UTR events, which are not in our tournament list at all and so carry `tournament_id: null` too. For every other event a surface that IS known is published on both the match and its `tournament`, and the two agree."
},
"indoor": {
"type": "boolean"
},
"format": {
"type": [
"string",
"null"
],
"enum": [
"BO3",
"BO5",
null
]
},
"round": {
"type": [
"string",
"null"
],
"description": "The free-text round label, verbatim, as published for the match. It is the round WITHIN the draw this match belongs to, and the draw is named by `is_qualifying` — NOT by this string. A qualifying draw has its own semi-final and its own final, so `\"Buenos Aires 3 - Semi-finals\"` with `is_qualifying: true` is the semi-final of the QUALIFYING draw, not of the tournament. Read the two fields together or not at all. The label also embeds the tournament name on most matches — parse the suffix after the last `\" - \"`, or better, use `round_code`."
},
"round_code": {
"type": [
"string",
"null"
],
"enum": [
"F",
"SF",
"QF",
"R16",
"R32",
"R64",
"R128",
"RR",
"BR",
"Q",
"Q1",
"Q2",
"Q3",
"Q4",
"ER",
null
],
"description": "The round in the archive's controlled vocabulary, normalized from the free-text label above (`Q` = qualifying round the label does not number). It matches `/history/archive/matches?round=` exactly. Null when the label is unrecognised — never guessed.\nIT IS DRAW-RELATIVE AND CANNOT, BY ITSELF, TELL YOU WHETHER A MATCH IS QUALIFYING. A qualifying semi-final carries `SF`, the same as a main-draw semi-final; only `Q`/`Q1`..`Q4` appear when the label itself names the round as qualifying, which most labels do not. To branch on main draw vs qualifying, use `round_code` AND `is_qualifying` together."
},
"is_qualifying": {
"type": [
"boolean",
"null"
],
"description": "Whether this match belongs to the QUALIFYING draw, as the source states it — never GUESSED from main-draw round vocabulary or from the tournament name. (A round label that itself reads `Qualification Round 1` states it, and reads `true`: a `false` beside such a round is a payload contradicting itself, not a main-draw assertion. `Semi-finals` and `Fi
# --- truncated at 32 KB (75 KB total) ---
# Full source: https://artifacts.apis.io/livetennisapi-com/json-schema/livetennisapi-com-history-tape-schema.json
Work with this as data
Every JSON Schema here is available over the APIs.io API and to AI agents over MCP.
MCP server
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
Tools for schemas
4 MCP tools reach this
find_json_schemasBrowse and filter every JSON Schema in the catalog.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.
Call it yourself
curl for this page
This JSON Schema
curl "https://apis.io/api/v1/json-schemas/livetennisapi-com-history-tape"
All schemas
curl "https://apis.io/api/v1/json-schemas?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
Get an API key
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.