How do you put live tennis scores into a broadcast graphics template?GET /broadcast/match/{matchId} returns ONE flat object for one match and GET /broadcast/live the same object for every live match in a single call. Both are ULTRA. A graphics template binds each field to a fixed path, so three things are contractual: the object is flat, every key is present on every read (null, never missing), and the score is already in display form — p1_points reads "40" or "AD" and set_line is one string. Built for 1 Hz: every response carries a strong ETag, so If-None-Match makes an unchanged state a 304 with no body. Detect a new state on sequence, never on served_at, which changes on every read.
2 endpoints on this page. Base URL
https://api.livetennisapi.com/api/public/v1; authenticate with the X-API-Key header. Plans involved:
ULTRA. A free key needs no card.
The endpoints
Each one below lists its plan tier, every parameter, the response shape and an example. They
are the same entries as in the full reference, which
carries all 48 operations of the API on one page.
GET/broadcast/match/{matchId}
One flat object for on-air graphics (ULTRA)
Plan required: ULTRA · operationId: getBroadcastMatch
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.
Parameters
GET /broadcast/match/{matchId} — parameters
Name
In
Type
Required
Notes
matchId
path
integer
yes
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.
Responses
GET /broadcast/match/{matchId} — responses
Status
Meaning
200
The broadcast object for this match
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.
Response fields
GET /broadcast/match/{matchId} — response fields
Field
Type
Description
match_id
integer
status
string
live, completed, upcoming
event_status
string or null
The terminal badge where one is stated (Finished, Retired, Walk Over, Interrupted); null on a normal live row.
outcome
string or null
winner
integer or null
1 or 2, null until a result is established.
tour
string or null
tournament
string or null
round
string or null
surface
string or null
best_of
integer or null
is_doubles
boolean
p1_id
integer or null
p1_name
string or null
p1_short
string or null
Display short form, "J. Sinner". A doubles team keeps each side's last token.
p1_country
string or null
Three-letter code.
p1_ranking
integer or null
p2_id
integer or null
p2_name
string or null
p2_short
string or null
p2_country
string or null
p2_ranking
integer or null
p1_sets
integer or null
p2_sets
integer or null
p1_games
integer or null
Games in the set in play.
p2_games
integer or null
p1_points
string or null
As printed: "0", "15", "30", "40", "AD", or the tiebreak count.
p2_points
string or null
p1_set1
integer or null
Per-set columns. A set not yet played is null on both sides.
p2_set1
integer or null
p1_set2
integer or null
p2_set2
integer or null
p1_set3
integer or null
p2_set3
integer or null
p1_set4
integer or null
p2_set4
integer or null
p1_set5
integer or null
p2_set5
integer or null
set_line
string or null
The completed and in-play sets as one string, "5-7 2-0".
current_set
integer or null
is_tiebreak
boolean
match_tiebreak
boolean
True while a deciding match tiebreak is in play.
server
integer or null
1 or 2.
p1_serving
boolean or null
p2_serving
boolean or null
break_point_for
integer or null
1, 2 or null. Cue an animation on the transition to non-null.
set_point_for
integer or null
match_point_for
integer or null
p1_win_pct
integer or null
The model probability as a whole percent. Null where no model figure was computed for this state — never guessed.
p2_win_pct
integer or null
sequence
integer or null
The per-match accept counter. THIS is how a poller detects a new state.
updated_at
string or null
age_seconds
integer or null
Seconds since the score CONTENT last changed.
stale
boolean or null
A live match whose content has frozen past the threshold.
served_at
string
When this response was built. Changes on every read — do not use it to detect a new state.
Plan required: ULTRA · operationId: getBroadcastLive
The same object as GET /broadcast/match/{matchId}, for every actively-live match, in one call. A ticker or a multi-match wall polls this instead of one URL per match, and it stays ONE request however many matches are live. At most 200 objects.
Same 1 Hz contract, same ETag and Cache-Control, same display-form fields, same every-key-present rule. An empty data means nothing is live right now and is the ordinary overnight answer, not a fault.
Parameters
GET /broadcast/live — parameters
Name
In
Type
Required
Notes
tour
query
string
no
Restrict to one tour, same vocabulary as ?tour= elsewhere.
Responses
GET /broadcast/live — responses
Status
Meaning
200
Every live match as a broadcast object
304
Unchanged since the ETag you sent
401
Missing, unknown, or disabled credentials
403
Your tier doesn't unlock this endpoint
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.