---
title: "Charted matches with shot-by-shot data (ULTRA)"
method: GET
path: "/rally/matches"
---

# Charted matches with shot-by-shot data (ULTRA)

`GET /rally/matches`

## Request

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

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.

## Query parameters

- `player` string
- `from` string, date
- `to` string, date
- `surface` string
- `gender` 'M' | 'W'
- `limit` integer
- `offset` integer

## Response `200`

Charted matches, with per-match parse-quality counts

- object
  - `data` RallyMatch[]
    - `rally_match_id` integer: The id this product is keyed on.
    - `source_id` string
    - `match_id` integer, nullable: OUR match id, when the charted match is also one we hold. Null otherwise — most charted matches predate our own collection.
    - `date` string, date, nullable
    - `tournament` string, nullable
    - `round` string, nullable
    - `surface` string, nullable
    - `gender` 'M' | 'W' | 'null', nullable
    - `best_of` integer, nullable
    - `players` object[]
      - `name` string, nullable
      - `hand` 'R' | 'L' | 'U' | 'A' | 'null', nullable
    - `points` integer: Charted points in this match.
    - `points_parsed` integer: How many of them our parser read cleanly — the per-match quality number.
  - `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
- `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.

---

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