# Table Tennis API complete reference

Live scores, results, events and players from professional table tennis (professional events since December 2024), as JSON over HTTPS. The whole documentation in one file.

- Base URL: `https://api.tabletennisapi.com/v1`
- Authentication: `Authorization: Bearer <key>` (or `X-API-Key: <key>`). Keys start with `tta_live_`. A free key: https://tabletennisapi.com/signup
- Site: https://tabletennisapi.com
- Endpoints: 15
- Generated: 9 October 2026 (the day its sources last changed)
- This file: https://tabletennisapi.com/documentation/manual.md
- Format: JSON, UTF-8. Every time is UTC; every `date` is the local day of play at the venue

## Before anything else

The API gives facts only: who played, when, where and the score. There are no odds, tips or predictions.

Not included yet:

- Seasons before December 2024.
- Point by point, match statistics and odds. These are planned; see "Planned endpoints" below. World rankings are held from the first week we read (October 2026) on; earlier weeks are not.
- National leagues and club competitions.

Every answer has one of two shapes. A successful call returns `data` (an object or a list) and `meta` (counts, paging and notes). An error returns `error` with a `code` to switch on, a `message` to show and a `docs` link. Nothing else is ever at the top level.

```json
{ "data": [ ... ], "meta": { "count": 50, "next_cursor": "eyJvIjo1MCwicCI6NTB9" } }
{ "error": { "code": "not_found", "message": "No match with that id.", "docs": "https://tabletennisapi.com/documentation/errors" } }
```

Ids are strings with a type prefix: `ev_` events, `mt_` matches, `pl_` players. They never change, so they are safe to store. The version in the address is optional: `/matches` is `/v1/matches`.

## Plans

| | Free key | Live | Archive | Complete |
| --- | --- | --- | --- | --- |
| Price | Free | $69 a month, $690 a year | $59 a month, $590 a year | $99 a month, $990 a year |
| Calls a day | 50 | 20,000 | 20,000 | 50,000 |
| Calls a minute | 10 | 60 | 60 | 120 |
| Dates it reads | Yesterday, today and the calendar ahead | 1 January this year onward, today and the calendar ahead | All history (since December 2024), up to yesterday | All history (since December 2024), today and the calendar ahead |
| Endpoints | 11 of 15 | All 15 | 14 of 15 | All 15 |
| Game scores, length, table | Null | Yes | Yes | Yes |

Yearly billing is ten months for twelve. `/v1/results/yesterday` works on every plan whatever its dates. Plans and prices: https://tabletennisapi.com/pricing

## Coverage

Counted from the data up to 9 October 2026, the day this file was generated:

- 83,747 official matches, 524 events and 8,448 players since December 2024
- Tier 1 (Grand Smash, Champions, Finals, World Cup): 57 events
- Tier 2 (Star Contender, Contender): 75 events
- Tier 3 (Feeder series): 89 events
- Tier 4 (youth): 273 events
- Tier 5 (para and other): 30 events
- The same figures, live: `GET /v1/events` and `GET /v1/matches` (their `meta.total`)

Disciplines: `MS` `WS` singles, `MD` `WD` `XD` doubles, `MT` `WT` `XT` team ties. Youth matches carry `age_group` (U11 to U19); para matches carry `para_class` (such as MS6).

## Endpoints

All 15 built endpoints. Each is a GET on `https://api.tabletennisapi.com` with your key in a header.

#### `GET /v1/usage`

Your plan, calls today, limits and the dates your plan reads. Not counted against your allowance.

Plans: Free key, Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/usage

Errors: `401 unauthorized`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is one Usage object (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/usage" -H "Authorization: Bearer $TT_KEY"
```

Sample response:

```json
{
    "data": {
        "plan": "Complete",
        "key_prefix": "tta_live_7k2m",
        "calls_today": 214,
        "calls_this_month": 5120,
        "limits": {
            "per_minute": 120,
            "per_day": 50000
        },
        "resets_at": "2026-09-13T00:00:00Z",
        "data_window": {
            "from": null,
            "to": "today and scheduled"
        }
    },
    "meta": []
}
```

#### `GET /v1/events`

Events: past, running and scheduled, newest first.

Plans: Free key, Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/events

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `from` | query | date | no | Events ending on or after this day (YYYY-MM-DD) | `2026-09-13` |
| `to` | query | date | no | Events starting on or before this day (YYYY-MM-DD) | `2026-09-13` |
| `q` | query | string | no | Words in the event name, such as "Champions" or "Doha" | `Harimoto` |
| `country` | query | string | no | Host country, three letters (CHN, QAT) | `CHN` |
| `tier` | query | int | no | 1 the top senior events, 2 other senior events, 3 feeders, 4 youth, 5 other | `1` |
| `per_page` | query | int | no | Rows a page, 1 to 200 (default 50) | `50` |
| `page` | query | int | no | Page number, from 1 | `2` |
| `cursor` | query | string | no | meta.next_cursor from the previous answer, as given |  |

Errors: `401 unauthorized`, `400 bad_request`, `403 upgrade_required`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is a list of Event objects (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/events" -H "Authorization: Bearer $TT_KEY"
```

Sample response (two rows shown):

```json
{
    "data": [
        {
            "id": "ev_c9k4098k",
            "name": "Contender Panagyurishte 2026 Presented by ASAREL",
            "start_date": "2026-09-08",
            "end_date": "2026-09-13",
            "venue": "Arena Asarel",
            "city": "Panagyurishte",
            "country": "BUL",
            "tier": 2,
            "utc_offset_min": 180,
            "matches": 225
        },
        {
            "id": "ev_w5hw10am",
            "name": "Champions Macao 2026 Presented by Galaxy Entertainment Group",
            "start_date": "2026-09-08",
            "end_date": "2026-09-13",
            "venue": "Macao East Asian Games Dome",
            "city": "Macao",
            "country": "MAC",
            "tier": 1,
            "utc_offset_min": 480,
            "matches": 62
        }
    ],
    "meta": {
        "count": 2,
        "next_cursor": null,
        "page": 1,
        "per_page": 2,
        "total": 2,
        "total_pages": 1,
        "next_page": null,
        "prev_page": null
    }
}
```

#### `GET /v1/events/{id}`

One event: name, dates, venue, and its disciplines with match counts.

Plans: Free key, Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/event

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `id` | path | id | yes | The event id | `ev_w5hw10am` |

Errors: `401 unauthorized`, `403 upgrade_required`, `404 not_found`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is one EventDetail object (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/events/ev_w5hw10am" -H "Authorization: Bearer $TT_KEY"
```

Sample response:

```json
{
    "data": {
        "id": "ev_w5hw10am",
        "name": "Champions Macao 2026 Presented by Galaxy Entertainment Group",
        "start_date": "2026-09-08",
        "end_date": "2026-09-13",
        "venue": "Macao East Asian Games Dome",
        "city": "Macao",
        "country": "MAC",
        "tier": 1,
        "utc_offset_min": 480,
        "matches": 62,
        "disciplines": [
            {
                "discipline": "MS",
                "age_group": null,
                "para_class": null,
                "matches": 31
            },
            {
                "discipline": "WS",
                "age_group": null,
                "para_class": null,
                "matches": 31
            }
        ]
    },
    "meta": []
}
```

#### `GET /v1/events/{id}/matches`

An event's matches, in order of play.

Plans: Free key, Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/event-matches

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `id` | path | id | yes | The event id | `ev_w5hw10am` |
| `discipline` | query | string | no | MS, WS, MD, WD, XD; teams MT, WT, XT | `MS` |
| `age_group` | query | string | no | Youth age group: U11, U13, U15, U17, U19. "senior" for seniors only | `U19` |
| `para_class` | query | string | no | Para sport class, such as MS6 | `MS6` |
| `round` | query | string | no | Round: final, semi_final, quarter_final, round_of_16 ... round_of_128, group, qualifying_1 to qualifying_4, qualifying_knockout or bronze | `final` |
| `status` | query | string | no | finished, live, scheduled or unconfirmed (played, no official result) | `finished` |
| `per_page` | query | int | no | Rows a page, 1 to 200 (default 50) | `50` |
| `page` | query | int | no | Page number, from 1 | `2` |
| `cursor` | query | string | no | meta.next_cursor from the previous answer, as given |  |

Errors: `401 unauthorized`, `400 bad_request`, `403 upgrade_required`, `404 not_found`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is a list of Match objects (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/events/ev_w5hw10am/matches" -H "Authorization: Bearer $TT_KEY"
```

Sample response (two rows shown):

```json
{
    "data": [
        {
            "id": "mt_3k4xxtgt",
            "event": {
                "id": "ev_w5hw10am",
                "name": "Champions Macao 2026 Presented by Galaxy Entertainment Group"
            },
            "date": "2026-09-08",
            "start_time": "2026-09-08T03:00:00Z",
            "discipline": "WS",
            "age_group": null,
            "para_class": null,
            "round": "round_of_32",
            "round_name": "Round of 32",
            "rubber": null,
            "status": "finished",
            "best_of": 5,
            "points_per_game": 11,
            "win_by": 2,
            "utc_offset_min": 480,
            "sides": [
                {
                    "side": "a",
                    "name": "Chen Yi",
                    "country": "CHN",
                    "players": [
                        {
                            "id": "pl_yp1ww51h",
                            "name": "Chen Yi",
                            "country": "CHN"
                        }
                    ]
                },
                {
                    "side": "b",
                    "name": "Yeh Yi-Tian",
                    "country": "TPE",
                    "players": [
                        {
                            "id": "pl_mpera4pm",
                            "name": "Yeh Yi-Tian",
                            "country": "TPE"
                        }
                    ]
                }
            ],
            "score": {
                "a": 3,
                "b": 0
            },
            "winner": "a",
            "result_note": null,
            "games": [
                {
                    "a": 11,
                    "b": 6
                },
                {
                    "a": 11,
                    "b": 8
                },
                {
                    "a": 11,
                    "b": 8
                }
            ],
            "duration_s": 1490,
            "table": 1,
            "started_at": null,
            "timeouts": {
                "a": 1,
                "b": 1
            },
            "cards": {
                "a": [],
                "b": []
            },
            "video_review": false,
            "challenges_each": 99,
            "serving": null
        },
        {
            "id": "mt_j4ymx6hd",
            "event": {
                "id": "ev_w5hw10am",
                "name": "Champions Macao 2026 Presented by Galaxy Entertainment Group"
            },
            "date": "2026-09-08",
            "start_time": "2026-09-08T03:35:00Z",
            "discipline": "MS",
            "age_group": null,
            "para_class": null,
            "round": "round_of_32",
            "round_name": "Round of 32",
            "rubber": null,
            "status": "finished",
            "best_of": 5,
            "points_per_game": 11,
            "win_by": 2,
            "utc_offset_min": 480,
            "sides": [
                {
                    "side": "a",
                    "name": "Geraldo Joao",
                    "country": "POR",
                    "players": [
                        {
                            "id": "pl_b4f3t8p6",
                            "name": "Geraldo Joao",
                            "country": "POR"
                        }
                    ]
                },
                {
                    "side": "b",
                    "name": "Lind Anders",
                    "country": "DEN",
                    "players": [
                        {
                            "id": "pl_z3jc3bsa",
                            "name": "Lind Anders",
                            "country": "DEN"
                        }
                    ]
                }
            ],
            "score": {
                "a": 0,
                "b": 3
            },
            "winner": "b",
            "result_note": null,
            "games": [
                {
                    "a": 5,
                    "b": 11
                },
                {
                    "a": 9,
                    "b": 11
                },
                {
                    "a": 9,
                    "b": 11
                }
            ],
            "duration_s": 1567,
            "table": 1,
            "started_at": null,
            "timeouts": {
                "a": 1,
                "b": 1
            },
            "cards": {
                "a": [],
                "b": [
                    "yellow"
                ]
            },
            "video_review": false,
            "challenges_each": 99,
            "serving": null
        }
    ],
    "meta": {
        "count": 2,
        "next_cursor": "eyJvIjoyLCJwIjoyfQ",
        "page": 1,
        "per_page": 2,
        "total": 62,
        "total_pages": 31,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/matches`

Matches on a day or across dates, every event.

Plans: Free key, Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/matches

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `date` | query | date | no | One day (YYYY-MM-DD), the local day of play. Default: today | `2026-09-13` |
| `from` | query | date | no | First day, with to | `2026-09-13` |
| `to` | query | date | no | Last day, with from (at most 31 days) | `2026-09-13` |
| `discipline` | query | string | no | MS, WS, MD, WD, XD; teams MT, WT, XT | `MS` |
| `age_group` | query | string | no | Youth age group: U11 to U19. "senior" for seniors only | `U19` |
| `tier` | query | int | no | Event tier, 1 to 5 | `1` |
| `status` | query | string | no | finished, live, scheduled or unconfirmed (played, no official result) | `finished` |
| `country` | query | string | no | Matches with a player from this country (three letters) | `CHN` |
| `per_page` | query | int | no | Rows a page, 1 to 200 (default 50) | `50` |
| `page` | query | int | no | Page number, from 1 | `2` |
| `cursor` | query | string | no | meta.next_cursor from the previous answer, as given |  |

Errors: `401 unauthorized`, `400 bad_request`, `403 upgrade_required`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is a list of Match objects (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/matches" -H "Authorization: Bearer $TT_KEY"
```

Sample response (two rows shown):

```json
{
    "data": [
        {
            "id": "mt_6twqf194",
            "event": {
                "id": "ev_w5hw10am",
                "name": "Champions Macao 2026 Presented by Galaxy Entertainment Group"
            },
            "date": "2026-09-13",
            "start_time": "2026-09-13T03:00:00Z",
            "discipline": "WS",
            "age_group": null,
            "para_class": null,
            "round": "semi_final",
            "round_name": "Semi-finals",
            "rubber": null,
            "status": "finished",
            "best_of": 7,
            "points_per_game": 11,
            "win_by": 2,
            "utc_offset_min": 480,
            "sides": [
                {
                    "side": "a",
                    "name": "Hayata Hina",
                    "country": "JPN",
                    "players": [
                        {
                            "id": "pl_awb8xnz2",
                            "name": "Hayata Hina",
                            "country": "JPN"
                        }
                    ]
                },
                {
                    "side": "b",
                    "name": "Chen Yi",
                    "country": "CHN",
                    "players": [
                        {
                            "id": "pl_yp1ww51h",
                            "name": "Chen Yi",
                            "country": "CHN"
                        }
                    ]
                }
            ],
            "score": {
                "a": 3,
                "b": 4
            },
            "winner": "b",
            "result_note": null,
            "games": [
                {
                    "a": 4,
                    "b": 11
                },
                {
                    "a": 6,
                    "b": 11
                },
                {
                    "a": 11,
                    "b": 8
                },
                {
                    "a": 8,
                    "b": 11
                },
                {
                    "a": 11,
                    "b": 8
                },
                {
                    "a": 18,
                    "b": 16
                },
                {
                    "a": 4,
                    "b": 11
                }
            ],
            "duration_s": 4067,
            "table": 1,
            "started_at": null,
            "timeouts": {
                "a": 1,
                "b": 1
            },
            "cards": {
                "a": [],
                "b": []
            },
            "video_review": false,
            "challenges_each": 99,
            "serving": null
        },
        {
            "id": "mt_4kejvgza",
            "event": {
                "id": "ev_w5hw10am",
                "name": "Champions Macao 2026 Presented by Galaxy Entertainment Group"
            },
            "date": "2026-09-13",
            "start_time": "2026-09-13T03:45:00Z",
            "discipline": "WS",
            "age_group": null,
            "para_class": null,
            "round": "semi_final",
            "round_name": "Semi-finals",
            "rubber": null,
            "status": "finished",
            "best_of": 7,
            "points_per_game": 11,
            "win_by": 2,
            "utc_offset_min": 480,
            "sides": [
                {
                    "side": "a",
                    "name": "Harimoto Miwa",
                    "country": "JPN",
                    "players": [
                        {
                            "id": "pl_51t63nz3",
                            "name": "Harimoto Miwa",
                            "country": "JPN"
                        }
                    ]
                },
                {
                    "side": "b",
                    "name": "Chen Xingtong",
                    "country": "CHN",
                    "players": [
                        {
                            "id": "pl_enrwxwqd",
                            "name": "Chen Xingtong",
                            "country": "CHN"
                        }
                    ]
                }
            ],
            "score": {
                "a": 4,
                "b": 1
            },
            "winner": "a",
            "result_note": null,
            "games": [
                {
                    "a": 11,
                    "b": 8
                },
                {
                    "a": 12,
                    "b": 10
                },
                {
                    "a": 6,
                    "b": 11
                },
                {
                    "a": 11,
                    "b": 9
                },
                {
                    "a": 12,
                    "b": 10
                }
            ],
            "duration_s": 2758,
            "table": 1,
            "started_at": null,
            "timeouts": {
                "a": 1,
                "b": 1
            },
            "cards": {
                "a": [],
                "b": []
            },
            "video_review": false,
            "challenges_each": 99,
            "serving": null
        }
    ],
    "meta": {
        "count": 2,
        "next_cursor": "eyJvIjoyLCJwIjoyfQ",
        "page": 1,
        "per_page": 2,
        "total": 10,
        "total_pages": 5,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/matches/{id}`

One match: players, score, every game, length, table, format, real start, timeouts and cards.

Plans: Free key, Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/match

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `id` | path | id | yes | The match id | `mt_fs5ttbe2` |

Errors: `401 unauthorized`, `403 upgrade_required`, `404 not_found`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is one Match object (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/matches/mt_fs5ttbe2" -H "Authorization: Bearer $TT_KEY"
```

Sample response:

```json
{
    "data": {
        "id": "mt_fs5ttbe2",
        "event": {
            "id": "ev_w5hw10am",
            "name": "Champions Macao 2026 Presented by Galaxy Entertainment Group"
        },
        "date": "2026-09-13",
        "start_time": "2026-09-13T10:30:00Z",
        "discipline": "WS",
        "age_group": null,
        "para_class": null,
        "round": "final",
        "round_name": "Final",
        "rubber": null,
        "status": "finished",
        "best_of": 7,
        "points_per_game": 11,
        "win_by": 2,
        "utc_offset_min": 480,
        "sides": [
            {
                "side": "a",
                "name": "Harimoto Miwa",
                "country": "JPN",
                "players": [
                    {
                        "id": "pl_51t63nz3",
                        "name": "Harimoto Miwa",
                        "country": "JPN"
                    }
                ]
            },
            {
                "side": "b",
                "name": "Chen Yi",
                "country": "CHN",
                "players": [
                    {
                        "id": "pl_yp1ww51h",
                        "name": "Chen Yi",
                        "country": "CHN"
                    }
                ]
            }
        ],
        "score": {
            "a": 4,
            "b": 3
        },
        "winner": "a",
        "result_note": null,
        "games": [
            {
                "a": 11,
                "b": 7
            },
            {
                "a": 11,
                "b": 7
            },
            {
                "a": 9,
                "b": 11
            },
            {
                "a": 8,
                "b": 11
            },
            {
                "a": 11,
                "b": 13
            },
            {
                "a": 11,
                "b": 9
            },
            {
                "a": 12,
                "b": 10
            }
        ],
        "duration_s": 4103,
        "table": 1,
        "started_at": null,
        "timeouts": {
            "a": 1,
            "b": 1
        },
        "cards": {
            "a": [],
            "b": []
        },
        "video_review": false,
        "challenges_each": 99,
        "serving": null
    },
    "meta": []
}
```

#### `GET /v1/matches/{id}/points`

Point by point: every score in every game as our live tracking saw it, about every 10 seconds, with who served. For matches followed live from October 2026.

Plans: Free key, Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/match-points

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `id` | path | id | yes | The match id | `mt_fs5ttbe2` |

Errors: `401 unauthorized`, `403 upgrade_required`, `404 not_found`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is a list of PointStep objects (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/matches/mt_fs5ttbe2/points" -H "Authorization: Bearer $TT_KEY"
```

Sample response:

```json
{
    "data": [],
    "meta": {
        "count": 0,
        "followed_live": false,
        "poll_seconds": 10
    }
}
```

#### `GET /v1/live` (Live, Complete only)

Matches being played now, with the game in progress. Refreshed about every 10 seconds.

Plans: Live, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/live

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `discipline` | query | string | no | MS, WS, MD, WD, XD; teams MT, WT, XT | `MS` |

Errors: `401 unauthorized`, `400 bad_request`, `403 upgrade_required`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is a list of Match objects (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/live" -H "Authorization: Bearer $TT_KEY"
```

Answers with the same Match objects as `GET /v1/matches`; its answer changes from minute to minute, so no sample is fixed here.

#### `GET /v1/results/yesterday`

Yesterday's finished matches, every event. On every plan.

Plans: Free key, Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/results-yesterday

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `discipline` | query | string | no | MS, WS, MD, WD, XD; teams MT, WT, XT | `MS` |
| `per_page` | query | int | no | Rows a page, 1 to 200 (default 50) | `50` |
| `page` | query | int | no | Page number, from 1 | `2` |
| `cursor` | query | string | no | meta.next_cursor from the previous answer, as given |  |

Errors: `401 unauthorized`, `400 bad_request`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is a list of Match objects (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/results/yesterday" -H "Authorization: Bearer $TT_KEY"
```

Answers with the same Match objects as `GET /v1/matches`; its answer changes from minute to minute, so no sample is fixed here.

#### `GET /v1/players`

Find players by name or country.

Plans: Free key, Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/players

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `q` | query | string | no | Part of the name, at least 2 letters | `Harimoto` |
| `country` | query | string | no | Three letters (CHN, JPN) | `CHN` |
| `per_page` | query | int | no | Rows a page, 1 to 200 (default 50) | `50` |
| `page` | query | int | no | Page number, from 1 | `2` |
| `cursor` | query | string | no | meta.next_cursor from the previous answer, as given |  |

Errors: `401 unauthorized`, `400 bad_request`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is a list of Player objects (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/players" -H "Authorization: Bearer $TT_KEY"
```

Sample response (two rows shown):

```json
{
    "data": [
        {
            "id": "pl_51t63nz3",
            "name": "Harimoto Miwa",
            "country": "JPN"
        },
        {
            "id": "pl_mc5w19rn",
            "name": "Harimoto Tomokazu",
            "country": "JPN"
        }
    ],
    "meta": {
        "count": 2,
        "next_cursor": null,
        "page": 1,
        "per_page": 2,
        "total": 2,
        "total_pages": 1,
        "next_page": null,
        "prev_page": null
    }
}
```

#### `GET /v1/players/{id}`

One player: name, country and their record by discipline.

Plans: Free key, Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/player

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `id` | path | id | yes | The player id | `pl_51t63nz3` |

Errors: `401 unauthorized`, `403 upgrade_required`, `404 not_found`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is one PlayerDetail object (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/players/pl_51t63nz3" -H "Authorization: Bearer $TT_KEY"
```

Sample response:

```json
{
    "data": {
        "id": "pl_51t63nz3",
        "name": "Harimoto Miwa",
        "country": "JPN",
        "born": "2008-06-16",
        "hand": "right",
        "height_cm": null,
        "record": [
            {
                "discipline": "WS",
                "played": 119,
                "won": 97,
                "lost": 22,
                "first": "2025-01-09",
                "last": "2026-10-09"
            },
            {
                "discipline": "WD",
                "played": 59,
                "won": 53,
                "lost": 6,
                "first": "2025-01-09",
                "last": "2026-10-09"
            },
            {
                "discipline": "XD",
                "played": 41,
                "won": 27,
                "lost": 14,
                "first": "2025-01-07",
                "last": "2026-10-08"
            }
        ],
        "world_ranks": [
            {
                "discipline": "WS",
                "doubles": null,
                "list": "senior",
                "rank": 3,
                "points": 6709,
                "week": "2026-41"
            },
            {
                "discipline": "WD",
                "doubles": "pair",
                "list": "senior",
                "rank": 1,
                "points": 6350,
                "week": "2026-41"
            },
            {
                "discipline": "WD",
                "doubles": "pair",
                "list": "senior",
                "rank": 44,
                "points": 400,
                "week": "2026-41"
            },
            {
                "discipline": "WD",
                "doubles": "player",
                "list": "senior",
                "rank": 2,
                "points": 6755,
                "week": "2026-41"
            },
            {
                "discipline": "XD",
                "doubles": "pair",
                "list": "senior",
                "rank": 10,
                "points": 1415,
                "week": "2026-41"
            },
            {
                "discipline": "XD",
                "doubles": "player",
                "list": "senior",
                "rank": 24,
                "points": 1415,
                "week": "2026-41"
            },
            {
                "discipline": "WS",
                "doubles": null,
                "list": "youth",
                "rank": 1,
                "points": 38445,
                "week": "2026-41"
            },
            {
                "discipline": "WD",
                "doubles": "player",
                "list": "youth",
                "rank": 1,
                "points": 33775,
                "week": "2026-41"
            },
            {
                "discipline": "XD",
                "doubles": "player",
                "list": "youth",
                "rank": 5,
                "points": 7075,
                "week": "2026-41"
            }
        ]
    },
    "meta": []
}
```

#### `GET /v1/players/{id}/matches` (Live, Archive, Complete only)

A player's matches, newest first, singles and doubles.

Plans: Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/player-matches

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `id` | path | id | yes | The player id | `pl_51t63nz3` |
| `from` | query | date | no | First day (YYYY-MM-DD) | `2026-09-13` |
| `to` | query | date | no | Last day (YYYY-MM-DD) | `2026-09-13` |
| `discipline` | query | string | no | MS, WS, MD, WD, XD; teams MT, WT, XT | `MS` |
| `per_page` | query | int | no | Rows a page, 1 to 200 (default 50) | `50` |
| `page` | query | int | no | Page number, from 1 | `2` |
| `cursor` | query | string | no | meta.next_cursor from the previous answer, as given |  |

Errors: `401 unauthorized`, `400 bad_request`, `403 upgrade_required`, `404 not_found`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is a list of Match objects (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/players/pl_51t63nz3/matches" -H "Authorization: Bearer $TT_KEY"
```

Sample response (two rows shown):

```json
{
    "data": [
        {
            "id": "mt_fs5ttbe2",
            "event": {
                "id": "ev_w5hw10am",
                "name": "Champions Macao 2026 Presented by Galaxy Entertainment Group"
            },
            "date": "2026-09-13",
            "start_time": "2026-09-13T10:30:00Z",
            "discipline": "WS",
            "age_group": null,
            "para_class": null,
            "round": "final",
            "round_name": "Final",
            "rubber": null,
            "status": "finished",
            "best_of": 7,
            "points_per_game": 11,
            "win_by": 2,
            "utc_offset_min": 480,
            "sides": [
                {
                    "side": "a",
                    "name": "Harimoto Miwa",
                    "country": "JPN",
                    "players": [
                        {
                            "id": "pl_51t63nz3",
                            "name": "Harimoto Miwa",
                            "country": "JPN"
                        }
                    ]
                },
                {
                    "side": "b",
                    "name": "Chen Yi",
                    "country": "CHN",
                    "players": [
                        {
                            "id": "pl_yp1ww51h",
                            "name": "Chen Yi",
                            "country": "CHN"
                        }
                    ]
                }
            ],
            "score": {
                "a": 4,
                "b": 3
            },
            "winner": "a",
            "result_note": null,
            "games": [
                {
                    "a": 11,
                    "b": 7
                },
                {
                    "a": 11,
                    "b": 7
                },
                {
                    "a": 9,
                    "b": 11
                },
                {
                    "a": 8,
                    "b": 11
                },
                {
                    "a": 11,
                    "b": 13
                },
                {
                    "a": 11,
                    "b": 9
                },
                {
                    "a": 12,
                    "b": 10
                }
            ],
            "duration_s": 4103,
            "table": 1,
            "started_at": null,
            "timeouts": {
                "a": 1,
                "b": 1
            },
            "cards": {
                "a": [],
                "b": []
            },
            "video_review": false,
            "challenges_each": 99,
            "serving": null
        },
        {
            "id": "mt_4kejvgza",
            "event": {
                "id": "ev_w5hw10am",
                "name": "Champions Macao 2026 Presented by Galaxy Entertainment Group"
            },
            "date": "2026-09-13",
            "start_time": "2026-09-13T03:45:00Z",
            "discipline": "WS",
            "age_group": null,
            "para_class": null,
            "round": "semi_final",
            "round_name": "Semi-finals",
            "rubber": null,
            "status": "finished",
            "best_of": 7,
            "points_per_game": 11,
            "win_by": 2,
            "utc_offset_min": 480,
            "sides": [
                {
                    "side": "a",
                    "name": "Harimoto Miwa",
                    "country": "JPN",
                    "players": [
                        {
                            "id": "pl_51t63nz3",
                            "name": "Harimoto Miwa",
                            "country": "JPN"
                        }
                    ]
                },
                {
                    "side": "b",
                    "name": "Chen Xingtong",
                    "country": "CHN",
                    "players": [
                        {
                            "id": "pl_enrwxwqd",
                            "name": "Chen Xingtong",
                            "country": "CHN"
                        }
                    ]
                }
            ],
            "score": {
                "a": 4,
                "b": 1
            },
            "winner": "a",
            "result_note": null,
            "games": [
                {
                    "a": 11,
                    "b": 8
                },
                {
                    "a": 12,
                    "b": 10
                },
                {
                    "a": 6,
                    "b": 11
                },
                {
                    "a": 11,
                    "b": 9
                },
                {
                    "a": 12,
                    "b": 10
                }
            ],
            "duration_s": 2758,
            "table": 1,
            "started_at": null,
            "timeouts": {
                "a": 1,
                "b": 1
            },
            "cards": {
                "a": [],
                "b": []
            },
            "video_review": false,
            "challenges_each": 99,
            "serving": null
        }
    ],
    "meta": {
        "count": 2,
        "next_cursor": "eyJvIjoyLCJwIjoyfQ",
        "page": 1,
        "per_page": 2,
        "total": 210,
        "total_pages": 105,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/h2h/{a}/{b}` (Live, Archive, Complete only)

Two players against each other: wins, games, and every meeting.

Plans: Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/h2h

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `a` | path | id | yes | The first player id | `pl_51t63nz3` |
| `b` | path | id | yes | The second player id | `pl_yp1ww51h` |
| `per_page` | query | int | no | Rows a page, 1 to 200 (default 50) | `50` |
| `page` | query | int | no | Page number, from 1 | `2` |
| `cursor` | query | string | no | meta.next_cursor from the previous answer, as given |  |

Errors: `401 unauthorized`, `400 bad_request`, `403 upgrade_required`, `404 not_found`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is a list of Match objects (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/h2h/pl_51t63nz3/pl_yp1ww51h" -H "Authorization: Bearer $TT_KEY"
```

Sample response (two rows shown):

```json
{
    "data": [
        {
            "id": "mt_y4m2qe95",
            "event": {
                "id": "ev_5mgjtt6p",
                "name": "China Smash 2026 Presented by Beijing Shijingshan Culture & Tourism Group"
            },
            "date": "2026-10-07",
            "start_time": "2026-10-07T12:55:00Z",
            "discipline": "XD",
            "age_group": null,
            "para_class": null,
            "round": "quarter_final",
            "round_name": "Quarter-finals",
            "rubber": null,
            "status": "finished",
            "best_of": 5,
            "points_per_game": 11,
            "win_by": 2,
            "utc_offset_min": 480,
            "sides": [
                {
                    "side": "a",
                    "name": "Huang Youzheng/Chen Yi",
                    "country": "CHN",
                    "players": [
                        {
                            "id": "pl_j4gj3bdr",
                            "name": "Huang Youzheng",
                            "country": "CHN"
                        },
                        {
                            "id": "pl_yp1ww51h",
                            "name": "Chen Yi",
                            "country": "CHN"
                        }
                    ]
                },
                {
                    "side": "b",
                    "name": "Matsushima Sora/Harimoto Miwa",
                    "country": "JPN",
                    "players": [
                        {
                            "id": "pl_5p9a00yg",
                            "name": "Matsushima Sora",
                            "country": "JPN"
                        },
                        {
                            "id": "pl_51t63nz3",
                            "name": "Harimoto Miwa",
                            "country": "JPN"
                        }
                    ]
                }
            ],
            "score": {
                "a": 2,
                "b": 3
            },
            "winner": "b",
            "result_note": null,
            "games": [
                {
                    "a": 11,
                    "b": 5
                },
                {
                    "a": 9,
                    "b": 11
                },
                {
                    "a": 11,
                    "b": 9
                },
                {
                    "a": 5,
                    "b": 11
                },
                {
                    "a": 5,
                    "b": 11
                }
            ],
            "duration_s": 2145,
            "table": 1,
            "started_at": "2026-10-07T13:42:25Z",
            "timeouts": {
                "a": 1,
                "b": 1
            },
            "cards": {
                "a": [],
                "b": []
            },
            "video_review": true,
            "challenges_each": 2,
            "serving": null
        },
        {
            "id": "mt_fs5ttbe2",
            "event": {
                "id": "ev_w5hw10am",
                "name": "Champions Macao 2026 Presented by Galaxy Entertainment Group"
            },
            "date": "2026-09-13",
            "start_time": "2026-09-13T10:30:00Z",
            "discipline": "WS",
            "age_group": null,
            "para_class": null,
            "round": "final",
            "round_name": "Final",
            "rubber": null,
            "status": "finished",
            "best_of": 7,
            "points_per_game": 11,
            "win_by": 2,
            "utc_offset_min": 480,
            "sides": [
                {
                    "side": "a",
                    "name": "Harimoto Miwa",
                    "country": "JPN",
                    "players": [
                        {
                            "id": "pl_51t63nz3",
                            "name": "Harimoto Miwa",
                            "country": "JPN"
                        }
                    ]
                },
                {
                    "side": "b",
                    "name": "Chen Yi",
                    "country": "CHN",
                    "players": [
                        {
                            "id": "pl_yp1ww51h",
                            "name": "Chen Yi",
                            "country": "CHN"
                        }
                    ]
                }
            ],
            "score": {
                "a": 4,
                "b": 3
            },
            "winner": "a",
            "result_note": null,
            "games": [
                {
                    "a": 11,
                    "b": 7
                },
                {
                    "a": 11,
                    "b": 7
                },
                {
                    "a": 9,
                    "b": 11
                },
                {
                    "a": 8,
                    "b": 11
                },
                {
                    "a": 11,
                    "b": 13
                },
                {
                    "a": 11,
                    "b": 9
                },
                {
                    "a": 12,
                    "b": 10
                }
            ],
            "duration_s": 4103,
            "table": 1,
            "started_at": null,
            "timeouts": {
                "a": 1,
                "b": 1
            },
            "cards": {
                "a": [],
                "b": []
            },
            "video_review": false,
            "challenges_each": 99,
            "serving": null
        }
    ],
    "meta": {
        "count": 2,
        "next_cursor": "eyJvIjoyLCJwIjoyfQ",
        "page": 1,
        "per_page": 2,
        "total": 13,
        "total_pages": 7,
        "next_page": 2,
        "prev_page": null,
        "summary": {
            "played": 13,
            "wins_a": 11,
            "wins_b": 2,
            "player_a": "pl_51t63nz3",
            "player_b": "pl_yp1ww51h"
        }
    }
}
```

#### `GET /v1/rankings`

World rankings: the top 100 of a list for a week, with points and movement. The latest week unless you ask for one.

Plans: Free key, Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/rankings

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `discipline` | query | string | no | MS, WS, MD, WD or XD | `MS` |
| `doubles` | query | string | no | For MD, WD and XD: pair (pairs ranked together) or player (each player ranked on their own doubles results) | `pair` |
| `list` | query | string | no | senior or youth | `senior` |
| `week` | query | string | no | A past week, such as 2026-41 (year and week). Weeks are held from the first week we read on | `2026-41` |
| `country` | query | string | no | Only players from this country (three letters); keeps their world rank | `CHN` |

Errors: `401 unauthorized`, `400 bad_request`, `404 not_found`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is a list of Ranking objects (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/rankings" -H "Authorization: Bearer $TT_KEY"
```

Sample response (two rows shown):

```json
{
    "data": [
        {
            "rank": 1,
            "previous_rank": 3,
            "movement": 2,
            "points": 6930,
            "player": {
                "id": "pl_5p9a00yg",
                "name": "Matsushima Sora",
                "country": "JPN"
            },
            "partner": null,
            "age_group": "U19"
        },
        {
            "rank": 2,
            "previous_rank": 2,
            "movement": 0,
            "points": 6379,
            "player": {
                "id": "pl_3cg5pd0f",
                "name": "Lebrun Felix",
                "country": "FRA"
            },
            "partner": null,
            "age_group": null
        }
    ],
    "meta": {
        "week": "2026-41",
        "published": "2026-10-05",
        "discipline": "MS",
        "doubles": null,
        "list": "senior",
        "count": 100
    }
}
```

#### `GET /v1/players/{id}/rankings` (Live, Archive, Complete only)

A player's world ranking week by week, on every list they are in, newest first.

Plans: Live, Archive, Complete. Documentation page: https://tabletennisapi.com/documentation/endpoints/player-rankings

| Parameter | In | Type | Required | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `id` | path | id | yes | The player id | `pl_51t63nz3` |
| `discipline` | query | string | no | MS, WS, MD, WD or XD | `MS` |
| `doubles` | query | string | no | For MD, WD and XD: pair or player | `pair` |
| `from` | query | date | no | Weeks published on or after this day (YYYY-MM-DD) | `2026-09-13` |
| `to` | query | date | no | Weeks published on or before this day (YYYY-MM-DD) | `2026-09-13` |
| `per_page` | query | int | no | Rows a page, 1 to 200 (default 50) | `50` |
| `page` | query | int | no | Page number, from 1 | `2` |
| `cursor` | query | string | no | meta.next_cursor from the previous answer, as given |  |

Errors: `401 unauthorized`, `400 bad_request`, `403 upgrade_required`, `404 not_found`, `422 unknown_parameter`, `429 rate_limited`, `429 quota_exceeded`.

`data` is a list of PlayerRanking objects (see Fields).

```bash
curl "https://api.tabletennisapi.com/v1/players/pl_51t63nz3/rankings" -H "Authorization: Bearer $TT_KEY"
```

Sample response (two rows shown):

```json
{
    "data": [
        {
            "week": "2026-41",
            "published": "2026-10-05",
            "discipline": "WD",
            "doubles": "pair",
            "list": "senior",
            "rank": 1,
            "previous_rank": 1,
            "movement": 0,
            "points": 6350,
            "partner": {
                "id": "pl_awb8xnz2",
                "name": "Hayata Hina",
                "country": "JPN"
            }
        },
        {
            "week": "2026-41",
            "published": "2026-10-05",
            "discipline": "WD",
            "doubles": "pair",
            "list": "senior",
            "rank": 44,
            "previous_rank": 45,
            "movement": 1,
            "points": 400,
            "partner": {
                "id": "pl_0e2p4z4v",
                "name": "Odo Satsuki",
                "country": "JPN"
            }
        }
    ],
    "meta": {
        "count": 2,
        "next_cursor": "eyJvIjoyLCJwIjoyfQ",
        "page": 1,
        "per_page": 2,
        "total": 9,
        "total_pages": 5,
        "next_page": 2,
        "prev_page": null
    }
}
```

## Fields

Every field an answer can hold. A field is always present; when there is no value it is `null`, never missing.

### Player

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `id` | string | no | Player id, such as pl_thrm642n. Stable: store it |
| `name` | string | no | Name, family name first where that is the custom |
| `country` | string | can be | Association, three letters (CHN, JPN, SWE) |

### PlayerDetail

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `id` | string | no | Player id, such as pl_thrm642n. Stable: store it |
| `name` | string | no | Name, family name first where that is the custom |
| `country` | string | can be | Association, three letters (CHN, JPN, SWE) |
| `born` | string | can be | Date of birth, YYYY-MM-DD, or YYYY when only the year is recorded, where it is publicly recorded (mostly senior players). Never given for anyone under 18; otherwise null |
| `hand` | string | can be | Playing hand: right, left or both, where it is publicly recorded; otherwise null |
| `height_cm` | integer | can be | Height in centimetres, where it is publicly recorded; otherwise null |
| `record` | list of Record | no | Finished matches by discipline, over the dates your plan reads |
| `world_ranks` | list of WorldRank | no | World ranks this week, on every list the player is in (empty when unranked). Week by week: GET /v1/players/{id}/rankings |

### WorldRank

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `discipline` | string | no | MS, WS, MD, WD or XD |
| `doubles` | string | can be | For doubles: pair or player; null for singles |
| `list` | string | no | senior or youth |
| `rank` | integer | no | World rank |
| `points` | integer | no | Ranking points |
| `week` | string | no | Year and week of the list, such as 2026-41 |

### Record

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `discipline` | string | no | MS, WS, MD, WD, XD, MT, WT or XT |
| `played` | integer | no | Finished matches |
| `won` | integer | no | Won |
| `lost` | integer | no | Lost (draws, matches not played and matches stopped count in played only) |
| `first` | string | no | First match day, YYYY-MM-DD |
| `last` | string | no | Latest match day, YYYY-MM-DD |

### Ranking

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `rank` | integer | no | World rank on the list that week |
| `previous_rank` | integer | can be | Rank the week before, null when not ranked then |
| `movement` | integer | can be | Places climbed since the week before (negative when fallen), null when not ranked then |
| `points` | integer | no | Ranking points |
| `player` | Player | no | The player; on the pair lists, the first of the pair |
| `partner` | Player | can be | The second player on the pair lists, otherwise null |
| `age_group` | string | can be | The player's youth age group (U19, U17, U15, U13) where they are one, otherwise null |

### PlayerRanking

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `week` | string | no | Year and week of the list, such as 2026-41 |
| `published` | string | no | Day the list was published, YYYY-MM-DD |
| `discipline` | string | no | MS, WS, MD, WD or XD |
| `doubles` | string | can be | For doubles: pair (ranked as a pair) or player (ranked on their own doubles results); null for singles |
| `list` | string | no | senior or youth |
| `rank` | integer | no | World rank that week |
| `previous_rank` | integer | can be | Rank the week before |
| `movement` | integer | can be | Places climbed since the week before (negative when fallen) |
| `points` | integer | no | Ranking points |
| `partner` | Player | can be | The partner on a pair list, otherwise null |

### Event

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `id` | string | no | Event id, such as ev_qdy6dadc. Stable: store it |
| `name` | string | no | Event name |
| `start_date` | string | can be | First day, YYYY-MM-DD |
| `end_date` | string | can be | Last day, YYYY-MM-DD |
| `venue` | string | can be | Venue name |
| `city` | string | can be | Host city |
| `country` | string | can be | Host country, three letters |
| `tier` | integer | no | 1 top senior events (Grand Smash, Champions, Finals, World Cup), 2 other senior (Star Contender, Contender), 3 feeders, 4 youth, 5 para and other |
| `utc_offset_min` | integer | can be | The venue's offset from UTC in minutes on its first day (480 for UTC+8): from its matches, or from the host country's clock where the whole country keeps one |
| `matches` | integer | no | Matches held at the event, in every discipline |

### EventDetail

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `id` | string | no | Event id, such as ev_qdy6dadc. Stable: store it |
| `name` | string | no | Event name |
| `start_date` | string | can be | First day, YYYY-MM-DD |
| `end_date` | string | can be | Last day, YYYY-MM-DD |
| `venue` | string | can be | Venue name |
| `city` | string | can be | Host city |
| `country` | string | can be | Host country, three letters |
| `tier` | integer | no | 1 top senior events (Grand Smash, Champions, Finals, World Cup), 2 other senior (Star Contender, Contender), 3 feeders, 4 youth, 5 para and other |
| `utc_offset_min` | integer | can be | The venue's offset from UTC in minutes on its first day (480 for UTC+8): from its matches, or from the host country's clock where the whole country keeps one |
| `matches` | integer | no | Matches held at the event, in every discipline |
| `disciplines` | list of Discipline | no | Every discipline held, with its match count |

### Discipline

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `discipline` | string | no | MS, WS, MD, WD, XD, MT, WT or XT |
| `age_group` | string | can be | U11 to U19 for youth, null for seniors |
| `para_class` | string | can be | Para sport class, such as MS6 |
| `matches` | integer | no | Matches in it |

### Match

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `id` | string | no | Match id, such as mt_qrvrky4e. Stable: store it |
| `event` | EventRef | no | The event it belongs to |
| `date` | string | can be | Local day of play at the venue, YYYY-MM-DD |
| `start_time` | string | can be | Start in UTC, such as 2026-03-14T09:40:00Z |
| `discipline` | string | no | MS, WS, MD, WD, XD singles and doubles; MT, WT, XT team ties |
| `age_group` | string | can be | U11 to U19 for youth matches, null for seniors |
| `para_class` | string | can be | Para sport class, such as MS6 |
| `round` | string | no | final, semi_final, quarter_final, round_of_16, round_of_32, round_of_64, round_of_128, group, qualifying_1 to qualifying_4, qualifying_knockout, bronze, or other |
| `round_name` | string | no | Round in words, such as Final, Round of 16 or Group 7 |
| `rubber` | integer | can be | In a team tie, which match of the tie this is: 1, 2, 3 ...; null otherwise |
| `status` | string | no | finished (official result), live (in play now), scheduled, or unconfirmed (played, no official result published) |
| `best_of` | integer | can be | Games needed to decide it: 5 or 7 |
| `points_per_game` | integer | can be | Points that win a game, usually 11 |
| `win_by` | integer | can be | Lead a game must be won by, usually 2 |
| `utc_offset_min` | integer | can be | The venue's offset from UTC in minutes at the match (480 for UTC+8), so start_time and started_at can be shown in venue time |
| `sides` | list of Side | no | Side a and side b, always in that order |
| `score` | Score | no | Games won by each side |
| `winner` | string | can be | a or b once finished, otherwise null |
| `result_note` | string | can be | When the match has no ordinary result: walkover, retired, disqualified, not played (no game started), draw (a fixed number of games, level) or stopped (a game left unfinished); otherwise null |
| `games` | list of Game | can be | Points in every game, in order. Null on the free key |
| `duration_s` | integer | can be | Length in seconds: from the match card, or measured by our live tracking (to about 10 seconds) for a match followed from its start when the card records none. Null when neither has it, and on the free key |
| `table` | integer | can be | Number of the table it was played on. Null on the free key |
| `started_at` | string | can be | When the first point was played, in UTC (start_time is the scheduled start). Where the match card records it; null otherwise |
| `timeouts` | Calls | can be | Timeouts taken by each side: 0 or 1. Null when not recorded |
| `cards` | Cards | no | Cards shown to each side: yellow, yellow-red, yellow-red 2; empty lists when none |
| `video_review` | boolean | can be | Whether video review was in use for the match |
| `challenges_each` | integer | can be | Video review challenges each side may make, where review was in use |
| `serving` | string | can be | While live: a or b, who served the latest point. Null otherwise |

### Calls

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `a` | integer | no | Side a |
| `b` | integer | no | Side b |

### Cards

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `a` | list of string | no | Side a, in the order shown |
| `b` | list of string | no | Side b, in the order shown |

### PointStep

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `game` | integer | no | Game number, from 1 |
| `a` | integer | no | Points side a had in that game at this moment |
| `b` | integer | no | Points side b had in that game at this moment |
| `server` | string | can be | a or b: who served the point that made this score |
| `seen_at` | string | no | When our live tracking first saw this score, in UTC. It looks about every 10 seconds, so two points close together can show as one step of two |

### EventRef

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `id` | string | no | Event id |
| `name` | string | no | Event name |

### Side

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `side` | string | no | a or b |
| `name` | string | no | Player, pair or team name |
| `country` | string | can be | Three letters; null for mixed pairs from two associations |
| `players` | list of Player | no | One player in singles, two in doubles, the squad in team ties |

### Score

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `a` | integer | can be | Games won by side a |
| `b` | integer | can be | Games won by side b |

### Game

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `a` | integer | no | Points won by side a |
| `b` | integer | no | Points won by side b |

### Usage

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `plan` | string | no | Your plan |
| `key_prefix` | string | no | The start of the key that made the call |
| `calls_today` | integer | no | Calls counted today (UTC), across all your keys: they share one allowance |
| `calls_this_month` | integer | no | Calls counted this month (UTC), across all your keys |
| `limits` | Limits | no | Your limits |
| `resets_at` | string | no | When the daily allowance resets, UTC |
| `data_window` | DataWindow | no | The days your plan reads, by local day of play |

### Limits

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `per_minute` | integer | no | Calls a minute |
| `per_day` | integer | no | Calls a day |

### DataWindow

| Field | Type | Null | What it is |
| --- | --- | --- | --- |
| `from` | string | can be | First day, YYYY-MM-DD; null for all history |
| `to` | string | no | Last day, YYYY-MM-DD, or "today and scheduled" |

### meta

| Field | What it is |
| --- | --- |
| `count` | Rows in this answer |
| `next_cursor` | Pass back as cursor for the next rows; null when there are no more |
| `page` | This page (when page was sent, or on a first call) |
| `per_page` | Rows a page, 1 to 200 (default 50) |
| `total` | Rows in the whole list |
| `total_pages` | Pages in the whole list |
| `next_page` | The next page number, or null |
| `prev_page` | The previous page number, or null |
| `note` | A plain message, such as which fields your plan leaves null |
| `seconds_behind` | /v1/live only: seconds since the live data last refreshed |
| `last_change` | /v1/live only: when the live data last refreshed, UTC |
| `poll_seconds` | /v1/live only: how often it refreshes |
| `summary` | /v1/h2h only: played, wins_a, wins_b, player_a, player_b over every meeting your plan reads |

## Errors

| HTTP | Code | Meaning |
| --- | --- | --- |
| 400 | `bad_request` | A value is malformed, a required filter is missing, or the key was put in the address |
| 401 | `unauthorized` | No key, or a key that is not recognised or has been revoked |
| 403 | `upgrade_required` | The endpoint or the dates are outside your plan; the message names the plans that include it, and pricing links to them |
| 404 | `not_found` | No such endpoint, or no match, event or player with that id |
| 405 | `bad_request` | The API answers GET only |
| 422 | `unknown_parameter` | A filter name the endpoint does not take; the message lists the ones it does |
| 429 | `rate_limited` | Too many calls this minute; wait for Retry-After |
| 429 | `quota_exceeded` | Today's allowance is used; it resets at 00:00 UTC (resets_at) |
| 500 | `server_error` | Our fault, already logged; try again shortly |

## Guides

### Getting started

From no key to your first result in about two minutes. Page: https://tabletennisapi.com/documentation/getting-started

1. **Get a key.** Enter your email on the sign-up page. The key arrives straight away and starts with `tta_live_`. It is shown once, so copy it somewhere safe.
2. **Make a call.** Ask for yesterday's results, which every plan can read.
3. **Read the answer.** Loop over `data`; each item is a match with its event, round, both sides and the score.

```bash
curl https://api.tabletennisapi.com/v1/results/yesterday \
  -H "Authorization: Bearer $TT_KEY"
```

```python
import requests
res = requests.get("https://api.tabletennisapi.com/v1/results/yesterday",
    headers={"Authorization": f"Bearer {key}"}).json()
for m in res["data"]:
    print(m["sides"][0]["name"], m["score"]["a"], "-", m["score"]["b"], m["sides"][1]["name"])
```

#### Check your key

`/v1/usage` tells you your plan, calls today, your limits and the dates your plan reads. It never counts against your allowance, so it is the safe first call from any new program.

> Keep the key on your server. Code that runs in a visitor's browser shows its key to anyone who looks.

### Authentication

Every call carries your key in a header. Keys in the address are refused. Page: https://tabletennisapi.com/documentation/authentication

#### Two headers, either works

```bash
curl https://api.tabletennisapi.com/v1/usage -H "Authorization: Bearer tta_live_..."
curl https://api.tabletennisapi.com/v1/usage -H "X-API-Key: tta_live_..."
```

#### Why not in the address

Addresses end up in server logs, browser history and analytics. A call with `?key=` or `?api_key=` is refused with `400 bad_request` so a key never leaks that way by accident.

#### Missing or revoked keys

A call without a key, or with a revoked one, gets `401 unauthorized`. Make a new key in your dashboard; revoking the old one takes effect at once.

#### One key, any plan

Your key stays the same when you change plan. The plan decides what it can read, not the key.

### Batch requests (planned, not built yet)

Several calls in one request, answered together. Page: https://tabletennisapi.com/documentation/batch-requests

> Planned, not built yet
> 
> This guide describes how it will work so you can plan around it. Nothing on this page can be called today.
> 
> - Needs: a batch runner on the API host
> - For: Live, Archive and Complete
> 
> How it will work: send up to 20 calls as a list; each one is answered in the same order, and each counts as one call against your allowance.
> 
> ```bash
> curl -X POST https://api.tabletennisapi.com/v1/batch \
>   -H "Authorization: Bearer $TT_KEY" -H "Content-Type: application/json" \
>   -d '{"requests": ["/v1/players/pl_51t63nz3", "/v1/players/pl_yp1ww51h/matches"]}'
> ```
> 
> [Ask about this](https://tabletennisapi.com/support)

### Pagination

Lists come back a page at a time. Two ways to move through them. Page: https://tabletennisapi.com/documentation/pagination

#### Pages

Most lists return 50 rows by default. Ask for up to 200 with `per_page`, and move with `page`. The first page's `meta` tells you `total` and `total_pages`.

```bash
curl "https://api.tabletennisapi.com/v1/matches?date=2026-09-28&per_page=200&page=2" \
  -H "Authorization: Bearer $TT_KEY"
```

#### Cursors

Long histories, such as a player's matches, use a cursor instead. When there is more, `meta.next_cursor` is set: pass it back as `cursor` exactly as given. When it is `null` you have everything.

```python
cursor = None
while True:
    params = {"cursor": cursor} if cursor else {}
    res = get("/v1/players/pl_51t63nz3/matches", params)
    rows += res["data"]
    cursor = res["meta"]["next_cursor"]
    if not cursor: break
```

> `per_page` outside 1 to 200, or a changed cursor, is refused with `400 bad_request`.

### Rate limits

Two limits: calls a minute and calls a day. Every response tells you where you stand. Page: https://tabletennisapi.com/documentation/rate-limits

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Calls a minute your plan allows |
| `X-RateLimit-Remaining` | Calls left in this minute |
| `X-RateLimit-Reset` | When the minute resets, as a Unix time |
| `X-Quota-Remaining` | Calls left today |

> Limits belong to your account, not to a key: all your keys share one allowance a minute and one a day.

#### When you go over

The call is refused with `429` and nothing is charged. `rate_limited` means slow down for the rest of the minute; `quota_exceeded` means today's allowance is used and resets at 00:00 UTC. Both carry `Retry-After` in seconds.

#### Pace yourself

Read `X-RateLimit-Remaining` and wait for `X-RateLimit-Reset` when it reaches zero. For live scores, one call to `/v1/live` every 10 seconds is 6 a minute and covers every table.

### Errors

Errors are JSON with a short code you can switch on and a message you can show. Page: https://tabletennisapi.com/documentation/errors

*403*

```json
{
  "error": {
    "code": "upgrade_required",
    "message": "2026-01-12 is outside the dates your plan (Free key) reads...",
    "pricing": "https://tabletennisapi.com/pricing",
    "docs": "https://tabletennisapi.com/documentation/errors"
  }
}
```

| HTTP | Code | What to do |
| --- | --- | --- |
| 400 | `bad_request` | A value is malformed, or the key was put in the address |
| 401 | `unauthorized` | Send a valid key in a header |
| 403 | `upgrade_required` | The data is outside your plan; the message names the plans that include it |
| 404 | `not_found` | No match, event or player has that id |
| 422 | `unknown_parameter` | A filter name is misspelt; the message lists the ones allowed |
| 429 | `rate_limited`, `quota_exceeded` | Wait for `Retry-After` |
| 500 | `server_error` | Our fault, already logged; try again shortly |

### Timezones and dates

Times are UTC. The date of a match is the local day of play. Page: https://tabletennisapi.com/documentation/timezones-and-dates

#### Two kinds of time

`start_time` is in UTC, written like `2026-03-14T09:40:00Z`. `date` is the local day at the venue, so an evening match in Macao on 14 March is dated 14 March wherever you are.

#### Filtering

`?date=`, `?from=` and `?to=` all use that local day, as `YYYY-MM-DD`. A range on `/v1/matches` can span up to 31 days a call.

#### What your plan can read

Each plan reads a window of days, by that local date. The free key reads yesterday, today and the days ahead; ask outside it and you get `403 upgrade_required`. `/v1/usage` tells you your window.

### Sandbox tier (planned, not built yet)

Test keys that return fixed sample data, for building and testing without using an allowance. Page: https://tabletennisapi.com/documentation/sandbox

> Planned, not built yet
> 
> This guide describes how it will work so you can plan around it. Nothing on this page can be called today.
> 
> - Needs: test keys and a fixed sample data set
> - For: every account, free
> 
> How it will work: keys starting `tta_test_` answer every endpoint, including the paid ones, from a small fixed set of real past matches. Calls are not counted, the data never changes, so tests stay stable.
> 
> [Ask about this](https://tabletennisapi.com/support)

### MCP server

Let Claude, ChatGPT, Cursor and other MCP apps call the API for you. Page: https://tabletennisapi.com/documentation/mcp-server

The MCP server gives an AI assistant 14 tools, from `live_matches` to `head_to_head`. Each tool is one API endpoint and calls it with your key, so it follows your plan and counts against your allowance like any other call. The [MCP page](https://tabletennisapi.com/mcp) lists every tool and the plans that include it.

#### Your MCP address

Your address is shown with your key when the key is made, on the sign-up page or your dashboard. It ends in the key, so keep it as private as the key; if it is seen, revoke the key on your dashboard and the address stops working.

*Address*

```json
https://tabletennisapi.com/mcp/YOUR_KEY
```

#### Adding it

In Claude or ChatGPT, add a custom connector and paste the address as its URL. In Claude Code:

*Terminal*

```json
claude mcp add --transport http tabletennis https://tabletennisapi.com/mcp/YOUR_KEY
```

In Cursor, in `.cursor/mcp.json`:

*JSON*

```json
{"mcpServers": {"tabletennis": {"url": "https://tabletennisapi.com/mcp/YOUR_KEY"}}}
```

#### Your own client

The server speaks MCP's streamable HTTP transport: JSON-RPC 2.0 over `POST`, with `initialize`, `tools/list`, `tools/call` and `ping`. You can send the key in a header and use the address without it:

```bash
curl -X POST https://tabletennisapi.com/mcp \
  -H "Authorization: Bearer $TT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

#### When a tool is refused

A refusal comes back as a tool result with `isError`, never as a protocol error, so the assistant can explain it: a tool outside your plan names the plans that include it and links pricing, and a limit reached says when to try again.

### Live odds (planned, not built yet)

Match odds alongside scores. Page: https://tabletennisapi.com/documentation/live-odds

> Planned, not built yet
> 
> This guide describes how it will work so you can plan around it. Nothing on this page can be called today.
> 
> - Needs: a source licensed for resale; the source we tested licenses its data to individuals only, so odds are on hold
> - For: to be decided
> 
> We do not publish odds. This page is here so it is clear that odds are not part of the API today.
> 
> [Ask about this](https://tabletennisapi.com/support)

### What we cover

Every professional event since December 2024, every discipline, the calendar ahead. Page: https://tabletennisapi.com/documentation/what-we-cover

#### Events

Every professional event from December 2024: Grand Smash, Champions, Finals and the World Cup (tier 1), Star Contender and Contender (tier 2), the Feeder series (tier 3), youth events (tier 4) and para and other events (tier 5). The calendar ahead is included.

#### Disciplines

| Code | Discipline |
| --- | --- |
| `MS` `WS` | Men's and women's singles |
| `MD` `WD` `XD` | Men's, women's and mixed doubles |
| `MT` `WT` `XT` | Team ties |

Youth matches carry `age_group` from U11 to U19; para matches carry `para_class`, such as `MS6`.

#### Not covered yet

- Seasons before December 2024.
- Point by point and odds: see the planned endpoints and guides.
- World rankings before October 2026: ranking history starts with the first week we read.
- National leagues and club competitions.

### The whole API in one file

Everything about the API in files you can hand to a tool or a language model. No key needed to read them. Page: https://tabletennisapi.com/documentation/the-whole-api-in-one-file

- **[manual.md](https://tabletennisapi.com/documentation/manual.md)**: The whole documentation as one Markdown file: every endpoint, field, error, plan and guide.
- **[llms.txt](https://tabletennisapi.com/llms.txt)**: A short guide for language models, linking everything else.
- **[openapi.json](https://tabletennisapi.com/openapi.json)**: OpenAPI 3.1: every endpoint, filter and field, for generating clients.
- **[Postman collection](https://tabletennisapi.com/documentation/postman-collection.json)**: Every call ready to run. Import it and set `TT_KEY`.

#### Give it to a language model

Paste the address of `manual.md` into Claude, ChatGPT or Cursor, or save the file into your project, and ask for the code you need. It is plain Markdown with every link written out in full, so nothing is lost when it is copied.

```bash
curl -O https://tabletennisapi.com/documentation/manual.md
```

#### Use with Claude and agents

The agent skill is one file that teaches an AI agent how to use the API: getting a key, the calls that answer the common questions, the limits, the errors and a typical workflow. It is built from the same source as these pages. [Download skill.md](https://tabletennisapi.com/skill.md), then save it as `~/.claude/skills/tabletennisapi/SKILL.md`, or upload it as a skill in Claude's settings. Agents find it by themselves at `/.well-known/agent-skills/index.json`.

```bash
mkdir -p ~/.claude/skills/tabletennisapi
curl -o ~/.claude/skills/tabletennisapi/SKILL.md https://tabletennisapi.com/skill.md
```

#### Generate a client

Any OpenAPI 3.1 generator can build a typed client from `openapi.json`. The examples in it, in the manual and in the Postman collection use real ids from a recent final, so every call works as written.

```bash
npx @openapitools/openapi-generator-cli generate \
  -i https://tabletennisapi.com/openapi.json -g python -o tabletennis-client
```

#### Always current

All four files are built from the same source as these pages whenever it changes, and the coverage figures in the manual are counted from the data each day. Each file carries the date it was last updated.

> Only built endpoints are in `openapi.json` and the Postman collection. The manual also lists the planned ones, marked as not built yet.

### WebSocket stream (planned, not built yet)

Live changes pushed to you the moment they happen, instead of polling. Page: https://tabletennisapi.com/documentation/websocket-stream

> Planned, not built yet
> 
> This guide describes how it will work so you can plan around it. Nothing on this page can be called today.
> 
> - Needs: the push service on the API host
> - For: Live and Complete
> 
> How it will work: open one connection and receive a message whenever a point, game or match changes at any table.
> 
> ```javascript
> const ws = new WebSocket("wss://api.tabletennisapi.com/v1/stream?key=...");
> ws.onmessage = (e) => draw(JSON.parse(e.data));
> ```
> 
> The key in the address above is a placeholder: the final design will pass it in a way that keeps it out of logs.
> 
> [Ask about this](https://tabletennisapi.com/support)

### Webhooks

We call your address the moment a match starts, a result is official or a new event is on the calendar. Page: https://tabletennisapi.com/documentation/webhooks

Instead of asking the API every few minutes, add an address on your dashboard's [Webhooks](https://tabletennisapi.com/account/webhooks) tab and we send you a POST when something happens. Webhooks are on the Live and Complete plans, up to 10 addresses an account.

#### Events

| Event | When it is sent | data holds |
| --- | --- | --- |
| `match.finished` | A match has its official result | The match, as `GET /v1/matches/{id}` |
| `match.live` | A match starts | The match, as `GET /v1/matches/{id}` |
| `event.added` | A new event is on the calendar | The event, as `GET /v1/events/{id}` |

Choose the events each address gets. Only matches played in the last three days are sent, so nothing old ever arrives.

#### What a delivery looks like

*JSON*

```json
{
  "id": "evt_6extq8sj",
  "type": "match.finished",
  "created_at": "2026-10-01T21:38:02Z",
  "data": { "id": "mt_qrvrky4e", "status": "finished", "score": { "a": 3, "b": 1 }, ... }
}
```

The headers say what it is: `X-TTA-Event` is the event type, `X-TTA-Delivery` a number for this delivery, and `X-TTA-Signature` proves it came from us.

#### Checking the signature

Every address has its own signing secret, shown on the dashboard. The signature header is `t=<unix time>,v1=<hash>`, where the hash is HMAC-SHA256 of the time, a full stop and the raw body. Recompute it and compare, and refuse anything older than five minutes:

```php
$body = file_get_contents('php://input');
[$t, $v1] = sscanf($_SERVER['HTTP_X_TTA_SIGNATURE'], 't=%d,v1=%s');
$ok = hash_equals(hash_hmac('sha256', "$t.$body", getenv('TT_WEBHOOK_SECRET')), $v1)
      && abs(time() - $t) < 300;
```

```python
import hmac, hashlib, time
t, v1 = (p.split("=", 1)[1] for p in request.headers["X-TTA-Signature"].split(","))
mac = hmac.new(SECRET.encode(), f"{t}.".encode() + request.get_data(), hashlib.sha256).hexdigest()
ok = hmac.compare_digest(mac, v1) and abs(time.time() - int(t)) < 300
```

#### Answering, and retries

- Answer with any 2xx within 10 seconds. Do the slow work afterwards.
- Anything else, or no answer, and we send it again: 8 tries over about 3 days (after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 1 day and 2 days).
- A 4xx (other than 408, 409, 425 or 429) means "do not send this again", and we stop for that delivery.
- After 20 failures in a row we turn the address off and email you. Turn it back on from the dashboard once it answers again.
- The same event can arrive more than once. Use its `id` to skip ones you have already handled.

#### Trying it

Press **Send a test** next to an address: we send a real `match.finished` for the most recent result straight away, and the dashboard shows what your address answered. Addresses must be https on the standard port, and never a private or local network.

### Browser extension (planned, not built yet)

Live table tennis scores in your browser toolbar, powered by your key. Page: https://tabletennisapi.com/documentation/browser-extension

> Planned, not built yet
> 
> This guide describes how it will work so you can plan around it. Nothing on this page can be called today.
> 
> - Needs: the extension itself, published to the Chrome Web Store
> - For: every plan
> 
> How it will work: pin players or events and see their live scores and results in a small panel, without opening a page.
> 
> [Ask about this](https://tabletennisapi.com/support)

### Changelog

What changed in the API, newest first. Page: https://tabletennisapi.com/documentation/changelog

- **9 Oct 2026, Point by point, format and the officials' calls.** `GET /v1/matches/{id}/points` gives every score in every game as our live tracking saw it, about every 10 seconds, with who served, for matches followed live from October 2026, on every plan. Every match now carries `points_per_game`, `win_by`, `utc_offset_min`, `started_at` (the first point), `timeouts`, `cards`, `video_review`, `challenges_each` and, while live, `serving`. Events carry `utc_offset_min`.
- **8 Oct 2026, World rankings.** `GET /v1/rankings` gives the top 100 of every world ranking list, senior and youth, singles and doubles (by pair or by player), with points and movement, on every plan. `GET /v1/players/{id}/rankings` gives a player's ranking week by week on the Live, Archive and Complete plans. Ranking history starts with the week of 5 October 2026 and grows every week. `GET /v1/players/{id}` now carries `world_ranks`, the player's ranks this week.
- **7 Oct 2026, Match length from live tracking.** When a match card records no length, `duration_s` is now measured by our live tracking for every match followed from its first point, to about 10 seconds.
- **7 Oct 2026, Date of birth and playing hand.** `GET /v1/players/{id}` now carries `born`, `hand` and `height_cm` where they are publicly recorded, mostly for senior players, and `lost` in a player's record no longer counts draws or matches not played.
- **7 Oct 2026, Every result explained.** An official match without a winner now says why in `result_note`: `not played`, `draw` or `stopped`, beside `walkover`, `retired` and `disqualified`. Team ties list each rubber's game score in `games`, and a tie level on rubbers in a format played to a games total takes the side with more games as its winner.
- **7 Oct 2026, Agent skill.** One SKILL.md that teaches Claude and other agents to use the API, at [/skill.md](https://tabletennisapi.com/skill.md) and found by agents at `/.well-known/agent-skills/index.json`. See [The whole API in one file](https://tabletennisapi.com/documentation/the-whole-api-in-one-file).
- **1 Oct 2026, Webhooks.** On the Live and Complete plans, add up to 10 addresses on your dashboard and get a signed POST for `match.finished`, `match.live` and `event.added`, with retries over about 3 days. See [Webhooks](https://tabletennisapi.com/documentation/webhooks).
- **1 Oct 2026, Key rotation.** Rotate a key from the dashboard: the new key works at once and the old one keeps working for 24 hours while you swap it in.
- **30 Sep 2026, Our own ids, codes and names everywhere.** `round` is now our own code (`final`, `semi_final`, `quarter_final`, `round_of_16` ... `group`, `qualifying_1`, `bronze`), and the `round` filter takes the same codes. `irm` is replaced by `result_note` (`walkover`, `retired` or `disqualified`), `table` is the table number, and team ties carry `rubber`. The outside player number and its filter are gone: find players by name or country. Names are in normal casing, such as Wang Chuqin, and event names carry no organiser's brand, such as Champions Macao 2026.
- **30 Sep 2026, One allowance per account.** Calls a minute and calls a day are now counted across all the keys of an account, and `/v1/usage` reports the account's calls.
- **30 Sep 2026, The whole API in one file.** The documentation as one Markdown file, llms.txt, an OpenAPI 3.1 file and a Postman collection, all generated from the same source as these pages.
- **30 Sep 2026, History starts December 2024.** Coverage and every page now say so. Earlier seasons are not in the API yet.
- **29 Sep 2026, Version 1 preview.** Twelve endpoints: live matches, matches, a match, yesterday's results, events, an event and its matches, players, a player, a player's matches, head to head and usage. Free key, Live, Archive and Complete plans.

## Planned endpoints

Not built yet, so they cannot be called. Each says what it needs and which plans will get it.

| Endpoint | What it will return | Needs | For |
| --- | --- | --- | --- |
| `GET /v1/matches/{id}/points` | Point by point: server, winner and running score of every point | Point-by-point collector | Live, Archive, Complete |
| `GET /v1/matches/{id}/stats` | Match stats: serve and return points won, longest run, points at 10-all | Point by point first | Live, Archive, Complete |
| `GET /v1/matches/{id}/rubbers` | Team ties: each individual match inside the tie | Team rubber parsing from the tie cards | All plans |
| `GET /v1/matches/changes` | Everything that changed since a time you give, for keeping a copy in sync | Change log on matches (data we hold) | Live, Archive, Complete |
| `WSS /v1/stream` | Live changes pushed over a WebSocket as they happen, no polling | Push service on the API host | Live, Complete |
| `GET /v1/events/{id}/draw` | The draw as a bracket for one discipline, round by round | Bracket builder from rounds (data we hold) | All plans |
| `GET /v1/events/{id}/results` | Winners, finalists and semi-finalists of every discipline | Data we hold | All plans |
| `GET /v1/events/{id}/players` | Everyone entered in the event, by discipline | Data we hold | All plans |
| `GET /v1/events/{id}/stats` | Event totals: matches, deciding games, longest match, biggest comebacks | Data we hold | Live, Archive, Complete |
| `GET /v1/players/{id}/stats` | Career stats: win rate by discipline, games won, deciding games, average length | Data we hold | Live, Archive, Complete |
| `GET /v1/players/{id}/form` | Recent form: last results, current streak, events played | Data we hold | All plans |
| `GET /v1/players/{id}/opponents` | Record against every opponent faced | Data we hold | Archive, Complete |
| `GET /v1/players/{id}/events` | Events played, with the best round reached in each | Data we hold | All plans |
| `GET /v1/players/{id}/partners` | Doubles partners and the record with each | Data we hold | Archive, Complete |
| `GET /v1/players/{id}/titles` | Titles and finals, by event and discipline | Data we hold | All plans |
| `GET /v1/players/compare` | Two to four players side by side on the same stats | Player stats first | Live, Archive, Complete |
| `GET /v1/stats/leaders` | Leaders over a period: wins, win rate, titles, by discipline | Data we hold | Live, Archive, Complete |
| `GET /v1/stats/longest-matches` | The longest matches by time, with every game | Data we hold | Archive, Complete |
| `GET /v1/stats/deciding-games` | Best and worst records in deciding games | Data we hold | Archive, Complete |
| `GET /v1/stats/serve` | Serve and return leaders: points won on serve and on return | Point by point first | Archive, Complete |
| `GET /v1/stats/countries` | Countries ranked by wins, titles and players, by discipline | Data we hold | All plans |
| `GET /v1/reference` | Every code the API uses: disciplines, age groups, para classes, rounds, tiers | Data we hold | All plans, no call counted |
| `GET /v1/status` | Data freshness: when live, results and events last updated. No key needed | Job log (data we hold) | Public |
| `GET /v1/keys` | List, label and revoke your keys from code | Account area | All plans |
| `GET /v1/webhooks` | Manage your webhook addresses from code. Webhooks themselves work today, set up from the dashboard | An API for webhook addresses | Live, Complete |
| `POST /v1/batch` | Up to 20 calls in one request, answered together | Batch runner on the API host | Live, Archive, Complete |

## About this document

Generated from the endpoint registry, the guides and the plans and prices the site itself runs on, never written by hand. The date at the top is the day those sources last changed: two copies with the same date are the same file.

- OpenAPI: https://tabletennisapi.com/openapi.json
- Postman collection: https://tabletennisapi.com/documentation/postman-collection.json
- MCP server: https://tabletennisapi.com/mcp
- Short index for language models: https://tabletennisapi.com/llms.txt
- Support: https://tabletennisapi.com/support
- Status: https://tabletennisapi.com/status
- Terms: https://tabletennisapi.com/terms
