Live Tennis API Fixtures API
The Fixtures API from Live Tennis API — 1 operation(s) for fixtures.
The Fixtures API from Live Tennis API — 1 operation(s) for fixtures.
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-fixtures-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 Fixtures 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: Fixtures
paths:
/fixtures:
get:
summary: Upcoming scheduled fixtures, earliest first (FREE)
operationId: listFixtures
parameters:
- $ref: '#/components/parameters/tour'
- $ref: '#/components/parameters/draw'
- $ref: '#/components/parameters/isQualifying'
- $ref: '#/components/parameters/gender'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/offset'
responses:
'200':
description: Fixtures with start time and player ids where resolved — the nulls are real states, not gaps (names are always present)
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Fixture'
meta:
$ref: '#/components/schemas/ListMeta'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
tags:
- Fixtures
components:
parameters:
isQualifying:
name: is_qualifying
in: query
required: false
schema:
type: boolean
description: 'Return only matches in the QUALIFYING draw (`true`) or only those the source states are not (`false`), matching the `is_qualifying` flag each row carries (field added 2026-08-12, filter added 2026-10-02). Omit for both. Anything other than true/false is a 400 `bad_is_qualifying`, never a silently ignored filter.
THIS IS THE ONLY WAY TO SPLIT THE TWO DRAWS, and the reason is in the `is_qualifying` field description: the two are routinely carried under ONE tournament name with ONE round vocabulary, so `tournament`, `tournament_id`, `round` and `round_code` all read main-draw for a qualifying match. Do not filter on the round text.
THREE-VALUED, so this filter is too: a row whose flag is `null` (no source has ever stated it) matches NEITHER value and is returned only when the filter is omitted. `null` is an answer, not a `false` — returning it under `is_qualifying=false` would assert "main draw" on the strength of nothing. Measured 2026-10-02 over completed matches: the flag is stated on 98.9% of the last 30 days and 98.3% of 2026 outside the UTR circuit, where it is mostly unstated (1,695 of the 1,831 unstated rows in the last 30 days are UTR).'
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
tour:
name: tour
in: query
description: Restrict results to one tour. Each value covers its singles and doubles draws, so `atp` includes ATP doubles and `juniors` covers the boys' and girls' Grand Slam draws. Omit for all tours. An unrecognised value is a 400 rather than a silent pass-through, so a caller never receives a tour it did not ask for. Applied before pagination, so `meta.count` reflects the filtered set.
schema:
type: string
enum:
- atp
- wta
- challenger
- itf
- juniors
draw:
name: draw
in: query
schema:
type: string
enum:
- singles
- doubles
description: 'Draw filter (added 2026-08-18) — the axis the tour filter deliberately collapses; the two compose (`?tour=itf&draw=doubles` is the ITF doubles slice). Same vocabulary as the `draw` field on Match, decided by the same shared definition, so filter and field cannot disagree. A row whose draw is null — a team tie, or no stated event type and no doubles-team participant — matches NEITHER value: null is an answer, not a wildcard. Two honesty notes: on /tournaments the answer comes from the event type alone (a tournament row has no participants to supply the doubles-team evidence matches have), and draw=doubles alone also returns mixed and exhibition doubles that no tour value reaches. An unknown value is a 400 bad_draw with the allowed values.'
gender:
name: gender
in: query
required: false
schema:
type: string
enum:
- men
- women
description: "Return only men's or only women's matches, matching the `gender` field each row carries (added 2026-09-09, documented here 2026-10-02). Omit for both. Anything other than men/women is a 400 `bad_gender`, never a silently ignored filter.\nIt exists because `?tour=` deliberately collapses: `challenger` is ATP Challenger AND WTA 125, and `itf` and `juniors` are both sexes — so without this there is no way to ask for men's Challenger at all. A row whose `gender` is `null` (mixed doubles, a team tie) matches NEITHER value, the same contract as `?draw=` and `?is_qualifying=`.\n bad_points_complete, bad_combination, points_read_disabled, bad_after_seq, bad_has_market, no_market, points_disabled,"
schemas:
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.
Fixture:
type: object
properties:
id:
type: integer
event_date:
type:
- string
- 'null'
format: date
start_time:
type:
- string
- 'null'
format: date-time
description: Scheduled start (UTC). Null until the order of play assigns a time — a date-only fixture is a real state, and rows written before 2026-08-03 stay null.
player1_id:
type:
- integer
- 'null'
description: Our player id, when the participant is in our roster (exact-key resolution, never a name match). Null otherwise — names are always present regardless.
player2_id:
type:
- integer
- 'null'
description: As `player1_id`, for the second participant.
tour:
type:
- string
- 'null'
description: The record's OWN tour, which is NOT the `tour` filter vocabulary. It is granular (`juniors_boys`, `juniors_girls`, `challenger_men`) where the filter is grouped (`juniors`, `challenger`), and a doubles team reports it UPPERCASE (`ATP`) where an individual reports lowercase (`atp`). Treat it as an opaque string; do not parse it into the filter enum.
tournament:
type:
- string
- 'null'
round:
type:
- string
- 'null'
round_code:
type:
- string
- 'null'
enum:
- F
- SF
- QF
- R16
- R32
- R64
- R128
- RR
- BR
- Q
- Q1
- Q2
- Q3
- Q4
- ER
- null
description: Normalized round — same vocabulary as `Match.round_code`. Null when the label is unrecognised, never guessed.
surface:
type:
- string
- 'null'
player1_name:
type:
- string
- 'null'
player2_name:
type:
- string
- 'null'
status:
type:
- string
- 'null'
responses:
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