---
title: "Rally construction for one charted match (ULTRA)"
method: GET
path: "/rally/matches/{rallyMatchId}"
---

# Rally construction for one charted match (ULTRA)

`GET /rally/matches/{rallyMatchId}`

## Request

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

One charted match with its points, in play order. Paged with `limit`/`offset`; `meta.total` is the match's full point count.

## Path parameters

- `rallyMatchId` integer, required

## Query parameters

- `limit` integer
- `offset` integer

## Response `200`

The charted match with its `rally` points

- object: One charted match of the rally-construction corpus — its own id space, keyed on `rally_match_id`.
  - `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.
  - `rally` RallyPoint[]
    - `point` integer
    - `set` integer[]
    - `games` integer[]
    - `score` string, nullable: e.g. '30-40'.
    - `game` integer, nullable
    - `is_tiebreak` boolean
    - `server` 1 | 2 | null, nullable
    - `point_winner` 1 | 2 | null, nullable
    - `raw` string, nullable: The charter's shot string; both serves joined by ';' when the first was a fault.
    - `parsed` boolean
    - `serve_number` 1 | 2 | null, nullable
    - `serve_direction` 'wide' | 'body' | 'down_the_t' | 'null', nullable
    - `rally_length` integer, nullable: Strokes including the serve. An ace is 1, a double fault 0.
    - `outcome` 'winner' | 'forced_error' | 'unforced_error' | 'error' | 'other' | 'null', nullable: `error` = the charter recorded a miss without saying whether it was forced. Never guessed.
    - `error_location` 'net' | 'wide' | 'deep' | 'wide_and_deep' | 'null', nullable
    - `ending_stroke` string, nullable
    - `ending_wing` string, nullable
    - `is_ace` boolean
    - `is_double_fault` boolean
    - `is_serve_and_volley` boolean
    - `is_unreturned` boolean: Derived (added 2026-09-05). The server won the point without the returner putting the ball in play. A double fault is never unreturned.
    - `unreturned_kind` 'ace' | 'serve_winner' | 'serve_forced_error' | 'return_forced_error' | 'return_unforced_error' | 'return_error' | 'null', nullable: How the serve went unreturned. `serve_winner` is a serve winner charted without a direction code (so `is_ace` is unset) — count it with the aces. `return_error` = the return missed and the charter did not say whether it was forced. Null when the return was in play or the parse could not say.
    - `shots` RallyShot[]
      - `number` integer
      - `code` string: The charter's raw code, e.g. 'f'.
      - `stroke` 'serve' | 'groundstroke' | 'slice' | 'volley' | 'half_volley' | 'swinging_volley' | 'overhead' | 'drop_shot' | 'lob' | 'trick' | 'unknown' | 'null', nullable
      - `wing` 'forehand' | 'backhand' | 'null', nullable: The side it was struck FROM.
      - `direction` 'forehand_side' | 'middle' | 'backhand_side' | 'null', nullable: Where the ball was sent.
      - `depth` 'shallow' | 'mid' | 'deep' | 'null', nullable
      - `position` 'approaching' | 'at_net' | 'baseline' | 'null', nullable

## Other responses

- `401`: Missing, unknown, or disabled credentials
- `403`: Your tier doesn't unlock this endpoint
- `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.

---

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