Current score only — lowest-latency REST read (FREE)

This is a POINT-IN-TIME SNAPSHOT: the single current state, overwritten on every score commit. It carries no history and no accumulated statistics. For the SEQUENCE of states — who served each game, hold/break, every score state in forward order — use /history/matches/{matchId}?sequence=clean, which works on a LIVE match, not only a completed one. For in-play statistics use /matches/{matchId}/statistics (ULTRA); they are deliberately not on this object, because they can be further behind the match than the score and must carry their own as_of. ARCHIVED FINALS (since 2026-09-20). The live-score rows behind this read are retired by a 90-day retention sweep, and a match recovered from an official day list may never have had one. When there is no live row at all, a SETTLED match — outcome non-null on the match object: completed, retired, walkover, default, abandoned, unresolved — is answered from its archived final: the same read GET /matches?status=completed already embeds, through the same serializer, so the listing and this endpoint can never disagree about whether a score exists. A live tape always outranks the archive — the fallback is reached only when no publishable live row exists, so a live match reads exactly what it did before. An archived final carries age_seconds: null, observed_age_seconds: null, sources_count: null and accepted_at: null — no clock is claimed for a state nobody watched — and timestamp is null where the archived row has none (a reconstructed final). The Score object carries no data-source label; whether that final was observed or reconstructed is what GET /history/matches/{matchId} reports once, in meta.point_source. 404 is kept for: an upcoming match with no row (nothing to serve yet), a cancelled match that was never played (nothing settled), and a settled match with nothing recorded anywhere — the fallback serves a final that exists, it never invents one. WITHDRAWN STATES (since 2026-09-23). When the legality gate behind this read refuses a state a stream has already published, the verdict object on the score says so: kind, superseded_sequence, reason and safe_to_resume. It is null on an ordinary read. The push feed, the WebSocket and webhooks carry the same judgement as a score_withdrawn frame naming the same sequence, so a stream consumer learns a state was taken back without polling. GET /history/incidents is the published register of data-quality incidents. THE RECOVERY PATH (since 2026-09-25). A consumer that missed the frame cannot get the live verdict back, because it is recomputed over the newest state and returns null as soon as the next state is accepted. Every withdrawal is now recorded durably when the judgement is made: pass ?sequence=N here to have verdict answer for the sequence you hold, and read GET /matches/{matchId}/withdrawals for the whole record on a match, including the frame exactly as the feed sent it. Before 2026-09-20 every settled match older than the retention window answered 404 here (13,437 completed matches from the previous 180 days, 157,170 all time, measured at the change) while the completed listing served its score.

get/matches/{matchId}/score

Request

  • Base URL: https://api.livetennisapi.com/api/public/v1
  • URL: https://api.livetennisapi.com/api/public/v1/matches/{matchId}/score
  • Auth: one of:
    • HTTP bearer
    • API key in header X-API-Key

Path parameters

matchIdinteger required

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.

Query parameters

sequenceinteger

The sequence YOU HOLD for this match, from the push feed, the WebSocket or an earlier read. verdict is recomputed on every read over the newest state, so on its own it says nothing about a sequence that was withdrawn before the next state landed. With ?sequence=N, and only when the read carries no verdict of its own, verdict answers for N from the durable withdrawal record: kind: withdrawn with the recorded reason when we discarded that state, null when we never did. Only a discarded record is re-asserted this way; a deferred state may since have been corroborated and a withheld read described that moment's scan, so neither is. A verdict the read produces itself always wins, because it describes the state being served. Every other field still describes the served state. A non-integer is 400 bad_sequence, never accepted and ignored.

Response

Current score (ULTRA adds win_probability_p1 + danger). On a settled match with no live row, the archived final (since 2026-09-20) — age_seconds, observed_age_seconds, sources_count and accepted_at null.

setsinteger[]
pointsstring[]
server1 | 2 | null nullable
is_tiebreakboolean

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_p1number nullable
dangernumber nullable
win_probability_p1_modelnumber nullable

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).

timestampstring date-time nullable

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 upstream observation time, never a source's own clock. NOT an ordering key — use sequence; for the accept instant use accepted_at.

accepted_atstring date-time nullable

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.

sequenceinteger nullable

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_secondsinteger nullable

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.

staleboolean

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_secondsinteger nullable

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_countinteger nullable

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_countinteger nullable

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 feed 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.

corroboratedboolean nullable

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_secondsinteger nullable

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.

detailstring

Present only when something is withheld — plain-language reason, e.g. a null games array.

Example response

{
  "win_probability_meta": {
    "model_version": "markov-population-2026-08-23"
  }
}

Changes

No recorded changes to this operation across all 1 revision of this API.