---
title: "In-play statistics — aces, double faults, serve split, hold/break %, break points, service & return points (ULTRA)"
method: GET
path: "/matches/{matchId}/statistics"
---

# In-play statistics — aces, double faults, serve split, hold/break %, break points, service & return points (ULTRA)

`GET /matches/{matchId}/statistics`

## Request

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

In-play statistics for one match, in TWO families that are deliberately not merged.

DERIVED (the top level of `players.pN`) are rebuilt from the point-by-point record: service and return games played and won, hold and break percentage, break points faced, saved and converted, service and return points.

MEASURED (`players.pN.measured`) are counted upstream, so they include what no point record can yield — ACES AND DOUBLE FAULTS, the first- and second-serve split, winners and unforced errors. Both families name some of the same quantities, computed two entirely different ways; that is a cross-check, not a duplication to collapse.

Measured coverage is not uniform and every measured field is optional — an absent field is OMITTED, never zero-filled, so read the keys you are given. Aces and double faults are present across every tour. The serve split and break points saved are present on the main tours and absent on ITF singles. Winners and unforced errors historically appeared on a minority of main-tour matches and have not been delivered upstream since 2026-07-12 (measured 2026-08-17).

`freshness.derived` and `freshness.measured` each carry their own `coverage` (`live` | `final` | `stale` | `none` | `diverged`; `final` = the closing figures of a completed match — a finished match cannot be "stale", so its `age_seconds` is null), `as_of`, `age_seconds` and `describes` — the match state the numbers describe. On `diverged` the measured VALUES are withheld and `freshness.measured_divergence` says why; the top-level `coverage` only summarises the response. `none` on both returns 200 with null players, not 404 — the match exists and holding nothing for it is the honest answer.

THE TWO AGES USE DIFFERENT CLOCKS AND MUST NOT BE COMPARED. The derived age is measured against the newest SCORE row, because between points there is no new score either and wall-clock age would report staleness that does not exist. The measured age is wall clock, because those are fetched on a fixed cadence.

Tiebreak games are excluded from the DERIVED family and counted separately; the live record collapses a whole tiebreak onto one entry, so most of its points are lost.

## Path parameters

- `matchId` integer, required

## Response `200`

Statistics with their own coverage and as_of

- MatchStatistics
  - `match_id` integer
  - `coverage` 'live' | 'final' | 'stale' | 'none' | 'diverged'
  - `as_of` string, nullable: When the underlying record was last updated (UTC)
  - `age_seconds` integer, nullable: Behind the newest SCORE row, not the wall clock
  - `games_counted` integer
  - `tiebreak_games_excluded` integer: Tiebreaks are excluded — the live record collapses a whole tiebreak onto one entry
  - `inconsistent_games_excluded` integer: Games whose recorded outcome is neither a legal hold nor a legal break
  - `sets_covered` integer[]
  - `freshness` MatchStatisticsFreshness: Per-family coverage and age. Branch on this rather than on the top-level `coverage`, which only summarises the response. The two ages use DIFFERENT clocks and must not be compared: `derived.age_seconds` is relative to the newest score row (between points there is no new score either, so wall-clock age would report staleness that does not exist), while `measured.age_seconds` is wall clock, because those are fetched on a fixed cadence.
    - `measured_divergence` object, nullable: Null when the families agree. Otherwise why the measured values were withheld, with both match states.
      - `reason` string
      - `games_in_statistics` integer
      - `games_in_score` integer
      - `delta_games` integer: positive = statistics ahead of the score, which staleness cannot cause
      - `detail` string
    - `derived` MatchStatisticsFamily
      - `coverage` 'live' | 'final' | 'stale' | 'none' | 'diverged': `final` = the closing figures of a completed match. A finished match cannot be "stale", so on `final` the family's `age_seconds` is null; `as_of` still says when the last point/fetch was recorded.
      - `as_of` string, nullable
      - `age_seconds` integer, nullable
      - `describes` object, nullable: The match state these statistics describe, per upstream. `age_seconds` says when we fetched; this says WHAT we fetched. Null when unavailable.
        - `games_p1` integer[]
        - `games_p2` integer[]
        - `total_games` integer
    - `measured` MatchStatisticsFamily
      - `coverage` 'live' | 'final' | 'stale' | 'none' | 'diverged': `final` = the closing figures of a completed match. A finished match cannot be "stale", so on `final` the family's `age_seconds` is null; `as_of` still says when the last point/fetch was recorded.
      - `as_of` string, nullable
      - `age_seconds` integer, nullable
      - `describes` object, nullable: The match state these statistics describe, per upstream. `age_seconds` says when we fetched; this says WHAT we fetched. Null when unavailable.
        - `games_p1` integer[]
        - `games_p2` integer[]
        - `total_games` integer
  - `detail` string: Present only when coverage is none
  - `players` object, nullable
    - `p1` MatchStatisticsSide: One player's in-play statistics, in TWO families that are deliberately not merged. The fields at this level are DERIVED from the point-by-point record. `measured` holds counts taken upstream, including the ones no point record can yield: aces, double faults, the serve split, winners and unforced errors. Both families name some of the same quantities, computed two entirely different ways — that is a cross-check, not a duplication to be collapsed.
      - `measured` MatchStatisticsMeasured: Measured counting statistics for one player. These are COUNTED upstream, not derived from the point record — which is why they can include aces and double faults, and the derived fields above cannot. They are MATCH TOTALS only — one whole-match rollup per player; there are no per-set measured statistics. EVERY FIELD IS OPTIONAL AND AN ABSENT FIELD IS OMITTED, never zero-filled — absent means NOT MEASURED, and a present 0 is a real measured zero. Do not assume a fixed shape; read the keys you are given. Measured over 24,552 stored payloads (2026-07-31), coverage falls into three tiers: TIER 1 — present on 98.6-100% of payloads, on every tour including ITF: aces, double faults, points won, service and return points won, break points won, service games won, longest point and game runs, return points. TIER 2 — present on essentially all ATP, WTA and Challenger matches and on NO ITF singles match: the first and second serve split, break points saved, tiebreaks won, games won, service games played. TIER 3 — the winners / unforced-errors / forced-errors family, in total and by shot type. Historically delivered on ~43% of ATP singles, ~24% of WTA singles and ~47% of tour doubles, and on NONE of Challenger, ITF or juniors — those zeros are hard, not sampling noise. The upstream feed has NOT DELIVERED this group at all since 2026-07-12 (measured 2026-08-17: 0 of 4,513 August payloads carry it, live ATP Masters included), so expect it absent on current matches until the upstream feed resumes it. A `_of` suffix is the denominator of its base field and a `_pct` suffix is the percentage, recomputed from the two counts rather than read from the upstream rounding.
        - `aces` integer, nullable
        - `break_points_won` integer, nullable
        - `double_faults` integer, nullable
        - `first_return_points_won` integer, nullable
        - `first_return_points_won_of` integer, nullable
        - `first_return_points_won_pct` integer, nullable
        - `max_games_in_row` integer, nullable
        - `max_points_in_row` integer, nullable
        - `points_won` integer, nullable
        - `return_points_won` integer, nullable
        - `second_return_points_won` integer, nullable
        - `second_return_points_won_of` integer, nullable
        - `second_return_points_won_pct` integer, nullable
        - `service_games_won` integer, nullable
        - `service_points_won` integer, nullable
        - `break_points_saved` integer, nullable
        - `break_points_saved_of` integer, nullable
        - `break_points_saved_pct` integer, nullable
        - `first_serve_points_won` integer, nullable
        - `first_serve_points_won_of` integer, nullable
        - `first_serve_points_won_pct` integer, nullable
        - `first_serves_in` integer, nullable
        - `first_serves_in_of` integer, nullable
        - `first_serves_in_pct` integer, nullable
        - `games_won` integer, nullable
        - `second_serve_points_won` integer, nullable
        - `second_serve_points_won_of` integer, nullable
        - `second_serve_points_won_pct` integer, nullable
        - `second_serves_in` integer, nullable
        - `second_serves_in_of` integer, nullable
        - `second_serves_in_pct` integer, nullable
        - `service_games_played` integer, nullable
        - `tiebreaks_won` integer, nullable
        - `backhand_errors` integer, nullable: Forced errors off the backhand.
        - `backhand_unforced_errors` integer, nullable: Unforced errors off the backhand.
        - `backhand_winners` integer, nullable: Winners off the backhand.
        - `drop_shot_unforced_errors` integer, nullable: Unforced errors on the drop shot.
        - `drop_shot_winners` integer, nullable: Winners on the drop shot.
        - `errors_total` integer, nullable: The total of FORCED errors, not of all errors. Verified over 3,766 stored payload sides (June-July 2026): it equals the forehand + backhand + return + overhead error sum in 96.2% of sides, it is SMALLER than `unforced_errors_total` in 11.7% of sides (impossible for a superset), and per-match points accounting — points won = own winners + opponent forced errors + opponent unforced errors — closes with median residual 0 over 367 matches only under the forced reading. Total errors = `errors_total` + `unforced_errors_total`; there is no separate forced-errors field to wait for, because these fields ARE the forced counts.
        - `forehand_errors` integer, nullable: Forced errors off the forehand.
        - `forehand_unforced_errors` integer, nullable: Unforced errors off the forehand.
        - `forehand_winners` integer, nullable: Winners off the forehand.
        - `groundstroke_errors` integer, nullable: `forehand_errors` + `backhand_errors` — a rollup of the two, not an additive extra.
        - `groundstroke_unforced_errors` integer, nullable: Unforced errors off the ground strokes.
        - `groundstroke_winners` integer, nullable: Winners off the ground strokes.
        - `lob_unforced_errors` integer, nullable: Unforced errors on the lob.
        - `lob_winners` integer, nullable: Winners on the lob.
        - `overhead_errors` integer, nullable: Forced errors on the overhead — the upstream's overhead-stroke count.
        - `overhead_winners` integer, nullable: Winners on the overhead.
        - `return_errors` integer, nullable: Forced errors on the return.
        - `return_winners` integer, nullable: Winners on the return.
        - `unforced_errors_total` integer, nullable: The total of unforced errors. Total errors = `errors_total` + `unforced_errors_total`.
        - `volley_unforced_errors` integer, nullable: Unforced errors on the volley.
        - `volley_winners` integer, nullable: Winners on the volley.
        - `winners_total` integer, nullable: Total winners.
      - `service_games_played` integer
      - `service_games_won` integer
      - `hold_pct` integer, nullable: null when no service game was played — never 0, so a present 0 is a real measured zero
      - `return_games_played` integer
      - `return_games_won` integer
      - `break_pct` integer, nullable
      - `break_points_faced` integer
      - `break_points_saved` integer
      - `break_points_saved_pct` integer, nullable
      - `break_points_played` integer
      - `break_points_converted` integer
      - `break_points_converted_pct` integer, nullable
      - `service_points_played` integer
      - `service_points_won` integer
      - `service_points_won_pct` integer, nullable
      - `return_points_played` integer
      - `return_points_won` integer
      - `return_points_won_pct` integer, nullable
      - `points_played` integer
      - `points_won` integer
    - `p2` MatchStatisticsSide: One player's in-play statistics, in TWO families that are deliberately not merged. The fields at this level are DERIVED from the point-by-point record. `measured` holds counts taken upstream, including the ones no point record can yield: aces, double faults, the serve split, winners and unforced errors. Both families name some of the same quantities, computed two entirely different ways — that is a cross-check, not a duplication to be collapsed.
      - `measured` MatchStatisticsMeasured: Measured counting statistics for one player. These are COUNTED upstream, not derived from the point record — which is why they can include aces and double faults, and the derived fields above cannot. They are MATCH TOTALS only — one whole-match rollup per player; there are no per-set measured statistics. EVERY FIELD IS OPTIONAL AND AN ABSENT FIELD IS OMITTED, never zero-filled — absent means NOT MEASURED, and a present 0 is a real measured zero. Do not assume a fixed shape; read the keys you are given. Measured over 24,552 stored payloads (2026-07-31), coverage falls into three tiers: TIER 1 — present on 98.6-100% of payloads, on every tour including ITF: aces, double faults, points won, service and return points won, break points won, service games won, longest point and game runs, return points. TIER 2 — present on essentially all ATP, WTA and Challenger matches and on NO ITF singles match: the first and second serve split, break points saved, tiebreaks won, games won, service games played. TIER 3 — the winners / unforced-errors / forced-errors family, in total and by shot type. Historically delivered on ~43% of ATP singles, ~24% of WTA singles and ~47% of tour doubles, and on NONE of Challenger, ITF or juniors — those zeros are hard, not sampling noise. The upstream feed has NOT DELIVERED this group at all since 2026-07-12 (measured 2026-08-17: 0 of 4,513 August payloads carry it, live ATP Masters included), so expect it absent on current matches until the upstream feed resumes it. A `_of` suffix is the denominator of its base field and a `_pct` suffix is the percentage, recomputed from the two counts rather than read from the upstream rounding.
        - `aces` integer, nullable
        - `break_points_won` integer, nullable
        - `double_faults` integer, nullable
        - `first_return_points_won` integer, nullable
        - `first_return_points_won_of` integer, nullable
        - `first_return_points_won_pct` integer, nullable
        - `max_games_in_row` integer, nullable
        - `max_points_in_row` integer, nullable
        - `points_won` integer, nullable
        - `return_points_won` integer, nullable
        - `second_return_points_won` integer, nullable
        - `second_return_points_won_of` integer, nullable
        - `second_return_points_won_pct` integer, nullable
        - `service_games_won` integer, nullable
        - `service_points_won` integer, nullable
        - `break_points_saved` integer, nullable
        - `break_points_saved_of` integer, nullable
        - `break_points_saved_pct` integer, nullable
        - `first_serve_points_won` integer, nullable
        - `first_serve_points_won_of` integer, nullable
        - `first_serve_points_won_pct` integer, nullable
        - `first_serves_in` integer, nullable
        - `first_serves_in_of` integer, nullable
        - `first_serves_in_pct` integer, nullable
        - `games_won` integer, nullable
        - `second_serve_points_won` integer, nullable
        - `second_serve_points_won_of` integer, nullable
        - `second_serve_points_won_pct` integer, nullable
        - `second_serves_in` integer, nullable
        - `second_serves_in_of` integer, nullable
        - `second_serves_in_pct` integer, nullable
        - `service_games_played` integer, nullable
        - `tiebreaks_won` integer, nullable
        - `backhand_errors` integer, nullable: Forced errors off the backhand.
        - `backhand_unforced_errors` integer, nullable: Unforced errors off the backhand.
        - `backhand_winners` integer, nullable: Winners off the backhand.
        - `drop_shot_unforced_errors` integer, nullable: Unforced errors on the drop shot.
        - `drop_shot_winners` integer, nullable: Winners on the drop shot.
        - `errors_total` integer, nullable: The total of FORCED errors, not of all errors. Verified over 3,766 stored payload sides (June-July 2026): it equals the forehand + backhand + return + overhead error sum in 96.2% of sides, it is SMALLER than `unforced_errors_total` in 11.7% of sides (impossible for a superset), and per-match points accounting — points won = own winners + opponent forced errors + opponent unforced errors — closes with median residual 0 over 367 matches only under the forced reading. Total errors = `errors_total` + `unforced_errors_total`; there is no separate forced-errors field to wait for, because these fields ARE the forced counts.
        - `forehand_errors` integer, nullable: Forced errors off the forehand.
        - `forehand_unforced_errors` integer, nullable: Unforced errors off the forehand.
        - `forehand_winners` integer, nullable: Winners off the forehand.
        - `groundstroke_errors` integer, nullable: `forehand_errors` + `backhand_errors` — a rollup of the two, not an additive extra.
        - `groundstroke_unforced_errors` integer, nullable: Unforced errors off the ground strokes.
        - `groundstroke_winners` integer, nullable: Winners off the ground strokes.
        - `lob_unforced_errors` integer, nullable: Unforced errors on the lob.
        - `lob_winners` integer, nullable: Winners on the lob.
        - `overhead_errors` integer, nullable: Forced errors on the overhead — the upstream's overhead-stroke count.
        - `overhead_winners` integer, nullable: Winners on the overhead.
        - `return_errors` integer, nullable: Forced errors on the return.
        - `return_winners` integer, nullable: Winners on the return.
        - `unforced_errors_total` integer, nullable: The total of unforced errors. Total errors = `errors_total` + `unforced_errors_total`.
        - `volley_unforced_errors` integer, nullable: Unforced errors on the volley.
        - `volley_winners` integer, nullable: Winners on the volley.
        - `winners_total` integer, nullable: Total winners.
      - `service_games_played` integer
      - `service_games_won` integer
      - `hold_pct` integer, nullable: null when no service game was played — never 0, so a present 0 is a real measured zero
      - `return_games_played` integer
      - `return_games_won` integer
      - `break_pct` integer, nullable
      - `break_points_faced` integer
      - `break_points_saved` integer
      - `break_points_saved_pct` integer, nullable
      - `break_points_played` integer
      - `break_points_converted` integer
      - `break_points_converted_pct` integer, nullable
      - `service_points_played` integer
      - `service_points_won` integer
      - `service_points_won_pct` integer, nullable
      - `return_points_played` integer
      - `return_points_won` integer
      - `return_points_won_pct` integer, nullable
      - `points_played` integer
      - `points_won` integer

## Other responses

- `401`: Missing, unknown, or disabled credentials
- `403`: Your tier doesn't unlock this endpoint
- `404`: No such resource, or no data yet
- `410`: The match id existed and was merged into another match record (since 2026-09-05). The body is `MatchMerged`, not `Error`: it carries a forwarding address, which is the whole point of answering 410 rather than 404.
- `429`: Rate limit exceeded (Retry-After header present). Three body shapes, told apart by `error` and `scope`: the per-MINUTE limit (`rate_limited`, with `upgrade_url`, `tier` and `price` naming the next tier up); the per-DAY quota (`rate_limited` with `scope: "day"`, `limit_per_day`, and `resets_at` — the absolute ISO instant the daily window resets); and `abuse_throttled` with `retry_at_epoch` — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it.

---

[API](https://skmtc.dev/livetennisapi/apis/live-tennis-api.md) · [All operations](https://skmtc.dev/livetennisapi/apis/live-tennis-api/llms.txt) · [OpenAPI document](https://skmtc.dev/livetennisapi/apis/live-tennis-api/revisions/7881947e8e43?raw)
