# ATP Tour Match Stats Scraper (`parsebird/atp-match-stats-scraper`) Actor

Scrape official ATP Tour player search, match history, and detailed match statistics: aces, break points, serve/return percentages, and set scores. Discover players, build deduplicated match plans, and pull match records at scale.

- **URL**: https://apify.com/parsebird/atp-match-stats-scraper.md
- **Developed by:** [ParseBird](https://apify.com/parsebird) (community)
- **Categories:** Developer tools, Automation, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 matches

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### ATP Tour Match Stats Scraper

Scrape player profiles, match history, and match statistics directly from the official [ATP Tour](https://www.atptour.com/) website — the same data source that powers atptour.com's own live scoreboards and match centre pages.

<table><tr>
<td style="border-left:4px solid #1C1917;padding:12px 16px;font-weight:600">
Search the ATP player directory, build a deduplicated match plan across one or more players, and pull match records with aces, double faults, break points saved/converted, and serve/return percentages — per set where ATP publishes them.
</td>
</tr></table>

##### Copy to your AI assistant

Copy this block into ChatGPT, Claude, Cursor, or any LLM to start using this actor.

```
parsebird/atp-match-stats-scraper on Apify. Call: ApifyClient("TOKEN").actor("parsebird/atp-match-stats-scraper").call(run_input={...}), then client.dataset(run["defaultDatasetId"]).list_items().items for results. Key inputs: operation (string, required: "discover-players" | "discover-matches" | "scrape-matches" | "scrape-match-ids"), search (string, player name for discover-players, e.g. "Sinner"), playerIds (string array, ATP player IDs for discover-matches/scrape-matches, e.g. ["S0AG"]), matchRequests (array of {matchId, requestedPlayerIds} for scrape-match-ids, up to 250, matchId format "{year}#{tournamentId}#{matchCode}" e.g. "2026#540#MS001"), maxMatchesPerPlayer (integer, default 5, 0=all), maxTotalMatches (integer, default 20, hard cap after dedup), matchFormat ("all" default or "singles"), includeStats (boolean, default true). Output recordType is "player", "match-reference", "match", or "match-exclusion". Match records include tournament, round, surface, sets (per-set game scores where published), and stats (flattened service/return/points rows with teamAValue/teamBValue). Full actor spec: fetch build via GET https://api.apify.com/v2/acts/parsebird~atp-match-stats-scraper (Bearer TOKEN). Get token: https://console.apify.com/account/integrations
```

### What does ATP Tour Match Stats Scraper do?

This Actor scrapes [atptour.com](https://www.atptour.com/), the official ATP Tour website, using the same JSON endpoints its own player pages and match centre use — no unofficial mirror, no stale cache.

- 🎾 **Player directory search** — look up any ATP player, past or present, by name and get their stable ATP player ID
- 📋 **Deduplicated match discovery** — request matches for several players at once; a match both players share is only fetched (and billed) once
- 📊 **Real match statistics** — aces, double faults, first/second serve percentages, break points saved and converted, and total points won, at match level and per set where ATP publishes a set-by-set breakdown
- 🏆 **Full match context** — tournament, round, surface, court, umpire, score, and both players' seeds and countries
- 🔀 **Singles and doubles** — doubles matches are detected automatically (via ATP's own `MD`/`QD` match codes); filter to singles-only with one input field
- 🎯 **Direct match lookup** — already know the match(es) you want? Skip discovery and fetch them by ID
- ⏱️ **Scheduling** — run this Actor on a recurring [Apify Schedule](https://docs.apify.com/platform/schedules) to keep a player's results current through a tournament or season
- 🔌 **API access** — trigger runs and pull results programmatically via the [Apify API](https://docs.apify.com/api/v2) or client libraries
- 📁 **Flexible export** — download results as JSON, CSV, or Excel from the Apify Console, or query them via API

### What data can you extract from ATP Tour?

| Field | Description |
|-------|-------------|
| `playerId` / `fullName` / `countryCode` | Player identity, from the ATP player directory |
| `matchId` | `{year}#{tournamentId}#{matchCode}` — ATP's own tournament and match numbering, e.g. `2026#540#MS001` |
| `tournament` / `round` / `surface` / `courtName` | Match context |
| `players` / `winnerId` / `matchFormat` | Both players (and partners, for doubles), the winner, and singles/doubles |
| `sets` | Per-set game score and tie-break, where ATP publishes a set-by-set breakdown |
| `stats` | Flattened service/return/points rows — aces, double faults, break points saved/converted, serve/return % — at match level and per set |
| `headToHeadUrl` | Link to the two players' ATP head-to-head page |
| `sourceUrl` | The exact ATP Tour match-stats page the record was built from |

Set-by-set statistics are published for most matches, but some ATP feeds (notably several Grand Slam matches we tested) only expose the match-level aggregate — in that case `sets` and per-set `stats` rows are simply absent, while match-level `stats` are still complete. Every match record's `stats` array is built directly from ATP's own field names, not inferred or estimated.

### Input parameters

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `operation` | string | **Yes** | `discover-players` | `discover-players`, `discover-matches`, `scrape-matches`, or `scrape-match-ids` |
| `search` | string | For `discover-players` | — | Player name, e.g. `Sinner` |
| `playerIds` | string array | For `discover-matches` / `scrape-matches` | — | ATP player IDs, e.g. `S0AG` |
| `matchRequests` | array | For `scrape-match-ids` | — | Up to 250 `{matchId, requestedPlayerIds}` objects |
| `maxMatchesPerPlayer` | integer | No | `5` | Most recent matches per player (0 = no per-player cap) |
| `maxTotalMatches` | integer | No | `20` | Hard ceiling on billed match records per run, after dedup |
| `matchFormat` | string | No | `all` | `all` or `singles` (skip doubles entirely) |
| `includeStats` | boolean | No | `true` | Include the `stats` array in match records |
| `proxy` | object | No | Apify Residential | Proxy configuration. ATP's Cloudflare protection blocks non-residential IPs, so this defaults to Apify's residential proxy group automatically — most users never need to change it |

#### The four operations

**`discover-players`** — search the player directory:

```json
{ "operation": "discover-players", "search": "Sinner" }
```

**`discover-matches`** — build a deduplicated match plan across players, at no charge (no match records are fetched yet):

```json
{
  "operation": "discover-matches",
  "playerIds": ["S0AG"],
  "maxMatchesPerPlayer": 10,
  "maxTotalMatches": 50
}
```

**`scrape-matches`** — discover and fetch full match records in one run:

```json
{
  "operation": "scrape-matches",
  "playerIds": ["S0AG"],
  "maxMatchesPerPlayer": 5,
  "maxTotalMatches": 20,
  "matchFormat": "singles",
  "includeStats": true
}
```

**`scrape-match-ids`** — fetch specific matches you already identified (e.g. from a previous `discover-matches` run):

```json
{
  "operation": "scrape-match-ids",
  "matchRequests": [
    { "matchId": "2026#540#MS001", "requestedPlayerIds": ["S0AG"] }
  ]
}
```

### Output example

A `match` record (Dinko Dinev vs. Valentin Basel, Plovdiv Challenger 2026 — a match with a full set-by-set breakdown):

```json
{
  "recordType": "match",
  "matchId": "2026#3169#QS024",
  "title": "Dinko Dinev vs Valentin Basel | 2026#3169#QS024 | ATP Tour Match Stats",
  "tournament": "Plovdiv 2",
  "eventType": "CH",
  "location": "Plovdiv",
  "surface": "Clay",
  "round": "1st Round Qualifying",
  "matchFormat": "singles",
  "status": "F",
  "playerIds": ["D0HS", "B0NF"],
  "winnerId": "B0NF",
  "sets": [
    { "setNumber": 1, "teamAGames": "4", "teamBGames": "6", "tieBreak": null },
    { "setNumber": 2, "teamAGames": "4", "teamBGames": "6", "tieBreak": null }
  ],
  "stats": [
    { "scope": "match", "setNumber": null, "category": "Service", "metric": "Aces", "teamAValue": "1", "teamBValue": "2" },
    { "scope": "set", "setNumber": 1, "category": "Service", "metric": "Break Points Saved", "teamAValue": "60% (3/5)", "teamBValue": "0% (0/1)" }
  ],
  "headToHeadUrl": "https://www.atptour.com/en/players/atp-head-2-head/dinko-dinev-vs-valentin-basel/d0hs/b0nf",
  "sourceUrl": "https://www.atptour.com/en/scores/match-stats/archive/2026/3169/qs024"
}
```

Download results in **JSON, CSV, or Excel** directly from the Apify Console, or pull them programmatically via the API.

### Use cases

- **Match scouting and opponent prep** — pull a player's recent results and serve/return numbers before a matchup
- **Tennis analytics and modeling** — build serve-performance or match-outcome models from official ATP stats
- **Content and media** — power score tickers, head-to-head graphics, or tournament recaps
- **Betting research** — compare break-point conversion and serve dominance across recent matches
- **Historical tracking** — schedule recurring runs to build a running log of a player's season

### How it works

1. Go to the [ATP Tour Match Stats Scraper](https://apify.com/parsebird/atp-match-stats-scraper) page on Apify Store
2. Click **Try for free** — new users get a free trial to test the Actor before paying
3. Run **`discover-players`** with a player name to find their ATP player ID
4. Run **`discover-matches`** with one or more player IDs to preview a deduplicated match plan at no charge
5. Run **`scrape-matches`** (or **`scrape-match-ids`** with specific matches) to fetch full match records with statistics
6. When the run finishes, download your data as **JSON, CSV, or Excel**, or fetch it via the API

### Use with the Apify API

**Python**

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_API_TOKEN")

run = client.actor("parsebird/atp-match-stats-scraper").call(run_input={
    "operation": "scrape-matches",
    "playerIds": ["S0AG"],
    "maxMatchesPerPlayer": 5,
    "maxTotalMatches": 20,
    "matchFormat": "singles",
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["recordType"] == "match":
        print(f"{item['tournament']} {item['round']}: {item['title']}")
```

**JavaScript**

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_API_TOKEN' });

const run = await client.actor('parsebird/atp-match-stats-scraper').call({
    operation: 'scrape-matches',
    playerIds: ['S0AG'],
    maxMatchesPerPlayer: 5,
    maxTotalMatches: 20,
    matchFormat: 'singles',
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
items
    .filter((item) => item.recordType === 'match')
    .forEach((item) => console.log(`${item.tournament} ${item.round}: ${item.title}`));
```

See the [Apify API documentation](https://docs.apify.com/api/v2) for full reference, or the Actor's **API** tab for ready-to-run request snippets in more languages. This Actor also works out of the box with [Make](https://www.apify.com/integrations/make), [Zapier](https://www.apify.com/integrations/zapier), and other Apify [integrations](https://apify.com/integrations).

### How much does it cost to scrape ATP Tour match stats?

This Actor uses **Pay-per-event** pricing — you only pay for player and match records actually returned, with no separate platform rental fee. `discover-matches` is free: it returns lightweight `match-reference` records so you can preview and size a run before spending anything.

| Event | Price per event | Price per 1,000 |
|-------|-----------------|-----------------|
| `match-scraped` | $0.025 | **$25.00** |
| `player-scraped` | $0.0019 | **$1.90** |

`match-scraped` is charged once per full match record (`recordType: "match"`) returned by `scrape-matches` or `scrape-match-ids` — not for `match-reference` or `match-exclusion` records. `player-scraped` is charged once per player returned by `discover-players`. Pulling 20 matches for one player costs about $0.50 on the Free plan. Start with a small `maxTotalMatches` on the free trial to preview a player's data before scaling up.

### FAQ

**Where does this data come from?**
Directly from [atptour.com](https://www.atptour.com/), the official ATP Tour website — the same JSON endpoints that power its own player pages, match centre, and live scoreboards.

**Does this include shot-by-shot, point-by-point rally data?**
No. This Actor returns match- and set-level statistics (aces, double faults, break points, serve/return percentages) and set scores, which is the full depth of statistical data ATP's public site exposes for the vast majority of matches. It does not include ball-by-ball rally or shot-type data.

**Why is the `sets` array sometimes empty?**
ATP publishes a full set-by-set statistics breakdown for most matches, but for some matches — including several Grand Slam matches in our own testing — only the match-level aggregate is available from the source. When that happens, `sets` and per-set `stats` rows are omitted, but match-level `stats` and all match metadata are still complete.

**What's the difference between `discover-matches` and `scrape-matches`?**
`discover-matches` returns lightweight `match-reference` records (score, round, opponent) for free, so you can review and size a batch first. `scrape-matches` does the same discovery step and then fetches the full match record — including statistics — for each match, billed per match.

**How do I avoid paying for matches I don't want?**
Run `discover-matches` first, review the `match-reference` records, then pass the specific `matchId` values you want into `scrape-match-ids`. This guarantees you only pay for matches you've explicitly chosen.

**Do I need to configure a proxy?**
No. ATP Tour's Cloudflare protection blocks non-residential traffic, so this Actor defaults to Apify's residential proxy group automatically on every run — you only need to touch the `proxy` input if you want to use your own proxy instead.

**Does `matchFormat: "singles"` save money on doubles matches?**
Yes. When set to `singles`, doubles matches are detected from ATP's own match code (`MD`/`QD`) and skipped before any match-stats request is made — they're returned as unbilled `match-exclusion` records instead of full match records.

**Can I schedule recurring runs?**
Yes. Use Apify's [Scheduler](https://docs.apify.com/platform/schedules) to run this Actor daily or weekly and pick up new matches as a tournament or season progresses.

**Can I access results via API?**
Yes. Every run's dataset is available through the [Apify API](https://docs.apify.com/api/v2) and client libraries (Python, JavaScript) — see the Actor's **API** tab for ready-made snippets.

**Something not working?**
Open an issue on the Actor's **Issues** tab in Apify Console — the ParseBird team monitors it directly.

### Is it legal to scrape ATP Tour data?

ATP Tour publishes player profiles, match results, and statistics for public viewing without a login or paywall. This Actor only reads publicly accessible data — it does not bypass any authentication. You're responsible for complying with atptour.com's terms of use for your specific use case. See Apify's [blog post on the legality of web scraping](https://blog.apify.com/is-web-scraping-legal/) for general background.

### Related Actors

Other tennis and data-extraction Actors from ParseBird:

- [Tennis Abstract Player Match Scraper & API](https://apify.com/parsebird/tennis-abstract-match-scraper) — full career match history and dominance-ratio stats from Tennis Abstract
- [SofaScore Scraper](https://apify.com/parsebird/sofascore-scraper) — live scores, lineups, and odds across football and other sports
- [Website Contact Finder](https://apify.com/parsebird/website-contact-finder) — extract emails, phone numbers, and social links from any website

# Actor input Schema

## `operation` (type: `string`):

What to do: search the player directory, build a deduplicated match plan, scrape full match records for one or more players, or scrape a specific list of match IDs.

## `search` (type: `string`):

Used by 'discover-players'. Full or partial player name, e.g. 'Sinner' or 'Alcaraz'.

## `playerIds` (type: `array`):

Used by 'discover-matches' and 'scrape-matches'. ATP player IDs from a 'discover-players' run, e.g. S0AG (Jannik Sinner).

## `matchRequests` (type: `array`):

Used by 'scrape-match-ids'. Up to 250 objects: {"matchId": "2026#540#MS001", "requestedPlayerIds": \["S0AG"]}. matchId is "{year}#{tournamentId}#{matchCode}", copied from a match-reference or match record this Actor produced.

## `maxMatchesPerPlayer` (type: `integer`):

Most recent matches to consider per player, newest first. 0 means no per-player cap (still bounded by 'Max total matches').

## `maxTotalMatches` (type: `integer`):

Hard ceiling on billed match records per run, applied after deduplicating matches shared between requested players.

## `matchFormat` (type: `string`):

'Singles' skips doubles entirely (cheaper, fewer requests). 'All' includes both and reports skipped doubles as match-exclusion records.

## `includeStats` (type: `boolean`):

Fetch serve, return, and points statistics for each match (aces, break points saved/converted, serve/return percentages, per set where published). Turn off for lightweight match records (score and metadata only).

## `proxy` (type: `object`):

atptour.com sits behind Cloudflare bot management that blocks datacenter IPs; a residential proxy is required for reliable access from the Apify platform.

## Actor input object example

```json
{
  "operation": "discover-players",
  "search": "Sinner",
  "playerIds": [
    "S0AG"
  ],
  "matchRequests": [
    {
      "matchId": "2026#540#MS001",
      "requestedPlayerIds": [
        "S0AG"
      ]
    }
  ],
  "maxMatchesPerPlayer": 5,
  "maxTotalMatches": 20,
  "matchFormat": "all",
  "includeStats": true,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "operation": "discover-players",
    "search": "Sinner",
    "playerIds": [
        "S0AG"
    ],
    "matchRequests": [
        {
            "matchId": "2026#540#MS001",
            "requestedPlayerIds": [
                "S0AG"
            ]
        }
    ],
    "maxMatchesPerPlayer": 5,
    "maxTotalMatches": 20,
    "matchFormat": "all",
    "includeStats": true,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("parsebird/atp-match-stats-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "operation": "discover-players",
    "search": "Sinner",
    "playerIds": ["S0AG"],
    "matchRequests": [{
            "matchId": "2026#540#MS001",
            "requestedPlayerIds": ["S0AG"],
        }],
    "maxMatchesPerPlayer": 5,
    "maxTotalMatches": 20,
    "matchFormat": "all",
    "includeStats": True,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("parsebird/atp-match-stats-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "operation": "discover-players",
  "search": "Sinner",
  "playerIds": [
    "S0AG"
  ],
  "matchRequests": [
    {
      "matchId": "2026#540#MS001",
      "requestedPlayerIds": [
        "S0AG"
      ]
    }
  ],
  "maxMatchesPerPlayer": 5,
  "maxTotalMatches": 20,
  "matchFormat": "all",
  "includeStats": true,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call parsebird/atp-match-stats-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,parsebird/atp-match-stats-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/rL50clCXfT0UyJW0m/builds/BXuigih8MDXafk1i4/openapi.json
