Live Tennis API · Schema

ArchiveTape

The reconstructed 2013-2022 point-by-point tape for one archive result. Same envelope as HistoryTape so one parser reads both halves of the tape product, with the differences that are TRUE: `match` is the archive result row (winner/loser-shaped), `profiles` is always empty, and every row carries a null `timestamp`, a null `age_seconds` and null model fields because nobody watched these matches and no model scored them. `meta` has no `match_id` — an archive id is not a match id.

CompanySportsDataReal-timeAPITennis

Properties

Name Type Description
match object
tape array The score sequence in seq order, WINNER-FIRST (`sets[0]` / `games[0]` / `points[0]` are the winner's, and a `server` of 1 means the winner was serving) — see HistoryTapeRow. `point_winner` and `winner
tiebreaks arraynull Per-set tiebreak final scores, same rule and shape as on HistoryTape. Usually PRESENT even on a per-game tape — the pre-2023 record expands points inside tiebreaks even where it does not elsewhere.
profiles array Always empty. The key is kept so the envelope matches HistoryTape; no model analysis exists, or could exist, for a pre-2023 match.
meta object
View JSON Schema on GitHub

JSON Schema

livetennisapi-com-archive-tape-schema.json Raw ↑
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/livetennisapi-com/json-schema/livetennisapi-com-archive-tape-schema.json",
  "title": "ArchiveTape",
  "description": "The reconstructed 2013-2022 point-by-point tape for one archive result. Same envelope as HistoryTape so one parser reads both halves of the tape product, with the differences that are TRUE: `match` is the archive result row (winner/loser-shaped), `profiles` is always empty, and every row carries a null `timestamp`, a null `age_seconds` and null model fields because nobody watched these matches and no model scored them. `meta` has no `match_id` — an archive id is not a match id.",
  "x-generated": "2026-10-28",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/livetennisapi-com-openapi.yaml#/components/schemas/ArchiveTape",
  "type": "object",
  "properties": {
    "match": {
      "$ref": "#/$defs/ArchiveMatch"
    },
    "tape": {
      "type": "array",
      "description": "The score sequence in seq order, WINNER-FIRST (`sets[0]` / `games[0]` / `points[0]` are the winner's, and a `server` of 1 means the winner was serving) — see HistoryTapeRow. `point_winner` and `winner` (since 2026-09-22) are present; on a per-GAME tape, where consecutive rows differ by a whole game, each reads the side whose game count rose (the game point was theirs) and says nothing about the other points of that game.",
      "items": {
        "$ref": "#/$defs/HistoryTapeRow"
      }
    },
    "tiebreaks": {
      "type": [
        "array",
        "null"
      ],
      "description": "Per-set tiebreak final scores, same rule and shape as on HistoryTape. Usually PRESENT even on a per-game tape — the pre-2023 record expands points inside tiebreaks even where it does not elsewhere.",
      "items": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "p1": {
            "type": "integer"
          },
          "p2": {
            "type": "integer"
          }
        }
      }
    },
    "profiles": {
      "type": "array",
      "description": "Always empty. The key is kept so the envelope matches HistoryTape; no model analysis exists, or could exist, for a pre-2023 match.",
      "items": {
        "type": "object"
      }
    },
    "meta": {
      "type": "object",
      "properties": {
        "archive_match_id": {
          "type": "integer",
          "description": "The `archive_matches.id` this tape belongs to. Named in full because it is NOT a match id — passing it to a `/matches/...` route resolves a different, real record without erroring."
        },
        "tiebreaks_source": {
          "type": [
            "array",
            "null"
          ],
          "description": "Aligned to `tiebreaks`, same contract as on HistoryTape. Every populated entry here reads `reconstruction` — the archive tape IS the reconstruction and nothing else is consulted.",
          "items": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "rows": {
          "type": "integer",
          "description": "Rows returned. Read next to `granularity` before comparing it with a 2023+ tape's row count. Across the corpus a tape averages 146 rows (median 138, range 4-570); a per-game tape carries one row per game and is far shorter than a point tape of the same match."
        },
        "coverage": {
          "type": "string",
          "enum": [
            "reconstructed",
            "reconstructed_partial"
          ],
          "description": "`reconstructed` (96.3% of the corpus) = the tape reproduces the published final score exactly, and that score is a match that finished. `reconstructed_partial` (3.7%) HAS TWO CAUSES and the label does not say which: either the match itself stopped early (a retirement or abandonment prefix), OR the tape is per-GAME — every one of the 556 game-granular tapes carries this label however complete it is, because the coverage vocabulary is closed and `game` has no word of its own. It therefore does NOT on its own mean incomplete match coverage: read `granularity` and the match's own score/outcome for that. Never `from_start` or `partial`: both assert we watched the match, and nobody watched these. On the 2023+ tape the same word means only short-at-either-end."
        },
        "granularity": {
          "type": "string",
          "enum": [
            "point",
            "game"
          ],
          "description": "What one row resolves to. `point` = the record advances within a game — 99.4% of the corpus (97,293 matches). `game` = one row per game, with points expanded only inside tiebreaks — 556 matches, 555 of them in 2013 and one in 2014; every season from 2015 on is 100% point-granular. `point_winner` / `winner` on a per-game tape read the side whose game count rose on each row, and nothing about the game's other points. Orthogonal to `coverage` as an axis, though a `game` tape is always labelled `reconstructed_partial`."
        },
        "point_source": {
          "type": "string",
          "enum": [
            "reconstructed"
          ],
          "description": "Always `reconstructed`. It is a property of the dataset, not a measurement of these rows, and there is no other value it can take."
        },
        "model_rows": {
          "type": "integer",
          "description": "Always 0 — no model output exists for a pre-2023 match. Present so a client parsing both halves of the tape product reads one shape."
        },
        "observed_rows": {
          "type": "integer",
          "description": "Always 0 — no row here was watched live."
        },
        "point_source_detail": {
          "type": "string",
          "description": "Always `reconstruction from public record`, the fixed label for reconstructed point sequences with no capture timestamps. Stated in the payload so the label travels with the data rather than living only in a customer's memory of these docs."
        },
        "sequence": {
          "type": "string",
          "enum": [
            "raw"
          ],
          "description": "Always `raw`. There is nothing to collapse — a reconstructed tape has one source and no corrections, so `?sequence=clean` has no meaning here; the parameter is ignored, not refused."
        },
        "generated_at": {
          "type": "string",
          "format": "date-time"
        }
      }
    }
  },
  "$defs": {
    "ArchiveMatch": {
      "type": "object",
      "description": "One result of the results archive (1968–2022). Winner/loser-shaped — results data is recorded that way at the source, so the winner is a field, never an inference. Its own id space; `source_id` is the stable corpus key. A result played 2013–2022 may additionally carry a reconstructed point-by-point tape (`ArchiveTape`); results from 1968–2012 have none and never will.",
      "properties": {
        "id": {
          "type": "integer"
        },
        "source_id": {
          "type": "string"
        },
        "tour": {
          "type": "string",
          "enum": [
            "atp",
            "wta"
          ]
        },
        "level": {
          "type": [
            "string",
            "null"
          ],
          "description": "Source tier code (G/M/A/F/D/C/O, or a futures category code as published)."
        },
        "tournament": {
          "type": [
            "string",
            "null"
          ]
        },
        "surface": {
          "type": [
            "string",
            "null"
          ]
        },
        "draw_size": {
          "type": [
            "integer",
            "null"
          ]
        },
        "event_date": {
          "type": [
            "string",
            "null"
          ],
          "format": "date",
          "description": "Tournament START date — per-match dates do not exist in this era's records, and we do not invent them."
        },
        "round": {
          "type": [
            "string",
            "null"
          ]
        },
        "best_of": {
          "type": [
            "integer",
            "null"
          ]
        },
        "minutes": {
          "type": [
            "integer",
            "null"
          ]
        },
        "winner": {
          "$ref": "#/$defs/ArchivePlayer"
        },
        "loser": {
          "$ref": "#/$defs/ArchivePlayer"
        },
        "score": {
          "type": [
            "string",
            "null"
          ],
          "description": "The final score as published, e.g. \"6-4 7-6(5)\", \"6-3 RET\", \"W/O\"."
        },
        "outcome": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "completed",
            "retired",
            "walkover",
            "default",
            "abandoned",
            null
          ],
          "description": "Parsed from the score's own vocabulary; null when unparseable — never guessed."
        },
        "stats": {
          "type": [
            "object",
            "null"
          ],
          "description": "Detail endpoint only. {\"winner\":{...}, \"loser\":{...}} with aces, double_faults, serve_points, first_in, first_won, second_won, serve_games, bp_saved, bp_faced where the source recorded them; null otherwise (most rows before 1991) — never synthesised."
        }
      }
    },
    "ArchivePlayer": {
      "type": "object",
      "description": "One participant of an archive result, as the corpus records them.",
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "hand": {
          "type": [
            "string",
            "null"
          ]
        },
        "country": {
          "type": [
            "string",
            "null"
          ],
          "description": "3-letter code, same vocabulary as `player.country`."
        },
        "rank": {
          "type": [
            "integer",
            "null"
          ],
          "description": "The player's rank AT THE TIME of the match, as published."
        },
        "seed": {
          "type": [
            "integer",
            "null"
          ]
        },
        "player_id": {
          "type": [
            "integer",
            "null"
          ],
          "description": "The corpus person id — joins `/history/archive/players` within the same tour. NOT a roster player id."
        },
        "height_cm": {
          "type": [
            "integer",
            "null"
          ]
        },
        "age": {
          "type": [
            "number",
            "null"
          ],
          "description": "Age at the time of the match, as the corpus records it."
        },
        "entry": {
          "type": [
            "string",
            "null"
          ],
          "description": "Draw entry where recorded (WC, Q, LL, PR, SE, ...) — null for direct acceptances."
        }
      }
    },
    "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."
        }
      }
    }
  }
}

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.
All 92 tools →

Call it yourself

curl for this page
This JSON Schema
curl "https://apis.io/api/v1/json-schemas/livetennisapi-com-archive-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.