---
title: "One player's bio + ranking + cached stats (FREE)"
method: GET
path: "/players/{playerId}"
---

# One player's bio + ranking + cached stats (FREE)

`GET /players/{playerId}`

## Request

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

`playerId` is a ROSTER id — the live player registry, the id space `/players`, `/matches` and `/rankings` all speak.
It is NOT the archive corpus person id. The results archive (1968–2022) keeps its own person registry, and archive match rows publish those ids as `winner.player_id` / `loser.player_id`. The two spaces are disjoint: no corpus id resolves here, and since 2026-09-20 a 404 for one says so and points at `/history/archive/players?id={playerId}`, which is where that id is read. A 404 with no `see` field is simply an id we do not hold.

## Path parameters

- `playerId` integer, required

## Response `200`

Player with `stats` ({ratings, season})

- Player
  - `id` integer
  - `name` string
  - `tour` string, nullable: 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, nullable
  - `ranking` integer, nullable: 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, nullable: Points on the same official table, refreshed with `ranking`.
  - `ranking_movement` 'up' | 'down' | 'same' | 'null', nullable
  - `hand` 'R' | 'L' | 'null', nullable
  - `backhand` 1 | 2 | null, nullable
  - `birthday` string, date, nullable
  - `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.
    - `known` integer, nullable: Fields populated, of `of`. NULL on a doubles team, where per-player biography does not apply — distinct from `0`, which means the fields apply and none are known. Do not decode this into a non-nullable integer.
    - `of` integer, nullable: Fields considered. NULL on a doubles team, as for `known`.
    - `missing` string[]: Names of the unpopulated fields, e.g. ["backhand", "hand"].
    - `note` string: Present only when the object is not applicable, explaining why (e.g. a doubles team).
  - `stats` object: Single-player endpoint only
    - `ratings` object, nullable
    - `ratings_as_of` string, date, nullable: The date (UTC) the `ratings` block was last refreshed (added 2026-09-10). The current Elo is refreshed every week from the published rating tables; read this before treating a rating as this week's. Null when there is no rating.
    - `season` unknown

## Other responses

- `401`: Missing, unknown, or disabled credentials
- `404`: No roster player holds this id. When the id IS a corpus person id the body adds `detail` and `see` (the archive lookup); otherwise it is the plain `Error` body.
- `410`: The player id existed and was merged into another player record, or was retired with no successor. The body is `PlayerMerged`, not `Error`: it carries a forwarding address, which is the whole point of answering 410 rather than 404. An id that was NEVER issued keeps its 404 — a 410 would assert it once existed.
- `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)
