# Tennis webhooks

Signed POST requests to your URL when Tennis events happen. Signing, retries, pauses and catch-up are the same for every sport: https://citoapi.com/docs/webhooks.md

Live catalog for this sport, with current status and volume: https://api.citoapi.com/api/v1/webhooks/events?game=tennis
Human docs: https://citoapi.com/docs/api/tennis/webhooks/
Status below was measured at 2026-11-08T20:30:57.835Z. Trust the live catalog over this file.

## Which events to subscribe to

- Live scoreboard: tennis.match.started, tennis.set.completed, tennis.match.completed. Match start, each set, and the final. For point-by-point score use /tennis/live/stream.
- Settle bets: tennis.match.completed, tennis.match.stats_ready. The final result with outcome (retirement and walkover included), then full stats for stat markets.
- Results only: tennis.match.completed, tennis.match.stats_ready. The result, then the serve statistics when they are in.
- Alerts: tennis.match.started, tennis.match.completed. Match start and result. Add a player filter to follow someone.

## Filters

- `matchId`: Only this match. Take the id from the schedule or live routes.
- `tour`: Leave empty for every tour.
- `tournamentId`: Find ids with /tennis/tournaments?q=.
- `playerId`: Matches either player. Find ids with /tennis/players/search?q=.

## Events

### tennis.match.started

A tennis match went live. data is the match as /tennis/matches/live returns it.

- When: A match goes live (first point played). Once per match.
- Status: Active. Sent during the last 7 days.
- Frequency: about 4 per day across all matches
- Plan: Pro and Scale
- Filters: matchId, tour, tournamentId, playerId
- Use it to: Flip a match to live and start your scoreboard.
- Notes: The envelope match_id equals data.id.

| Field | Type | Meaning |
|---|---|---|
| `best_of` | number | 3 or 5 sets. |
| `canonical_id` | string | The id to store. Same as id for almost all matches. |
| `canonical_tournament_id` | string | The tournament id to store. |
| `court` | null or string | Court name when known. |
| `current_set` | number | The set being played. |
| `game_score` | string | Point score of the current game. |
| `id` | string | The match id. Works on /tennis/matches/{id}. Same as the envelope match_id. |
| `is_simulated` | boolean |  |
| `last_updated_at` | string | When this match last changed. |
| `live_match_id` | string | The id on /tennis/matches/live. |
| `loser` | null | Losing player. Null while live. |
| `match_id` | string | The match id. Works on the match routes for this sport. |
| `outcome` | null | COMPLETED, RETIREMENT, WALKOVER, DEFAULT or ABANDONED. |
| `player1` | object | First player. On completed matches player1 is the winner, as on /matches/{id}. |
| `player2` | object | Second player. |
| `round` | string | Round code, for example F or Q. |
| `round_name` | string | Round name in words. |
| `scheduled_at` | string | Scheduled start, ISO 8601 UTC. |
| `server` | number | Who is serving: 1 or 2. |
| `sets` | array | Each set with games per player and any tiebreak. |
| `started_at` | string | When the match started. |
| `stats_available` | boolean | True when serve statistics exist for this match. |
| `stats_points_counted` | null or number |  |
| `stats_unavailable_code` | null or string |  |
| `status` | string | IN_PROGRESS or COMPLETED. |
| `surface` | string | Hard, Clay, Grass or Carpet. |
| `tiebreak_score` | null | Tiebreak points while one is being played. |
| `tour` | string | ATP, WTA and so on. |
| `tournament_display_name` | string |  |
| `tournament_id` | string | Tournament id. Works on /tennis/tournaments/{id}. |
| `tournament_name` | string | Tournament name. |
| `tournament_short_name` | string |  |
| `winner` | null | Winning player. Null while live. |
| `winner_side` | null | 1 or 2. |

```json
{
  "event": "tennis.match.started",
  "id": "a1b2c3d4-0000-4000-8000-000000000000",
  "timestamp": "2026-11-08T18:30:03.776Z",
  "event_type": "tennis.match.started",
  "match_id": "{matchId}",
  "previous_state": null,
  "data": {
    "id": "{matchId}",
    "sets": [
      {
        "score": "0-0",
        "set_num": 1,
        "tiebreak": null,
        "set_number": 1,
        "is_completed": false,
        "player1_games": 0,
        "player2_games": 0,
        "tiebreak_points": null
      }
    ],
    "tour": "ATP",
    "court": "Cancha Central",
    "loser": null,
    "round": "Q",
    "server": 1,
    "status": "IN_PROGRESS",
    "winner": null,
    "best_of": 3,
    "outcome": null,
    "player1": {
      "id": "atp_200335",
      "ioc": "BRA",
      "name": "Felipe Meligeni",
      "seed": null,
      "sets_won": 0,
      "is_serving": true,
      "canonical_id": "atp_200335",
      "current_game_score": "15"
    },
    "player2": {
      "id": "atp_111797",
      "ioc": "CHI",
      "name": "Nicolas Jarry",
      "seed": null,
      "sets_won": 0,
      "is_serving": false,
      "canonical_id": "atp_111797",
      "current_game_score": "0"
    },
    "surface": "Clay",
    "match_id": "{matchId}",
    "game_score": "15-0",
    "round_name": "Qualifying (round unspecified)",
    "started_at": "2026-11-08T18:20:00+00:00",
    "current_set": 1,
    "winner_side": null,
    "canonical_id": "{matchId}",
    "is_simulated": false,
    "scheduled_at": "2026-11-08T18:20:00+00:00",
    "live_match_id": "{id}",
    "tournament_id": "{id}-challenger-lima-6948-7",
    "tiebreak_score": null,
    "last_updated_at": "2026-11-08T18:29:54.118590+00:00",
    "stats_available": false,
    "tournament_name": "Lima",
    "stats_points_counted": null,
    "tournament_short_name": "Lima",
    "stats_unavailable_code": "not_published_yet",
    "canonical_tournament_id": "{id}-challenger-lima-6948-7",
    "tournament_display_name": "Lima CH"
  }
}
```

### tennis.set.completed

A set finished: data.set_number, data.set_score, data.set_winner_side, data.set_winner, plus the live match.

- When: A set finishes. Once per set; the last set arrives just before tennis.match.completed.
- Status: Active. Sent during the last 7 days.
- Frequency: about 10 per day across all matches (about 2 per match)
- Plan: Pro and Scale
- Filters: matchId, tour, tournamentId, playerId
- Use it to: Update the set score and alert on set winners.
- Notes: data.set_score lists player1 first.

| Field | Type | Meaning |
|---|---|---|
| `best_of` | number | 3 or 5 sets. |
| `canonical_id` | string | The id to store. Same as id for almost all matches. |
| `canonical_tournament_id` | string | The tournament id to store. |
| `court` | null or string | Court name when known. |
| `current_set` | number | The set being played. |
| `game_score` | string | Point score of the current game. |
| `id` | string | The match id. Works on /tennis/matches/{id}. Same as the envelope match_id. |
| `is_simulated` | boolean |  |
| `last_updated_at` | string | When this match last changed. |
| `live_match_id` | string | The id on /tennis/matches/live. |
| `loser` | null or object | Losing player. Null while live. |
| `match_id` | string | The match id. Works on the match routes for this sport. |
| `outcome` | null or string | COMPLETED, RETIREMENT, WALKOVER, DEFAULT or ABANDONED. |
| `player1` | object | First player. On completed matches player1 is the winner, as on /matches/{id}. |
| `player2` | object | Second player. |
| `result_available` | null |  |
| `result_unavailable_reason` | null |  |
| `round` | string | Round code, for example F or Q. |
| `round_name` | string | Round name in words. |
| `scheduled_at` | string | Scheduled start, ISO 8601 UTC. |
| `server` | number | Who is serving: 1 or 2. |
| `set` | object | The set that finished, with games and tiebreak. |
| `set_number` | number | Which set finished. |
| `set_score` | string | Games in that set, player1 first. |
| `set_winner` | object | Player who won the set. |
| `set_winner_side` | number | 1 or 2. |
| `sets` | array | Each set with games per player and any tiebreak. |
| `started_at` | string | When the match started. |
| `stats_available` | boolean | True when serve statistics exist for this match. |
| `stats_points_counted` | null or number |  |
| `stats_unavailable_code` | null or string |  |
| `status` | string | IN_PROGRESS or COMPLETED. |
| `surface` | string | Hard, Clay, Grass or Carpet. |
| `tiebreak_score` | null | Tiebreak points while one is being played. |
| `tour` | string | ATP, WTA and so on. |
| `tournament_display_name` | string |  |
| `tournament_id` | string | Tournament id. Works on /tennis/tournaments/{id}. |
| `tournament_name` | string | Tournament name. |
| `tournament_short_name` | string |  |
| `winner` | null or object | Winning player. Null while live. |
| `winner_side` | null or number | 1 or 2. |

```json
{
  "event": "tennis.set.completed",
  "id": "a1b2c3d4-0000-4000-8000-000000000000",
  "timestamp": "2026-11-08T17:19:04.284Z",
  "event_type": "tennis.set.completed",
  "match_id": "{matchId}",
  "previous_state": null,
  "data": {
    "id": "{matchId}",
    "set": {
      "score": "6-7",
      "set_num": 2,
      "tiebreak": "4-7",
      "set_number": 2,
      "is_completed": true,
      "player1_games": 6,
      "player2_games": 7,
      "tiebreak_points": {
        "loser": 4,
        "winner": 7,
        "player1": 4,
        "player2": 7,
        "in_progress": false
      }
    },
    "sets": [
      {
        "score": "1-6",
        "set_num": 1,
        "tiebreak": null,
        "set_number": 1,
        "is_completed": true,
        "player1_games": 1,
        "player2_games": 6,
        "tiebreak_points": null
      },
      {
        "score": "6-7",
        "set_num": 2,
        "tiebreak": "4-7",
        "set_number": 2,
        "is_completed": true,
        "player1_games": 6,
        "player2_games": 7,
        "tiebreak_points": {
          "loser": 4,
          "winner": 7,
          "player1": 4,
          "player2": 7,
          "in_progress": false
        }
      }
    ],
    "tour": "ATP",
    "court": "CENTRE COURT",
    "loser": {
      "id": "atp_111200",
      "age": null,
      "ioc": "SWE",
      "name": "Elias Ymer",
      "rank": 245,
      "seed": null,
      "entry": null,
      "is_winner": false,
      "rank_date": "2026-11-02",
      "canonical_id": "atp_111200"
    },
    "round": "R32",
    "server": 1,
    "status": "FINISHED",
    "winner": {
      "id": "atp_207678",
      "age": null,
      "ioc": "ARG",
      "name": "Juan Manuel Cerundolo",
      "rank": 54,
      "seed": null,
      "entry": null,
      "is_winner": true,
      "rank_date": "2026-11-02",
      "canonical_id": "atp_207678"
    },
    "best_of": 3,
    "outcome": "COMPLETED",
    "player1": {
      "id": "atp_111200",
      "ioc": "SWE",
      "name": "Elias Ymer",
      "seed": null,
      "sets_won": 0,
      "is_serving": true,
      "canonical_id": "atp_111200",
      "current_game_score": "0"
    },
    "player2": {
      "id": "atp_207678",
      "ioc": "ARG",
      "name": "Juan Manuel Cerundolo",
      "seed": null,
      "sets_won": 2,
      "is_serving": false,
      "canonical_id": "atp_207678",
      "current_game_score": "0"
    },
    "surface": "Hard",
    "match_id": "{matchId}",
    "set_score": "6-7",
    "game_score": "0-0",
    "round_name": "Round of 32",
    "set_number": 2,
    "set_winner": {
      "id": "atp_207678",
      "ioc": "ARG",
      "name": "Juan Manuel Cerundolo",
      "seed": null,
      "sets_won": 2,
      "is_serving": false,
      "canonical_id": "atp_207678",
      "current_game_score": "0"
    },
    "started_at": "2026-11-08T15:30:00+00:00",
    "current_set": 2,
    "winner_side": 2,
    "canonical_id": "{matchId}",
    "is_simulated": false,
    "scheduled_at": "2026-11-08T15:30:00+00:00",
    "live_match_id": "{id}",
    "tournament_id": "atp_2026_0429",
    "tiebreak_score": null,
    "last_updated_at": "2026-11-08T17:18:59.328323+00:00",
    "set_winner_side": 2,
    "stats_available": true,
    "tournament_name": "Stockholm",
    "result_available": null,
    "tournament_short_name": "Stockholm",
    "stats_unavailable_code": null,
    "canonical_tournament_id": "atp_2026_0429",
    "tournament_display_name": "Stockholm",
    "result_unavailable_reason": null
  }
}
```

### tennis.match.completed

A tennis result is final. data is the match as /tennis/matches/{id} returns it, without stats and point_stats.

- When: The result is final. Once per match.
- Status: Active. Sent during the last 7 days.
- Frequency: about 4 per day across all matches
- Plan: Pro and Scale
- Filters: matchId, tour, tournamentId, playerId
- Use it to: Settle on data.winner, data.score and data.outcome. Handle RETIREMENT and WALKOVER by your own rules.
- Notes: As on /tennis/matches/{id}, player1 is the winner. data.outcome is COMPLETED, RETIREMENT, WALKOVER, DEFAULT or ABANDONED.

| Field | Type | Meaning |
|---|---|---|
| `best_of` | number | 3 or 5 sets. |
| `canonical_id` | string | The id to store. Same as id for almost all matches. |
| `canonical_tournament_id` | string | The tournament id to store. |
| `court` | null or string | Court name when known. |
| `current_set` | number | The set being played. |
| `date_is_tournament_start` | boolean |  |
| `duration_minutes` | null | Match length in minutes. |
| `game_score` | string | Point score of the current game. |
| `id` | string | The match id. Works on /tennis/matches/{id}. Same as the envelope match_id. |
| `is_simulated` | boolean |  |
| `last_updated_at` | string | When this match last changed. |
| `live_match_id` | null or string | The id on /tennis/matches/live. |
| `loser` | object | Losing player. Null while live. |
| `loser_id` | string | Losing player id. |
| `loser_partner` | null |  |
| `loser_partner_id` | null |  |
| `loser_sets_won` | number | Sets won by the loser. |
| `match_date` | string | Match date, YYYY-MM-DD. |
| `match_id` | string | The match id. Works on the match routes for this sport. |
| `outcome` | string | COMPLETED, RETIREMENT, WALKOVER, DEFAULT or ABANDONED. |
| `player1` | object | First player. On completed matches player1 is the winner, as on /matches/{id}. |
| `player2` | object | Second player. |
| `qualifying_round` | null |  |
| `result_available` | boolean or null |  |
| `result_unavailable_reason` | null |  |
| `round` | string | Round code, for example F or Q. |
| `round_name` | string | Round name in words. |
| `scheduled_at` | null or string | Scheduled start, ISO 8601 UTC. |
| `score` | string | Final score with the winner first, for example 7-6 7-5. |
| `server` | number | Who is serving: 1 or 2. |
| `sets` | array | Each set with games per player and any tiebreak. |
| `start_time_kind` | null |  |
| `start_time_known` | boolean |  |
| `started_at` | string | When the match started. |
| `stats_available` | boolean | True when serve statistics exist for this match. |
| `stats_coverage` | object | How complete the statistics are. serve_stats is "full" when this event fires. |
| `stats_unavailable_code` | null |  |
| `status` | string | IN_PROGRESS or COMPLETED. |
| `surface` | string | Hard, Clay, Grass or Carpet. |
| `tiebreak_score` | null | Tiebreak points while one is being played. |
| `tour` | string | ATP, WTA and so on. |
| `tournament` | object | Tournament details. |
| `tournament_display_name` | string |  |
| `tournament_id` | string | Tournament id. Works on /tennis/tournaments/{id}. |
| `tournament_name` | string | Tournament name. |
| `tournament_short_name` | string |  |
| `winner` | object | Winning player. Null while live. |
| `winner_id` | string | Winning player id. |
| `winner_partner` | null |  |
| `winner_partner_id` | null |  |
| `winner_sets_won` | number | Sets won by the winner. |
| `winner_side` | number | 1 or 2. |

```json
{
  "event": "tennis.match.completed",
  "id": "a1b2c3d4-0000-4000-8000-000000000000",
  "timestamp": "2026-11-08T18:17:13.519Z",
  "event_type": "tennis.match.completed",
  "match_id": "{matchId}",
  "previous_state": null,
  "data": {
    "id": "{matchId}",
    "sets": [
      {
        "tiebreak": null,
        "set_number": 1,
        "loser_games": 6,
        "winner_games": 7,
        "player1_games": 7,
        "player2_games": 6,
        "tiebreak_points": null
      },
      {
        "tiebreak": null,
        "set_number": 2,
        "loser_games": 5,
        "winner_games": 7,
        "player1_games": 7,
        "player2_games": 5,
        "tiebreak_points": null
      }
    ],
    "tour": "ATP",
    "court": null,
    "loser": {
      "id": "atp_127108",
      "age": null,
      "ioc": "BRA",
      "name": "Igor Ribeiro Marcondes",
      "rank": 279,
      "seed": null,
      "entry": null,
      "is_winner": false,
      "rank_date": "2026-11-02",
      "canonical_id": "atp_127108"
    },
    "round": "Q",
    "score": "7-6 7-5",
    "status": "COMPLETED",
    "winner": {
      "id": "atp_212944",
      "age": null,
      "ioc": "JPN",
      "name": "Ryo Tabata",
      "rank": 934,
      "seed": null,
      "entry": null,
      "is_winner": true,
      "rank_date": "2026-11-02",
      "canonical_id": "atp_212944"
    },
    "best_of": 3,
    "outcome": "COMPLETED",
    "player1": {
      "id": "atp_212944",
      "age": null,
      "ioc": "JPN",
      "name": "Ryo Tabata",
      "rank": 934,
      "seed": null,
      "entry": null,
      "is_winner": true,
      "rank_date": "2026-11-02",
      "canonical_id": "atp_212944"
    },
    "player2": {
      "id": "atp_127108",
      "age": null,
      "ioc": "BRA",
      "name": "Igor Ribeiro Marcondes",
      "rank": 279,
      "seed": null,
      "entry": null,
      "is_winner": false,
      "rank_date": "2026-11-02",
      "canonical_id": "atp_127108"
    },
    "surface": "Clay",
    "loser_id": "atp_127108",
    "match_id": "{matchId}",
    "winner_id": "atp_212944",
    "match_date": "2026-11-08",
    "round_name": "Qualifying (round unspecified)",
    "tournament": {
      "id": "{id}-challenger-lima-6948-7",
      "city": "Lima",
      "name": "Lima CH",
      "year": 2026,
      "level": "C",
      "country": "PER",
      "surface": "Clay",
      "category": "challenger",
      "short_name": "Lima",
      "canonical_id": "{id}-challenger-lima-6948-7",
      "display_name": "Lima CH",
      "edition_category": "Challenger 75"
    },
    "winner_side": 1,
    "canonical_id": "{matchId}",
    "is_simulated": false,
    "scheduled_at": null,
    "live_match_id": null,
    "loser_partner": null,
    "tournament_id": "{id}-challenger-lima-6948-7",
    "loser_sets_won": 0,
    "stats_coverage": {
      "note": "Some serve statistics were not published for this match; the fields in missing are null.",
      "reason": "partially_published",
      "missing": [
        "break_points_saved",
        "break_points_faced"
      ],
      "serve_stats": "partial",
      "expected_later": false,
      "missing_by_side": {
        "loser": [
          "break_points_saved",
          "break_points_faced"
        ],
        "winner": [
          "break_points_saved",
          "break_points_faced"
        ]
      }
    },
    "winner_partner": null,
    "start_time_kind": null,
    "tournament_name": "Lima CH",
    "winner_sets_won": 2,
    "duration_minutes": null,
    "loser_partner_id": null,
    "qualifying_round": null,
    "result_available": true,
    "start_time_known": false,
    "winner_partner_id": null,
    "tournament_short_name": "Lima",
    "canonical_tournament_id": "{id}-challenger-lima-6948-7",
    "tournament_display_name": "Lima CH",
    "date_is_tournament_start": false,
    "result_unavailable_reason": null
  }
}
```

### tennis.match.stats_ready

The full serve statistics of a completed match are available. data is the full /tennis/matches/{id} body.

- When: data.stats_coverage.serve_stats becomes "full", which can be minutes or hours after the result. Once per match; not sent for matches whose statistics stay partial.
- Status: Active. Sent during the last 7 days.
- Frequency: about 5 per week
- Plan: Pro and Scale
- Filters: matchId, tour, tournamentId, playerId
- Use it to: Store the full serve statistics. Do not poll for them after the result: wait for this.

| Field | Type | Meaning |
|---|---|---|
| `best_of` | number | 3 or 5 sets. |
| `canonical_id` | string | The id to store. Same as id for almost all matches. |
| `canonical_tournament_id` | string | The tournament id to store. |
| `court` | null or string | Court name when known. |
| `date_is_tournament_start` | boolean |  |
| `duration_minutes` | null or number | Match length in minutes. |
| `id` | string | The match id. Works on /tennis/matches/{id}. Same as the envelope match_id. |
| `is_simulated` | boolean |  |
| `live_match_id` | null | The id on /tennis/matches/live. |
| `loser` | object | Losing player. Null while live. |
| `loser_id` | string | Losing player id. |
| `loser_partner` | null |  |
| `loser_partner_id` | null |  |
| `loser_sets_won` | number | Sets won by the loser. |
| `match_date` | string | Match date, YYYY-MM-DD. |
| `match_id` | string | The match id. Works on the match routes for this sport. |
| `outcome` | string | COMPLETED, RETIREMENT, WALKOVER, DEFAULT or ABANDONED. |
| `player1` | object | First player. On completed matches player1 is the winner, as on /matches/{id}. |
| `player2` | object | Second player. |
| `point_stats` | null or object | Point-level statistics. |
| `qualifying_round` | null |  |
| `result_available` | boolean |  |
| `result_unavailable_reason` | null |  |
| `round` | string | Round code, for example F or Q. |
| `round_name` | string | Round name in words. |
| `scheduled_at` | null or string | Scheduled start, ISO 8601 UTC. |
| `score` | string | Final score with the winner first, for example 7-6 7-5. |
| `sets` | array | Each set with games per player and any tiebreak. |
| `start_time_kind` | null or string |  |
| `start_time_known` | boolean |  |
| `stats` | object | Serve and return statistics for winner and loser. |
| `stats_coverage` | object | How complete the statistics are. serve_stats is "full" when this event fires. |
| `status` | string | IN_PROGRESS or COMPLETED. |
| `surface` | string | Hard, Clay, Grass or Carpet. |
| `tour` | string | ATP, WTA and so on. |
| `tournament` | object | Tournament details. |
| `tournament_display_name` | string |  |
| `tournament_id` | string | Tournament id. Works on /tennis/tournaments/{id}. |
| `tournament_name` | string | Tournament name. |
| `tournament_short_name` | string |  |
| `winner` | object | Winning player. Null while live. |
| `winner_id` | string | Winning player id. |
| `winner_partner` | null |  |
| `winner_partner_id` | null |  |
| `winner_sets_won` | number | Sets won by the winner. |
| `winner_side` | number | 1 or 2. |

```json
{
  "event": "tennis.match.stats_ready",
  "id": "a1b2c3d4-0000-4000-8000-000000000000",
  "timestamp": "2026-11-08T20:21:49.492Z",
  "event_type": "tennis.match.stats_ready",
  "match_id": "{matchId}",
  "previous_state": null,
  "data": {
    "id": "{matchId}",
    "sets": [
      {
        "tiebreak": {
          "loser_points": 3,
          "winner_points": 7
        },
        "set_number": 1,
        "loser_games": 6,
        "winner_games": 7,
        "player1_games": 7,
        "player2_games": 6,
        "tiebreak_points": {
          "loser": 3,
          "winner": 7,
          "player1": 7,
          "player2": 3,
          "in_progress": false
        }
      },
      {
        "tiebreak": null,
        "set_number": 2,
        "loser_games": 4,
        "winner_games": 6,
        "player1_games": 6,
        "player2_games": 4,
        "tiebreak_points": null
      }
    ],
    "tour": "ATP",
    "court": "Center",
    "loser": {
      "id": "{playerId}",
      "age": null,
      "ioc": null,
      "name": "Stephanos Schinas",
      "rank": null,
      "seed": null,
      "entry": null,
      "is_winner": false,
      "rank_date": null,
      "canonical_id": "{playerId}"
    },
    "round": "Q",
    "score": "7-6 6-4",
    "stats": {
      "loser": {
        "aces": 7,
        "serve_points": 69,
        "double_faults": 5,
        "first_serve_pct": 60.9,
        "first_serves_in": 42,
        "first_serves_won": 31,
        "total_points_won": 65,
        "return_points_won": 19,
        "second_serves_won": 15,
        "break_points_faced": 4,
        "break_points_saved": 2,
        "max_games_in_a_row": 2,
        "points_won_last_10": 6,
        "service_points_won": 46,
        "first_serve_win_pct": 73.8,
        "first_serve_won_pct": 73.8,
        "max_points_in_a_row": 5,
        "return_points_played": 66,
        "second_serve_win_pct": 55.6,
        "second_serve_won_pct": 55.6,
        "service_games_played": 11,
        "service_points_played": 69,
        "break_points_converted": 1,
        "break_points_saved_pct": 50,
        "first_serve_points_won": 31,
        "first_return_points_won": 12,
        "second_serve_points_won": 15,
        "second_return_points_won": 7,
        "break_point_opportunities": 2
      },
      "winner": {
        "aces": 0,
        "serve_points": 66,
        "double_faults": 0,
        "first_serve_pct": 66.7,
        "first_serves_in": 44,
        "first_serves_won": 32,
        "total_points_won": 70,
        "return_points_won": 23,
        "second_serves_won": 15,
        "break_points_faced": 2,
        "break_points_saved": 1,
        "max_games_in_a_row": 3,
        "points_won_last_10": 4,
        "service_points_won": 47,
        "first_serve_win_pct": 72.7,
        "first_serve_won_pct": 72.7,
        "max_points_in_a_row": 5,
        "return_points_played": 69,
        "second_serve_win_pct": 68.2,
        "second_serve_won_pct": 68.2,
        "service_games_played": 11,
        "service_points_played": 66,
        "break_points_converted": 2,
        "break_points_saved_pct": 50,
        "first_serve_points_won": 32,
        "first_return_points_won": 11,
        "second_serve_points_won": 15,
        "second_return_points_won": 12,
        "break_point_opportunities": 4
      }
    },
    "status": "COMPLETED",
    "winner": {
      "id": "{playerId}",
      "age": null,
      "ioc": "UKR",
      "name": "Nikita Mashtakov",
      "rank": null,
      "seed": null,
      "entry": null,
      "is_winner": true,
      "rank_date": null,
      "canonical_id": "{playerId}"
    },
    "best_of": 3,
    "outcome": "COMPLETED",
    "player1": {
      "id": "{playerId}",
      "age": null,
      "ioc": "UKR",
      "name": "Nikita Mashtakov",
      "rank": null,
      "seed": null,
      "entry": null,
      "is_winner": true,
      "rank_date": null,
      "canonical_id": "{playerId}"
    },
    "player2": {
      "id": "{playerId}",
      "age": null,
      "ioc": null,
      "name": "Stephanos Schinas",
      "rank": null,
      "seed": null,
      "entry": null,
      "is_winner": false,
      "rank_date": null,
      "canonical_id": "{playerId}"
    },
    "surface": "Hard",
    "loser_id": "{playerId}",
    "match_id": "{matchId}",
    "winner_id": "{playerId}",
    "match_date": "2026-11-08",
    "round_name": "Qualifying (round unspecified)",
    "tournament": {
      "id": "{id}-challenger-hersonissos-8606-9",
      "city": "Hersonissos",
      "name": "Hersonissos CH",
      "year": 2026,
      "level": "C",
      "country": "GRE",
      "surface": "Hard",
      "category": "challenger",
      "short_name": "Hersonissos",
      "canonical_id": "{id}-challenger-hersonissos-8606-9",
      "display_name": "Hersonissos CH",
      "edition_category": "Challenger 50"
    },
    "point_stats": {
      "loser": {
        "games_won": 10,
        "points_won": 65,
        "service_games": 11,
        "max_games_in_row": 2,
        "max_points_in_row": 5,
        "return_points_won": 19,
        "service_games_won": 9,
        "break_points_faced": 4,
        "break_points_saved": 2,
        "points_won_last_10": 6,
        "service_points_won": 46,
        "service_points_played": 69,
        "break_points_converted": 1,
        "break_point_opportunities": 2
      },
      "winner": {
        "games_won": 13,
        "points_won": 70,
        "service_games": 11,
        "max_games_in_row": 3,
        "max_points_in_row": 5,
        "return_points_won": 23,
        "service_games_won": 10,
        "break_points_faced": 2,
        "break_points_saved": 1,
        "points_won_last_10": 4,
        "service_points_won": 47,
        "service_points_played": 66,
        "break_points_converted": 2,
        "break_point_opportunities": 4
      }
    },
    "winner_side": 1,
    "canonical_id": "{matchId}",
    "is_simulated": false,
    "scheduled_at": "2026-11-08T14:45:54+00:00",
    "live_match_id": null,
    "loser_partner": null,
    "tournament_id": "{id}-challenger-hersonissos-8606-9",
    "loser_sets_won": 0,
    "stats_coverage": {
      "note": null,
      "reason": null,
      "missing": [],
      "serve_stats": "full",
      "expected_later": false,
      "missing_by_side": {
        "loser": [],
        "winner": []
      }
    },
    "winner_partner": null,
    "start_time_kind": "actual",
    "tournament_name": "Hersonissos CH",
    "winner_sets_won": 2,
    "duration_minutes": 94,
    "loser_partner_id": null,
    "qualifying_round": null,
    "result_available": true,
    "start_time_known": true,
    "winner_partner_id": null,
    "tournament_short_name": "Hersonissos",
    "canonical_tournament_id": "{id}-challenger-hersonissos-8606-9",
    "tournament_display_name": "Hersonissos CH",
    "date_is_tournament_start": false,
    "result_unavailable_reason": null
  }
}
```
