Live Tennis API · Schema

Score

One state at one instant. ULTRA adds win_probability_p1 + danger. Order and de-duplicate on `sequence`, never on `timestamp`.

CompanySportsDataReal-timeAPITennis

Properties

Name Type Description
sets array
games arraynull [games_p1, games_p2]; each a per-set list. NULL means WITHHELD — we hold no state for this match we can stand behind (see `detail`); it never means 0-0, and an empty array is never used to mean "unkno
points array
server integernull
is_tiebreak boolean The current game is a tiebreak. On a DECIDING set played as a 10-point match tiebreak this is true for the whole set, whose `games` slot holds the breaker.
win_probability_p1 numbernull
danger numbernull
win_probability_p1_model numbernull ULTRA, since 2026-09-12. The same model read computed WITHOUT the market-prior anchor — a probability no market price touched. Equals win_probability_p1 on a match the anchor does not apply to, and al
win_probability_meta objectnull ULTRA, since 2026-09-12. Our own model-regime identifier (`model_version`), the UTC instant the two probabilities were generated (`generated_at`), whether the anchor applies to this MATCH (`market_anc
timestamp stringnull Our clock, not the court's: stamped in our process when the state was accepted (the same step that assigns `sequence`), then refreshed — at most every 8 s — while the owning source keeps re-asserting
accepted_at stringnull Since 2026-09-15. The instant we accepted this state (UTC): stamped once at insert, never refreshed by a re-assertion — the clock to difference a latency study against. Present on live score reads and
sequence integernull Per-match accept counter for this state: 1, 2, 3, … in the order we accepted states for THIS match, gapless within a match — higher is later, equal is the same state re-served, and a gap between two r
age_seconds integernull Seconds since the score CONTENT last changed (not since we last polled) — it keeps rising between points. Null = change-clock unknown for this row, never "fresh". Always null on the archived final tha
stale boolean Our own staleness verdict on age_seconds for a LIVE match (threshold currently 120s). Always false on a completed match: its last state is final, not stale.
observed_age_seconds integernull Seconds since ANY of our sources last CONFIRMED this state — the insert itself, the owning source re-asserting it, or an independent source agreeing with it. The "is anyone still watching" clock, next
sources_count integernull How many DISTINCT sources inserted or independently confirmed this match's state in the last 60 seconds (our public source window). A count, never a name; it counts sources we accepted a state from or
changing_sources_count integernull Since 2026-09-24. How many DISTINCT sources CHANGED this match's score in the last 300 seconds — sources that inserted a new state, counted on the content clock alone. A source re-asserting an unchang
corroborated booleannull Since 2026-09-24. Has an INDEPENDENT source vouched for THIS EXACT state? True when a source other than the one that wrote this state has since presented us the same state; false when none has, includ
corroborated_age_seconds integernull Since 2026-09-24. Seconds since that independent vouch. Null when corroborated is false (nobody has) or null (unknown). Compare it with age_seconds: a vouch OLDER than the state means somebody agreed
detail string Present only when something is withheld — plain-language reason, e.g. a null `games` array.
verdict objectnull Why this read is not the newest state we judged (since 2026-09-23). Null on an ordinary read. Otherwise the legality gate refused a state a stream had already published, and this says which and why. `
View JSON Schema on GitHub

JSON Schema

livetennisapi-com-score-schema.json Raw ↑
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/livetennisapi-com/json-schema/livetennisapi-com-score-schema.json",
  "title": "Score",
  "description": "One state at one instant. ULTRA adds win_probability_p1 + danger. Order and de-duplicate on `sequence`, never on `timestamp`.",
  "x-generated": "2026-10-28",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/livetennisapi-com-openapi.yaml#/components/schemas/Score",
  "type": "object",
  "properties": {
    "sets": {
      "type": "array",
      "items": {
        "type": "integer"
      }
    },
    "games": {
      "type": [
        "array",
        "null"
      ],
      "description": "[games_p1, games_p2]; each a per-set list. NULL means WITHHELD — we hold no state for this match we can stand behind (see `detail`); it never means 0-0, and an empty array is never used to mean \"unknown\". A deciding set played as a 10-point MATCH TIEBREAK (super-tiebreak) carries the BREAKER's score in its slot WHILE IT IS BEING PLAYED — `[[6,4,10],[4,6,5]]` is `6-4 3-6 [10-5]`, not a 10-game set — and `is_tiebreak` is true then. Once the breaker is over that set is often published as a one-game `1-0`: take the match winner from `sets`, never from the deciding set's games.",
      "items": {
        "type": "array",
        "items": {
          "type": "integer"
        }
      }
    },
    "points": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "server": {
      "type": [
        "integer",
        "null"
      ],
      "enum": [
        1,
        2,
        null
      ]
    },
    "is_tiebreak": {
      "type": "boolean",
      "description": "The current game is a tiebreak. On a DECIDING set played as a 10-point match tiebreak this is true for the whole set, whose `games` slot holds the breaker."
    },
    "win_probability_p1": {
      "type": [
        "number",
        "null"
      ]
    },
    "danger": {
      "type": [
        "number",
        "null"
      ]
    },
    "win_probability_p1_model": {
      "type": [
        "number",
        "null"
      ],
      "description": "ULTRA, since 2026-09-12. The same model read computed WITHOUT the market-prior anchor — a probability no market price touched. Equals win_probability_p1 on a match the anchor does not apply to, and also on some rows of an anchored match (the anchor shifts the pre-match hold rates, which enter the engine snapped to a 0.01 grid, so a shift smaller than a grid step often solves to an identical value); the per-row test is win_probability_p1 != win_probability_p1_model. Null on rows stamped before it existed (never back-filled)."
    },
    "win_probability_meta": {
      "type": [
        "object",
        "null"
      ],
      "description": "ULTRA, since 2026-09-12. Our own model-regime identifier (`model_version`), the UTC instant the two probabilities were generated (`generated_at`), whether the anchor applies to this MATCH (`market_anchored` — per match, not per row) and, since 2026-09-15, whether it moved THIS ROW's win_probability_p1 away from the twin (`anchor_effective`). Null on rows stamped before it existed. SEGREGATING A RECORDED CORPUS — cut on the CLOCK, not on the version string. Two changes moved published live probabilities before `model_version` reflected them, so split a corpus on `generated_at` (or the row's `timestamp`) at these two instants: 2026-08-18T08:29:28Z, when the over-inclusive deciding-set rule for lower-tier singles began (some final sets priced as a race-to-ten breaker), and 2026-09-16T07:59:54Z, when the draw-based rule replaced it (main-draw singles priced as a full set — a move of up to 0.20 on affected deciding-set states). `model_version` is NOT a safe discriminator across the second boundary: the `+itfdraw-2026-09-16` suffix first appears on rows generated 2026-09-17T05:04:25Z, 21 h 04 m AFTER the rule itself landed, and in that gap 33,510 states across 351 matches (9,719 states over 113 matches at the ITF M15/W15/W35 levels the rule governs) were computed under the CORRECTED rule while still carrying `markov-population-2026-08-23`. Cutting on the version string — or on a whole-day boundary — mislabels exactly those rows; cutting at 2026-09-16T07:59:54Z does not. Measured against production 2026-09-19. CORRECTION (2026-09-19): both instants were first published, the same day, THREE HOURS LATE — 2026-08-18T11:29:28Z and 2026-09-16T11:02:00Z — because the switch times were read off a column stored in local wall time and labelled UTC. The instants above are the corrected UTC ones. If you cut a corpus on either of the old values, re-cut: everything in the three hours before them is on the wrong side. Earlier than that there is no stamp to cut on at all: no row generated before 2026-09-12T12:08:17Z carries `model_version` or `generated_at`, and the bulk tape packages have no version column, so for those the row `timestamp` is the only discriminator. The `model_version` on an analysis profile (`ModelProfile`) is a different identifier for a different object — the forecast's prompt scaffold — and does not apply to score rows.",
      "properties": {
        "model_version": {
          "type": "string",
          "description": "The model regime that produced this row: engine family, calibration key scheme and — since 2026-09-17 — every pricing rule in force, as a `+…` suffix. It is not a safe cut for a recorded corpus across the 2026-09-16 boundary; see this object's description for the two instants to segregate on."
        },
        "generated_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "market_anchored": {
          "type": "boolean",
          "description": "The market-prior anchor applies to this match: a two-sided pre-match price and a pre-play first score gave the model a shift to its pre-match prior. Per match, not per row."
        },
        "anchor_effective": {
          "type": [
            "boolean",
            "null"
          ],
          "description": "Since 2026-09-15. This row's win_probability_p1 differs from win_probability_p1_model, i.e. the anchor moved this value. Null when either probability is null."
        }
      }
    },
    "timestamp": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "Our clock, not the court's: stamped in our process when the state was accepted (the same step that assigns `sequence`), then refreshed — at most every 8 s — while the owning source keeps re-asserting the unchanged state, so on a live read it is usually the last-assertion instant, not the accept instant. Never an observation time from outside our process. NOT an ordering key — use `sequence`; for the accept instant use `accepted_at`."
    },
    "accepted_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "Since 2026-09-15. The instant we accepted this state (UTC): stamped once at insert, never refreshed by a re-assertion — the clock to difference a latency study against. Present on live score reads and push frames, and since 2026-09-24 on history-tape rows too (this description said \"absent on history-tape rows\" until 2026-09-25, which had been wrong since the tape started carrying it — see `HistoryTapeRow.accepted_at`). Null on the archive fallback of a retention-trimmed completed match."
    },
    "sequence": {
      "type": [
        "integer",
        "null"
      ],
      "description": "Per-match accept counter for this state: 1, 2, 3, … in the order we accepted states for THIS match, gapless within a match — higher is later, equal is the same state re-served, and a gap between two reads is exactly that many missed updates. On /matches/{matchId}/score it moves BACKWARDS in two cases only: we withdrew a state we cannot stand behind and re-served the last good one, or the read deferred to a strictly higher-trust source's fresh state (about 1% of live reads, measured 2026-09-15). On the push feed and the native WebSocket it only ever rises. Null on states that predate the counter — never fabricated. Present on live score reads AND, since 2026-09-26, on history-tape rows too — see `HistoryTapeRow.sequence`. This description said \"absent on history-tape rows, which are already served in order\" until then, and the reasoning was the gap: being served in order does not let you resolve the `replaced_by_sequence` that `GET /matches/{matchId}/withdrawals` hands you, which is a pointer AT a counter and not a position in a list. Since 2026-09-24 a set completion that does not end the match is assigned a sequence only once confirmed — the same source keeps presenting the completed set for 10 s or more, a source at least as trusted presents it, or the same source shows play continuing inside the new set. A live scoring display can render the set-ending frame ON set point, before the point is decided, and step back when it is not won; the unconfirmed frame is now never stored, so no sequence ever carries it and nothing is published as provisional. Until a frame is confirmed every surface keeps serving the set-point state; one nobody confirms within 120 s is discarded. A set completion that ENDS the match is exempt — it follows the completion rules instead. Measured over the 30 days to 2026-09-24 this adds a median 10 s between the point and the published set-end, and on a feed that does not re-present states the set-end lands with the first point of the next set (90th percentile 133 s). It does not cover a set genuinely won and then corrected on court, which can still be published and rewound."
    },
    "age_seconds": {
      "type": [
        "integer",
        "null"
      ],
      "description": "Seconds since the score CONTENT last changed (not since we last polled) — it keeps rising between points. Null = change-clock unknown for this row, never \"fresh\". Always null on the archived final that `/matches/{matchId}/score` serves for a settled match whose live rows were retired (since 2026-09-20): no clock is claimed for a state nobody watched."
    },
    "stale": {
      "type": "boolean",
      "description": "Our own staleness verdict on age_seconds for a LIVE match (threshold currently 120s). Always false on a completed match: its last state is final, not stale."
    },
    "observed_age_seconds": {
      "type": [
        "integer",
        "null"
      ],
      "description": "Seconds since ANY of our sources last CONFIRMED this state — the insert itself, the owning source re-asserting it, or an independent source agreeing with it. The \"is anyone still watching\" clock, next to `age_seconds`, the \"has anything happened\" clock: a long age_seconds with a small observed_age_seconds is a changeover or set break, not a stall. Null = unknown: no observation clock stored on the row, or the archive fallback on a retention-trimmed completed match (where age_seconds and sources_count are null too). Absent on history-tape rows."
    },
    "sources_count": {
      "type": [
        "integer",
        "null"
      ],
      "description": "How many DISTINCT sources inserted or independently confirmed this match's state in the last 60 seconds (our public source window). A count, never a name; it counts sources we accepted a state from or that agreed with the stored state, so it can under-count and never over-counts. 1 is ordinary; 0 on a live match past a full minute means nobody we accepted a state from, or who agreed with the stored state, has reported it in the last 60 seconds; 0 on a completed match is normal. Null = unknown (a read blip, or the archive fallback). Present wherever observed_age_seconds is."
    },
    "changing_sources_count": {
      "type": [
        "integer",
        "null"
      ],
      "description": "Since 2026-09-24. How many DISTINCT sources CHANGED this match's score in the last 300 seconds — sources that inserted a new state, counted on the content clock alone. A source re-asserting an unchanged state is not counted, which is the difference from sources_count: a source stuck re-sending its last state keeps sources_count at 1 for as long as the echo lasts and keeps this at 0. The window is wider than sources_count's on purpose, because a point lands every 30-60 s and a changeover is about 90 s. A cross-confirmation is agreement, not a change, so this can read below sources_count and never above it. 0 on a live match past five minutes means nobody moved the score in that time; 0 on a completed match is normal. Null = unknown (a read blip, or the archive fallback). A count, never a name. Present wherever sources_count is."
    },
    "corroborated": {
      "type": [
        "boolean",
        "null"
      ],
      "description": "Since 2026-09-24. Has an INDEPENDENT source vouched for THIS EXACT state? True when a source other than the one that wrote this state has since presented us the same state; false when none has, including a brand-new state nobody has had a chance to agree with yet. This is the field to gate a SET-END on. The two counts above are window measures over the match and say nothing about the state in front of you, so a set completion only one source has announced can sit beside a perfectly healthy sources_count — and a set-end is the one state whose correction changes a result rather than a number. EVIDENCE, NOT PROOF: a vouched state can still be corrected, so read corroborated_age_seconds beside it and treat a vouch that is younger than the state it vouches for as the weaker signal it is. When we do take a state back the `sequence` goes down and `verdict` says so. Null = unknown (the archive fallback on a retention-trimmed completed match, where age_seconds and sources_count are null too). A yes/no, never a name. Present wherever sources_count is."
    },
    "corroborated_age_seconds": {
      "type": [
        "integer",
        "null"
      ],
      "description": "Since 2026-09-24. Seconds since that independent vouch. Null when corroborated is false (nobody has) or null (unknown). Compare it with age_seconds: a vouch OLDER than the state means somebody agreed with this state after it was published; a vouch younger than the state is one that landed late."
    },
    "detail": {
      "type": "string",
      "description": "Present only when something is withheld — plain-language reason, e.g. a null `games` array."
    },
    "verdict": {
      "type": [
        "object",
        "null"
      ],
      "description": "Why this read is not the newest state we judged (since 2026-09-23). Null on an ordinary read. Otherwise the legality gate refused a state a stream had already published, and this says which and why. `superseded_sequence` is the `sequence` of the refused state, the value a stream consumer holds. `reason` is the gate's slug. `kind` is `withdrawn` (discard the refused state and resume from the served one), `deferred` (it may be early and is pending the next accept) or `withheld` (nothing publishable, `games` is null). `safe_to_resume` is true exactly when `kind` is `withdrawn`. The push feed, the WebSocket and webhooks announce the same judgement as a `score_withdrawn` frame naming the same sequence, so a stream consumer never has to poll to learn a state was taken back. That frame carries `type`, `match_id`, `kind`, `superseded_sequence`, `reason`, `safe_to_resume`, `ts` and `published_at`, and no replacement score: resume from the next `score` frame or from a fresh read. Since 2026-09-25 this field also answers for a sequence you name with `?sequence=N`, from the durable record served by `GET /matches/{matchId}/withdrawals` — which is how a consumer that missed the frame recovers it.",
      "properties": {
        "kind": {
          "type": "string",
          "enum": [
            "withdrawn",
            "deferred",
            "withheld"
          ]
        },
        "superseded_sequence": {
          "type": [
            "integer",
            "null"
          ]
        },
        "reason": {
          "type": "string"
        },
        "safe_to_resume": {
          "type": "boolean"
        }
      }
    }
  }
}

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-score"
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.