GET /players
Search players by name (FREE)
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
search | query | string | no | |
limit | query | integer | no | Default 50. |
offset | query | integer | no | Default 0. |
Responses
| Status | Meaning |
|---|---|
200 | Players (ranked first; no stats object on the list) |
401 | Missing, unknown, or disabled credentials |
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
| Field | Type | Description |
|---|---|---|
data | array of object | |
meta | object |
Example
curl https://api.livetennisapi.com/api/public/v1/players \
-H "Authorization: Bearer twjp_..."
GET /players/{playerId}
One player's bio + ranking + cached stats (FREE)
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
playerId | path | integer | yes |
Responses
| Status | Meaning |
|---|---|
200 | Player with stats ({ratings, season}) |
401 | Missing, unknown, or disabled credentials |
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
| Field | Type | Description |
|---|---|---|
id | integer | |
name | string | |
tour | string or null | 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. |
country | string or null | |
ranking | integer or null | Official singles ranking POSITION (the ordinal: 1, 25, 100) — never points, seed, race, UTR, Elo or a doubles rank. Men from the official ATP table, women from the WTA table, chosen by the player (a Challenger or ITF man who holds an ATP ranking carries it here). Refreshed from the standings ahead of each match the player has with us; a player who has not played recently keeps their last refreshed position. null when no ranking is held (doubles teams, most ITF/junior-only players). Always the CURRENT record, even on historical matches — use /rankings?as_of= for the rank in force on a date. |
ranking_points | integer or null | Points on the same official table, refreshed with ranking. |
ranking_movement | string or null (up, down, same, null) | |
hand | string or null (R, L, null) | |
backhand | integer or null (1, 2, null) | |
birthday | string or null | |
is_doubles_team | boolean | |
data_completeness | object | How much biographical detail is known for this player, so a consumer can distinguish "not in the feed" from "not yet fetched" without probing. Present on every player in a match payload. Lower tours carry far less of it than main tour. |
stats | object | Single-player endpoint only |
Example
curl https://api.livetennisapi.com/api/public/v1/players/1104 \
-H "Authorization: Bearer twjp_..."
GET /tournaments
Tournament catalogue — the id space Match.tournament_id joins (FREE)
Stable tournament identity, one row per tournament × event type, stable across seasons. city/country come from a curated table and category only where our catalogues agree unambiguously on an exact-name join — each is null otherwise, never derived from the tournament name.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
search | query | string | no | Case-insensitive substring match on the tournament name. |
tour | query | string (atp, wta, challenger, itf, juniors) | no | 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. |
draw | query | string (singles, doubles) | no | 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. |
limit | query | integer | no | Default 50. |
offset | query | integer | no | Default 0. |
Responses
| Status | Meaning |
|---|---|
200 | Tournaments, name order |
400 | Bad query parameter |
401 | Missing, unknown, or disabled credentials |
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
| Field | Type | Description |
|---|---|---|
data | array of object | |
meta | object |
Example
curl https://api.livetennisapi.com/api/public/v1/tournaments \
-H "Authorization: Bearer twjp_..."
GET /tournaments/{tournamentId}
One tournament by its stable id (FREE)
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
tournamentId | path | string | yes | The tournament_id carried on match objects. |
Responses
| Status | Meaning |
|---|---|
200 | The tournament |
401 | Missing, unknown, or disabled credentials |
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
| Field | Type | Description |
|---|---|---|
id | string | The stable id Match.tournament_id joins. |
name | string or null | |
tour | string or null (atp, wta, challenger, itf, juniors, null) | |
surface | string or null (hard, clay, grass, null) | |
indoor | boolean | |
city | string or null | Host city, from a curated table — null where not curated. |
country | string or null | Host country, ISO-3166 alpha-2 — null where not curated. (NOTE this differs from player.country and the ?country= filter, which use IOC-style lowercase 3-letter codes.) |
category | string or null (grand_slam, masters_1000, tour_finals, atp_500, atp_250, wta_1000, wta_500, wta_250, wta_125, challenger, itf, juniors, null) | Tournament category where our catalogues agree unambiguously on an exact-name join — null otherwise, never derived from the name. |
Example
curl https://api.livetennisapi.com/api/public/v1/tournaments/{tournamentId} \
-H "Authorization: Bearer twjp_..."
GET /rankings
Rankings and Elo — rank-ordered listing (PRO) or per-player as-of records (ULTRA); the as-of Elo tape is ULTRA in both modes
Returns, per ranking system, the newest record effective ON OR BEFORE as_of — never one dated after it. Every other ranking field in this API is the player's CURRENT value joined at read time, so replaying an old match elsewhere shows today's ranks; this endpoint is the point-in-time answer. Systems are never collapsed into a single "rank": ATP/WTA and the ITF circuits carry rank+points, UTR carries a rating with null rank and points because it has neither. meta.coverage.oldest_available gives the earliest date each system can answer for — ITF and UTR observations reach back to 2026-06-01 (append-only per-player history accumulates from 2026-07-29) and nothing earlier can be reconstructed.
TWO MODES — with player ids (ULTRA) — the per-player point-in-time records described above. WITHOUT player (PRO) — the FULL published table in rank order for exactly one system, the newest week at or before as_of; rows carry player_name as published and a null player_id for players outside our roster, so the table has no silent holes. utr has no listing (a rating, not a ranking).
SYSTEM=UTR — observed ratings, honestly bounded. UTR records are observed from UTR's public search: a rating UTR withholds appears as ABSENT, never as 0, and rating is the only populated value — rank and points are always null. Per-player as-of ONLY — there is deliberately no UTR listing, because a table of only the players we happen to track would be a fake leaderboard. Per-player history accumulates from 2026-07-29; scattered earlier single-snapshot observations reach back to 2026-06-01. Coverage is a deliberate bias, not a roster mirror: the 24-hour sweep targets players with no official rank and no Elo rating (so it skews ITF), and among players active in the last 60 days it holds 931 of 5,606 ITF players (16.6%), 197 of 1,903 Challenger (10.4%), 43 of 573 WTA (7.5%) and 15 of 525 ATP (2.9%) — measured 2026-08-17.
SYSTEM=ELO — THE AS-OF ELO TAPE (ULTRA in BOTH modes). Our own computed Elo for 65,622 players on four independent ladders (overall, hard, clay, grass), back to 1877 (ATP) and 1968 (WTA), covering the main tours plus challengers plus the futures tier. It answers what a player was rated BEFORE a given match, which is the only shape a backtest can consume. It is a HISTORICAL TAPE rather than a live leaderboard — the corpus behind it is fixed and no longer receives new results, so meta.coverage.newest_available states the tape's head date on every response (2026-06-15 at publication), and you should read it before treating the table as current. A week's results become effective 14 days after that week begins — strictly after the longest event in tennis — so the failure direction is staleness, never look-ahead. rating is the Elo. rank is LISTING MODE ONLY and is null in per-player mode, because an Elo has no global rank at a past instant until you say which field and which activity window you mean. points is always null. matches is the count on THAT ladder, published so that a rating still near its 1500 cold start is visible rather than inferred. A ladder a player has never played is omitted, never substituted. Ratings are on our own scale, are not comparable with Elo published elsewhere, and do not decay — a surface played only a few weeks a year (grass in particular) moves slowly, so a rising player can sit below an established one for several seasons while beating them. A CURRENT per-player Elo remains free on GET /players/{id}; this is the point-in-time series, the leaderboard and the bulk export. That free rating is sourced differently and sits on a DIFFERENT scale — the two differ by roughly 150 Elo of per-player standard deviation — so never present a rating from one scale against a rating from the other. The Elo listing REQUIRES tour — the ATP and WTA walks are disjoint, so a combined leaderboard would not be comparable — and takes exactly one surface (default overall). elo is never implicit — omitting system returns the official systems only.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
player | query | array of integer | no | Player id — repeatable, max 50 (ULTRA per-player mode). OMIT it for the rank-ordered listing mode (PRO), which then requires exactly one system. |
archive_player | query | array of integer | no | Historical-corpus person id — repeatable, max 50, system=elo only, and REQUIRES tour because that id is only unique within one tour. It is the same id GET /history/archive/players returns as its id. Use it to reach the ~62,000 rated people who have no row in our player roster; player reaches the rest. |
tour | query | string (atp, wta) | no | REQUIRED for an elo listing and for archive_player. The ATP and WTA Elo walks are disjoint universes whose players never meet, so a combined leaderboard would not be comparable. |
surface | query | array of string (overall, hard, clay, grass) | no | system=elo only. Listing mode takes exactly one (default overall); per-player mode is repeatable and defaults to all four ladders. A ladder the player has never played is omitted, never substituted. |
min_matches | query | integer | no | system=elo listing only. Minimum matches on that ladder. Every ladder starts at 1500, so without a floor the top of the table is players who won a handful of matches from the starting rating. Echoed back in meta.coverage.qualified. Default 20. |
activity_weeks | query | integer | no | system=elo listing only. The ladder must have moved within this many weeks of as_of. Elo does not decay, so without an activity window every leaderboard is topped permanently by players who have stopped playing. Echoed back in meta.coverage.qualified. Default 52. |
as_of | query | string | no | YYYY-MM-DD. Omit for the latest known record. |
system | query | array of string (atp, wta, itf_jt, itf_mt, itf_wt, utr, elo, atp_doubles, wta_doubles) | no | Restrict to one or more systems. Omit for all of the official systems — elo, atp_doubles and wta_doubles are NEVER included implicitly and must be named (a doubles tie prints as repeated ranks, as the publisher prints it), so an existing request's response is unchanged. Naming a system your plan does not cover refuses the whole call with 403 rather than silently returning the part you are entitled to. |
limit | query | integer | no | Default 50. |
offset | query | integer | no | Default 0. |
Responses
| Status | Meaning |
|---|---|
200 | Ranking records in force at as_of |
400 | Bad query parameter |
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. |
Response fields
| Field | Type | Description |
|---|---|---|
data | array of object | |
meta | object |
Example
curl https://api.livetennisapi.com/api/public/v1/rankings \
-H "Authorization: Bearer twjp_..."