Live Tennis API Rally API
The Rally API from Live Tennis API — 2 operation(s) for rally.
The Rally API from Live Tennis API — 2 operation(s) for rally.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.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.curl "https://apis.io/api/v1/apis/livetennisapi-com:livetennisapi-com-rally-api"
curl "https://apis.io/api/v1/apis?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
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.
openapi: 3.2.0
info:
title: Live Tennis Rally API
version: 1.13.76
contact:
name: Live Tennis API
url: https://livetennisapi.com
license:
name: MIT
identifier: MIT
termsOfService: https://livetennisapi.com/terms
description: 'Real-time tennis scores, player data, match-winner market prices, and
model-driven match analysis. Read-only. Coverage spans ATP, WTA,
Challenger, ITF and the junior Grand Slam draws — depth differs by tour
and surface; `GET /history/coverage` states the measured numbers. Access is tiered (FREE / BASIC / PRO / ULTRA). Each tier includes
everything in the tiers below it; the concrete deltas are:
`FREE` — self-serve, no card (https://livetennisapi.com/subscribe/free). Live and upcoming matches, current scores, players, fixtures, the
tournament catalogue (`/tournaments`), and your own usage stats. 30 requests/minute, 100/day. No market prices, no model fields, no
WebSocket. Historical results are not part of the tier, but a FREE key
may spend 20 calls per calendar month on the history endpoints as a
taste of the product — served exactly like an entitled call. Past that
they answer `403 upgrade_required` carrying
`free_history_taste: "used"`. `BASIC` — adds historical data: the completed-match listing
(`/history/matches`, and `status=completed` on `/matches`), the
per-match point-by-point tape with the model win-probability on the
rows where the model ran
(`/history/matches/{matchId}`), the measured completeness rollup
(`/history/coverage`), and the results archive (1968–2022) —
deep results (`/history/archive/matches`), archive player bios
(`/history/archive/players`), career aggregates
(`/history/archive/career`) and head-to-head (`/h2h`). 60 requests/minute, 1,000/day. `PRO` — adds match events (`/matches/{matchId}/events`), market prices
(`/markets`, `/markets/{matchId}/prices`, `/matches/{matchId}/prices`),
the pre-built monthly bulk history packages (`/history/packages`), the
rank-ordered rankings listing (`/rankings?system=`) and one player''s
win/loss and set-based record (`/players/{playerId}/stats`). 300 requests/minute, 10,000/day. `ULTRA` — adds model analysis (`/matches/{matchId}/analysis`), the live
model fields (`win_probability_p1`, `danger`) on every score object,
in-play match statistics (`/matches/{matchId}/statistics`) and their
per-player sums (`serve_return` on `/players/{playerId}/stats`), per-player
as-of ranking records (`/rankings?player=`), the as-of Elo tape
(`/rankings?system=elo` — both modes, plus `kind=elo` bulk packages),
rally construction
(`/rally/matches`, shot-by-shot charted data), career and per-match
charting stats (`/charting/players`, `/charting/matches/{chartingMatchId}`),
the reconstructed 2013–2022 archive tape
(`/history/archive/matches/{archiveId}/tape` — also opened by ANY active
History plan, Starter included), the WebSocket live feed at `/ws` and the
high-fan-out push feed (`/ws-token`), and outbound webhooks (direct keys). 600 requests/minute, 500,000/day. History runs in two continuous halves, deliberately non-overlapping: the
point-by-point tape (2023→now) covers January 2023 to now, match by
match, point by point; the results archive (1968–2022) covers 1968
through 2022 as winner/loser-shaped RESULTS (final score, seeds, ranks at
the time — no point-by-point). The archive ends exactly where the tape
begins, so no match is ever served from two datasets. Archive results played **2013–2022** additionally carry a RECONSTRUCTED
point-by-point tape at `/history/archive/matches/{archiveId}/tape` — the
score sequence behind the published result: reconstructed point sequences,
no capture timestamps. 97,901 matches / 14,340,663 rows, seasons **2013–2022
ONLY**: the archive holds a further 977,903 results from 1968–2012 and NOT
ONE of them has a tape, because we hold no point sequence for those years and
we do not manufacture one. Write the range as
2013–2022, never as "pre-2023" — the second phrasing reads as 1968 onward
and is wrong by 45 seasons.'
servers:
- url: https://api.livetennisapi.com/api/public/v1
security:
- bearerAuth: []
- apiKeyHeader: []
tags:
- name: Rally
paths:
/rally/matches:
get:
summary: Charted matches with shot-by-shot data (ULTRA)
description: 'Charted matches, newest first. RALLY CONSTRUCTION IS THE LAYER BELOW THE TAPE: the tape says what the score became after each point, this says how the point was played.
It has its OWN id space. The charted corpus and our own match table are different populations — the corpus reaches back decades and concentrates on the biggest events, while our matches begin when our own collection did. Keying this on our match ids would hide most of it. Ask this endpoint for the authoritative coverage list rather than assuming a match is charted: charting is human work, so coverage is deep, not universal.'
operationId: listRallyMatches
parameters:
- name: player
in: query
required: false
schema:
type: string
description: Substring match on either player name.
- name: from
in: query
required: false
schema:
type: string
format: date
description: '`YYYY-MM-DD`.'
- name: to
in: query
required: false
schema:
type: string
format: date
description: '`YYYY-MM-DD`.'
- name: surface
in: query
required: false
schema:
type: string
- name: gender
in: query
required: false
schema:
type: string
enum:
- M
- W
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/offset'
responses:
'200':
description: Charted matches, with per-match parse-quality counts
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/RallyMatch'
meta:
$ref: '#/components/schemas/ListMeta'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/UpgradeRequired'
'429':
$ref: '#/components/responses/RateLimited'
tags:
- Rally
/rally/matches/{rallyMatchId}:
get:
summary: Rally construction for one charted match (ULTRA)
description: One charted match with its points, in play order. Paged with `limit`/`offset`; `meta.total` is the match's full point count.
operationId: getRallyMatch
parameters:
- name: rallyMatchId
in: path
required: true
schema:
type: integer
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/offset'
responses:
'200':
description: The charted match with its `rally` points
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/RallyMatch'
- type: object
properties:
meta:
$ref: '#/components/schemas/ListMeta'
rally:
type: array
items:
$ref: '#/components/schemas/RallyPoint'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/UpgradeRequired'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
tags:
- Rally
components:
parameters:
offset:
name: offset
in: query
schema:
type: integer
default: 0
minimum: 0
limit:
name: limit
in: query
schema:
type: integer
default: 50
minimum: 1
maximum: 200
schemas:
RallyMatch:
type: object
description: One charted match of the rally-construction corpus — its own id space, keyed on `rally_match_id`.
properties:
rally_match_id:
type: integer
description: The id this product is keyed on.
source_id:
type: string
match_id:
type:
- integer
- 'null'
description: OUR match id, when the charted match is also one we hold. Null otherwise — most charted matches are older than our match table.
date:
type:
- string
- 'null'
format: date
tournament:
type:
- string
- 'null'
round:
type:
- string
- 'null'
surface:
type:
- string
- 'null'
gender:
type:
- string
- 'null'
enum:
- M
- W
- null
best_of:
type:
- integer
- 'null'
players:
type: array
maxItems: 2
items:
type: object
properties:
name:
type:
- string
- 'null'
hand:
type:
- string
- 'null'
enum:
- R
- L
- U
- A
- null
points:
type: integer
description: Charted points in this match.
points_parsed:
type: integer
description: How many of them our parser read cleanly — the per-match quality number.
RallyShot:
type: object
description: 'One stroke. Shots are numbered from the serve: serve 1, return 2, the server''s next ball 3.'
properties:
number:
type: integer
code:
type: string
description: The charter's raw code, e.g. 'f'.
stroke:
type:
- string
- 'null'
enum:
- serve
- groundstroke
- slice
- volley
- half_volley
- swinging_volley
- overhead
- drop_shot
- lob
- trick
- unknown
- null
wing:
type:
- string
- 'null'
enum:
- forehand
- backhand
- null
description: The side it was struck FROM.
direction:
type:
- string
- 'null'
enum:
- forehand_side
- middle
- backhand_side
- null
description: Where the ball was sent.
depth:
type:
- string
- 'null'
enum:
- shallow
- mid
- deep
- null
position:
type:
- string
- 'null'
enum:
- approaching
- at_net
- baseline
- null
Error:
type: object
properties:
error:
type: string
description: Stable machine-readable code, e.g. bad_date, bad_coverage, bad_sequence, bad_points, bad_points_complete, bad_combination, points_read_disabled, bad_after_seq, bad_changed_since, bad_restated_since, points_disabled, bad_period, bad_year, bad_format, bad_tour, bad_tier, bad_country, ambiguous_name, not_charted, not_found, no_analysis, no_market, upgrade_required, rate_limited, abuse_throttled.
detail:
type: string
description: Human-readable explanation, when one adds anything.
allowed:
type: array
items:
type: string
description: On a rejected enumerated parameter, the values that would have been accepted (e.g. the coverage vocabulary for bad_coverage).
ListMeta:
type: object
properties:
limit:
type: integer
offset:
type: integer
count:
type: integer
total:
type:
- integer
- 'null'
description: Size of the whole filtered set. Null when it cannot be counted cheaply.
has_more:
type: boolean
description: More results exist beyond this page. Read this rather than comparing count to limit. For a complete enumeration of a filtered set, page `offset` forward by `limit` until this is false; on `completed`/`cancelled` listings `total` is null and this is the only end-of-data signal.
next_cursor:
type:
- string
- 'null'
description: Change-feed only (`?updated_since=` / `?cursor=`). Opaque token to pass back as `?cursor=`. Null means you are caught up — stop paging and switch to polling from `watermark`.
watermark:
type:
- string
- 'null'
format: date-time
description: Change-feed only. The floor to use on your NEXT poll. Deliberately trails the server clock by 5s so a row committed during this request cannot fall through the gap.
RallyPoint:
type: object
description: One charted point. `raw` is the charter's own string, verbatim, and is ALWAYS present; the parsed fields are our reading of it. `parsed` is false when the notation contained something we could not read cleanly — the recognised part is still returned. A consumer who wants only unambiguous rows filters on `parsed`.
properties:
point:
type: integer
set:
type: array
items:
type:
- integer
- 'null'
maxItems: 2
games:
type: array
items:
type:
- integer
- 'null'
maxItems: 2
score:
type:
- string
- 'null'
description: e.g. '30-40'.
game:
type:
- integer
- 'null'
is_tiebreak:
type: boolean
server:
type:
- integer
- 'null'
enum:
- 1
- 2
- null
point_winner:
type:
- integer
- 'null'
enum:
- 1
- 2
- null
raw:
type:
- string
- 'null'
description: The charter's shot string; both serves joined by ';' when the first was a fault.
parsed:
type: boolean
serve_number:
type:
- integer
- 'null'
enum:
- 1
- 2
- null
serve_direction:
type:
- string
- 'null'
enum:
- wide
- body
- down_the_t
- null
rally_length:
type:
- integer
- 'null'
description: Strokes including the serve. An ace is 1, a double fault 0.
outcome:
type:
- string
- 'null'
enum:
- winner
- forced_error
- unforced_error
- error
- other
- null
description: '`error` = the charter recorded a miss without saying whether it was forced. Never guessed.'
error_location:
type:
- string
- 'null'
enum:
- net
- wide
- deep
- wide_and_deep
- null
ending_stroke:
type:
- string
- 'null'
ending_wing:
type:
- string
- 'null'
is_ace:
type: boolean
is_double_fault:
type: boolean
is_serve_and_volley:
type: boolean
is_unreturned:
type: boolean
description: Derived (added 2026-09-05). The server won the point without the returner putting the ball in play. A double fault is never unreturned.
unreturned_kind:
type:
- string
- 'null'
enum:
- ace
- serve_winner
- serve_forced_error
- return_forced_error
- return_unforced_error
- return_error
- null
description: How the serve went unreturned. `serve_winner` is a serve winner charted without a direction code (so `is_ace` is unset) — count it with the aces. `return_error` = the return missed and the charter did not say whether it was forced. Null when the return was in play or the parse could not say.
shots:
type: array
items:
$ref: '#/components/schemas/RallyShot'
responses:
BadRequest:
description: Bad query parameter
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
UpgradeRequired:
description: Your tier doesn't unlock this endpoint
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: No such resource, or no data yet
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Missing, unknown, or disabled credentials
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
RateLimited:
description: '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.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: 'Direct API key: Authorization: Bearer <key>'
apiKeyHeader:
type: apiKey
in: header
name: X-API-Key