---
title: "Tournament catalogue — the id space `Match.tournament_id` joins (FREE)"
method: GET
path: "/tournaments"
---

# Tournament catalogue — the id space \`Match.tournament_id\` joins (FREE)

`GET /tournaments`

## Request

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

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. Each row also carries the CURRENT season's `tier` / `tier_source` (added 2026-09-22) — the level in a closed vocabulary (`atp_500`, `challenger_125`, `wta_125`, `itf_w35` … | null); the tier is per season, so a match carries its own.

## Query parameters

- `search` string
- `tour` 'atp' | 'wta' | 'challenger' | 'itf' | 'juniors'
- `draw` 'singles' | 'doubles'
- `tier` string
- `limit` integer
- `offset` integer

## Response `200`

Tournaments, name order

- object
  - `data` Tournament[]
    - `id` string: The stable id `Match.tournament_id` joins.
    - `name` string, nullable
    - `tour` 'atp' | 'wta' | 'challenger' | 'itf' | 'juniors' | 'null', nullable
    - `surface` 'hard' | 'clay' | 'grass' | 'null', nullable: Court surface, null where the catalogue holds none — 61 of 10,287 tournaments on 2026-09-20. 35 of those are team competitions (Davis Cup, Billie Jean King Cup, Laver Cup), which have no tournament-level surface to hold: each tie is played on the host nation's choice, and that surface is published on each MATCH of the tie instead. The upstream field for those rows carries the DRAW TIER rather than a surface ("- Preliminary", "- Play Offs", "- Promotion"), and a value that is not one of hard/clay/grass is rejected rather than published. 3 are the exhibition catch-all and 23 are ordinary events the upstream catalogue simply never stated a surface for.
    - `indoor` boolean
    - `city` string, nullable: Host city, from a curated table — null where not curated.
    - `country` string, nullable: 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` 'grand_slam' | 'masters_1000' | 'tour_finals' | 'atp_500' | 'atp_250' | 'wta_1000' | 'wta_500' | 'wta_250' | 'wta_125' | 'challenger' | 'itf' | 'juniors' | 'null', nullable: Tournament category where our catalogues agree unambiguously on an exact-name join — null otherwise, never derived from the name. Unchanged by `tier` (2026-09-22) and remains the coarse class; read `tier` for the level.
    - `tier` 'grand_slam' | 'atp_finals' | 'atp_1000' | 'atp_500' | 'atp_250' | 'next_gen_finals' | 'united_cup' | 'davis_cup' | 'olympics' | 'laver_cup' | 'wta_finals' | 'wta_elite_trophy' | 'wta_1000' | 'wta_500' | 'wta_250' | 'wta_125' | 'bjk_cup' | 'challenger_175' | 'challenger_125' | 'challenger_100' | 'challenger_75' | 'challenger_50' | 'itf_m15' | 'itf_m25' | 'itf_w15' | 'itf_w25' | 'itf_w35' | 'itf_w40' | 'itf_w50' | 'itf_w60' | 'itf_w75' | 'itf_w80' | 'itf_w100' | 'juniors' | 'utr' | 'exhibition' | 'null', nullable: The level this tournament is played at in the CURRENT season (added 2026-09-22) — the same closed vocabulary as `tier` on a match (`grand_slam`, `atp_1000` … `atp_250`, `wta_1000` … `wta_125`, `challenger_175` … `challenger_50`, `itf_m15` / `itf_m25`, `itf_w15` … `itf_w100`, the team events, `juniors`, `utr`, `exhibition`). A catalogue row has no season of its own; a match does: the tier is PER SEASON and it moves under one id — Dallas was `atp_250` in 2023 and 2024 and `atp_500` from 2025; Estoril `atp_250`, then `challenger_175` in 2025, then `atp_250` again in 2026; Eastbourne `wta_500` through 2024 and `wta_250` from 2025 — so read the tier off each MATCH for its season, never copy this value onto a match. Null when this season's calendar does not list the event — never guessed.
    - `tier_source` 'calendar' | 'name' | 'wikipedia' | 'resolver' | 'null', nullable: How `tier` was established (added 2026-09-22): `calendar` = the official per-season tour calendar named the level (ATP tour and Challenger calendars, the WTA tournament list); `name` = an unambiguous name rule (the ITF category is in the event's official name — `M25 …`, `W35 …` — team and UTR events are named as such, and the Slam junior draws and exhibitions are their own event type); `wikipedia` = the season's schedule page, used only where the official calendar could not state the level for that season (Eastbourne 2023–2025); `resolver` = resolved after the seed dataset, by the same rules, from the event's name, tour and dates in that season. Null exactly when `tier` is null.
  - `meta` ListMeta
    - `limit` integer
    - `offset` integer
    - `count` integer
    - `total` integer, nullable: Size of the whole filtered set. Null when it cannot be counted cheaply.
    - `has_more` boolean: 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` string, nullable: 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` string, date-time, nullable: 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.

## Other responses

- `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.

---

[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)
