---
title: "One flat object for on-air graphics (ULTRA)"
method: GET
path: "/broadcast/match/{matchId}"
---

# One flat object for on-air graphics (ULTRA)

`GET /broadcast/match/{matchId}`

## Request

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

Added 2026-09-24, from a broadcaster's request. ONE flat JSON object for one match, built so a graphics template can bind each field to a fixed path.

Three properties make it different from `GET /matches/{matchId}`, and each exists because a graphics pipeline breaks without it. The object is FLAT — no nested score object, no arrays of arrays. Every key is PRESENT on every read: a value we do not hold is `null`, never missing, because a missing key breaks a template binding. And the score is already in DISPLAY form — `p1_points` reads `"40"` or `"AD"`, `set_line` is one string such as `"5-7 2-0"`, and each set has its own column — so nothing has to be formatted downstream.

It is a PROJECTION of what the public API already publishes: the match object, its embedded score and the model probability, plus the break-, set- and match-point flags derived by the same classifiers the scoring pipeline uses. Nothing here is a new fact and nothing is guessed.

BUILT FOR 1 Hz. Every response carries a strong `ETag` and `Cache-Control: no-cache, max-age=1`; send `If-None-Match` and an unchanged state costs a 304 with no body. ULTRA's burst window is 600 requests a minute, so ten single-match pollers at 1 Hz fit on one key. Detect a new state on `sequence`, not on `served_at`, which changes on every read.

The key may be sent as `Authorization: Bearer <key>`, as `X-API-Key`, or as `?token=<key>` for tools that cannot set a header.

## Path parameters

- `matchId` integer, required

## Response `200`

The broadcast object for this match

- BroadcastObject: One flat object for a live-graphics template. EVERY key below is present on every read; a value we do not hold is `null` rather than absent, because a missing key breaks a template binding. Points and the set line are already in display form.
  - `match_id` integer
  - `status` string: `live`, `completed`, `upcoming`
  - `event_status` string, nullable: The terminal badge where one is stated (`Finished`, `Retired`, `Walk Over`, `Interrupted`); null on a normal live row.
  - `outcome` string, nullable
  - `winner` integer, nullable: 1 or 2, null until a result is established.
  - `tour` string, nullable
  - `tournament` string, nullable
  - `round` string, nullable
  - `surface` string, nullable
  - `best_of` integer, nullable
  - `is_doubles` boolean
  - `p1_id` integer, nullable
  - `p1_name` string, nullable
  - `p1_short` string, nullable: Display short form, `"J. Sinner"`. A doubles team keeps each side's last token.
  - `p1_country` string, nullable: Three-letter code.
  - `p1_ranking` integer, nullable
  - `p2_id` integer, nullable
  - `p2_name` string, nullable
  - `p2_short` string, nullable
  - `p2_country` string, nullable
  - `p2_ranking` integer, nullable
  - `p1_sets` integer, nullable
  - `p2_sets` integer, nullable
  - `p1_games` integer, nullable: Games in the set in play.
  - `p2_games` integer, nullable
  - `p1_points` string, nullable: As printed: `"0"`, `"15"`, `"30"`, `"40"`, `"AD"`, or the tiebreak count.
  - `p2_points` string, nullable
  - `p1_set1` integer, nullable: Per-set columns. A set not yet played is null on both sides.
  - `p2_set1` integer, nullable
  - `p1_set2` integer, nullable
  - `p2_set2` integer, nullable
  - `p1_set3` integer, nullable
  - `p2_set3` integer, nullable
  - `p1_set4` integer, nullable
  - `p2_set4` integer, nullable
  - `p1_set5` integer, nullable
  - `p2_set5` integer, nullable
  - `set_line` string, nullable: The completed and in-play sets as one string, `"5-7 2-0"`.
  - `current_set` integer, nullable
  - `is_tiebreak` boolean
  - `match_tiebreak` boolean: True while a deciding match tiebreak is in play.
  - `server` integer, nullable: 1 or 2.
  - `p1_serving` boolean, nullable
  - `p2_serving` boolean, nullable
  - `break_point_for` integer, nullable: 1, 2 or null. Cue an animation on the transition to non-null.
  - `set_point_for` integer, nullable
  - `match_point_for` integer, nullable
  - `p1_win_pct` integer, nullable: The model probability as a whole percent. Null where no model figure was computed for this state — never guessed.
  - `p2_win_pct` integer, nullable
  - `sequence` integer, nullable: The per-match accept counter. THIS is how a poller detects a new state.
  - `updated_at` string, date-time, nullable
  - `age_seconds` integer, nullable: Seconds since the score CONTENT last changed.
  - `stale` boolean, nullable: A live match whose content has frozen past the threshold.
  - `served_at` string, date-time: When this response was built. Changes on every read — do not use it to detect a new state.

## Other responses

- `304`: Unchanged since the `ETag` you sent
- `401`: Missing, unknown, or disabled credentials
- `403`: Your tier doesn't unlock this endpoint
- `404`: No such resource, or no data yet
- `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)
