# Tennis Abstract Player Match History Scraper (`automation-lab/tennis-abstract-player-match-history-scraper`) Actor

Export public Tennis Abstract player match histories with opponents, rankings, surfaces, rounds, scores, match statistics, and source links.

- **URL**: https://apify.com/automation-lab/tennis-abstract-player-match-history-scraper.md
- **Developed by:** [Stas Persiianenko](https://apify.com/automation-lab) (community)
- **Categories:** Sports
- **Stats:** 3 total users, 2 monthly users, 40.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Tennis Abstract Player Match History Scraper

Export public Tennis Abstract match histories for one or many tennis players.
Get clean dataset rows with dates, tournaments, surfaces, rounds, rankings, opponents, outcomes, scores, match statistics, duration, and source links.

The Actor accepts familiar player slugs such as `NovakDjokovic` and public Tennis Abstract player URLs.
It works for ATP and WTA player pages and applies the same filters to both input methods.

### What does this Tennis Abstract scraper do?

Tennis Abstract publishes unusually rich player-centric match histories.
This Actor turns those public pages into integration-ready JSON, CSV, Excel, XML, or RSS output in an Apify dataset.

For each player, it:

1. opens the lightweight public player data surface;
2. identifies the correct ATP or WTA history endpoint;
3. extracts recent or full historical match rows as needed;
4. applies date and court-surface filters;
5. deduplicates matches with stable IDs;
6. charges only for accepted rows and saves them to the default dataset.

Small recent-result requests avoid downloading the full career history.
Older backfills automatically use the complete history surface.

### Who is it for?

- **Tennis analysts** comparing performance by opponent, surface, or round.
- **Betting researchers** building their own historical features and models.
- **Scouts and coaches** reviewing player schedules and recent outcomes.
- **Sports journalists** checking tournament paths and prior meetings.
- **Fantasy sports teams** enriching player profiles with recent form.
- **Data engineers** scheduling repeatable player-history refreshes.
- **Academic researchers** assembling bounded public sports datasets.

This Actor exports source facts; it does not predict match winners or provide betting advice.

### Why use this Actor?

- Supports both player slugs and complete Tennis Abstract player URLs.
- Handles ATP and WTA history endpoint differences automatically.
- Returns one typed record per match, not raw HTML.
- Includes source URLs and stable player-match IDs for audit and deduplication.
- Supports inclusive date filtering and surface filtering.
- Stops at a user-controlled global `maxItems` limit.
- Uses a lightweight HTTP-first extraction path rather than an expensive browser by default.
- Supports optional Apify or custom proxies when the source blocks a network.
- Produces the standard Apify default dataset for API and integration compatibility.

### What data can you extract?

| Field | Meaning |
| --- | --- |
| `matchId` | Stable hash identifying this player's match row |
| `playerSlug` | Tennis Abstract player identifier |
| `playerName` | Readable name derived from the player slug |
| `playerUrl` | Public Tennis Abstract player URL |
| `date` | Match date in `YYYY-MM-DD` format |
| `tournament` | Tournament name |
| `tournamentUrl` | Tournament source URL when safely available |
| `surface` | Hard, Clay, Grass, Carpet, or another published value |
| `round` | Tournament round such as F, SF, QF, R16, or R32 |
| `playerRank` | Player ranking at the tournament, or `null` |
| `opponent` | Opponent name |
| `opponentSlug` | Opponent player identifier when available or inferred |
| `opponentUrl` | Public opponent page URL when available |
| `opponentRank` | Opponent ranking at the tournament, or `null` |
| `opponentCountry` | Published three-letter country code, or `null` |
| `outcome` | `win`, `loss`, or `null` when the source is ambiguous |
| `score` | Published match score, including RET or W/O markers |
| `dominanceRatio` | Tennis Abstract dominance ratio, or a value derived from published points |
| `aceRate` | Aces divided by serve points, expressed from 0 to 1 |
| `doubleFaultRate` | Double faults divided by serve points, from 0 to 1 |
| `firstServeIn` | First serves in, from 0 to 1 |
| `firstServeWon` | First-serve points won, from 0 to 1 |
| `secondServeWon` | Second-serve points won, from 0 to 1 |
| `breakPointsSaved` | Saved/opportunities text such as `10/13` |
| `duration` | Match duration such as `2:20`, or `null` |
| `sourceUrl` | Exact fragment or history surface used |
| `scrapedAt` | UTC extraction timestamp |

Older matches may not have point-level statistics.
Those values are returned as `null` rather than guessed.

### How to scrape Tennis Abstract player matches

1. Open the Actor in Apify Console.
2. Add one or more player slugs, such as `NovakDjokovic` or `IgaSwiatek`.
3. Alternatively, paste public Tennis Abstract player URLs into **Player URLs**.
4. Set **Maximum matches** to bound the dataset and run cost.
5. Optionally select court surfaces and an inclusive date range.
6. Leave proxy disabled unless your network is challenged by Tennis Abstract.
7. Click **Start**.
8. Open the default dataset and export JSON, CSV, Excel, XML, or RSS.

A small first run with 10–50 rows is useful for confirming your downstream mapping.

### Input parameters

| Input | Type | Default | Description |
| --- | --- | --- | --- |
| `playerSlugs` | string array | none | Player identifiers from the page `p=` parameter |
| `startUrls` | request list | none | Public Tennis Abstract player or classic-player URLs |
| `maxItems` | integer | `100` | Global row limit from 1 to 10,000 |
| `surfaces` | string array | all | Any of Hard, Clay, Grass, or Carpet |
| `fromDate` | string | none | Inclusive earliest match date, `YYYY-MM-DD` |
| `toDate` | string | none | Inclusive latest match date, `YYYY-MM-DD` |
| `proxyConfiguration` | object | direct | Optional Apify or custom proxy settings |

At least one slug or URL is required.
Duplicate slugs across inputs are processed once.
Malformed dates, unsupported domains, and invalid slugs fail clearly instead of producing an empty successful run.

#### Basic input example

```json
{
  "playerSlugs": ["NovakDjokovic"],
  "maxItems": 10
}
```

#### Surface and date backfill

```json
{
  "playerSlugs": ["IgaSwiatek", "ArynaSabalenka"],
  "surfaces": ["Clay"],
  "fromDate": "2024-01-01",
  "toDate": "2026-12-31",
  "maxItems": 200
}
```

### Output example

The default dataset contains one object per accepted match:

```json
{
  "matchId": "4e70c8b270707463c75d46d64383e95caf96c5cd",
  "playerSlug": "NovakDjokovic",
  "playerName": "Novak Djokovic",
  "playerUrl": "https://www.tennisabstract.com/cgi-bin/player.cgi?p=NovakDjokovic",
  "date": "2026-06-29",
  "tournament": "Wimbledon",
  "tournamentUrl": "https://www.tennisabstract.com/cgi-bin/tourney.cgi?t=2026-540/Wimbledon",
  "surface": "Grass",
  "round": "SF",
  "playerRank": 8,
  "opponent": "Jannik Sinner",
  "opponentSlug": "JannikSinner",
  "opponentUrl": "https://www.tennisabstract.com/cgi-bin/player.cgi?p=JannikSinner",
  "opponentRank": 1,
  "opponentCountry": "ITA",
  "outcome": "loss",
  "score": "6-4 6-4 6-4",
  "dominanceRatio": 0.55,
  "aceRate": 0.076,
  "doubleFaultRate": 0.029,
  "firstServeIn": 0.638,
  "firstServeWon": 0.761,
  "secondServeWon": 0.342,
  "breakPointsSaved": "10/13",
  "duration": "2:20",
  "sourceUrl": "https://www.tennisabstract.com/jsfrags/NovakDjokovic.js",
  "scrapedAt": "2026-07-28T06:00:00.000Z"
}
```

Percentages are represented as decimal ratios so they are immediately usable in calculations.
For example, `0.076` means 7.6%.

### How much does it cost to scrape Tennis Abstract player matches?

The Actor uses pay-per-event pricing:

- **Start:** $0.005 once per run.
- **Match item:** tiered pricing starting at $0.000041071 per accepted row on the FREE tier.
- Higher usage tiers receive lower per-row prices, down to $0.00001 on DIAMOND.

At the FREE-tier item price, illustrative Actor charges are:

| Accepted matches | Actor charge |
| ---: | ---: |
| 10 | start fee + 10 × FREE-tier item price |
| 100 | start fee + 100 × FREE-tier item price |
| 1,000 | start fee + 1,000 × FREE-tier item price |

The actual Console total can also include Apify platform compute, storage, and optional proxy usage according to your plan.
Rows rejected by filters, duplicates, and failed records are not item-charged.
Use `maxItems` to keep each run predictable.

### Scheduling and integration workflows

#### Scheduled form tracker

Run daily or weekly for a shortlist of players.
Use `matchId` in your database to upsert rows without duplicates.
This works well for dashboards and newsroom alerts.

#### Surface comparison dataset

Run separate tasks for Hard, Clay, and Grass.
Join the datasets by `playerSlug` and compare outcomes, rankings, serve rates, or dominance ratio.

#### Tournament preview research

Backfill recent histories for an event's entrants.
Filter downstream by common opponents, surface, date, and round.

#### Data warehouse pipeline

Schedule the Actor, connect a webhook or integration, and load the default dataset into BigQuery, Snowflake, PostgreSQL, Google Sheets, or an S3-compatible store.

### Run with the Apify API

Replace `YOUR_APIFY_TOKEN` with your token.
The Actor identifier is `automation-lab/tennis-abstract-player-match-history-scraper`.

#### cURL

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/automation-lab~tennis-abstract-player-match-history-scraper/runs?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"playerSlugs":["NovakDjokovic"],"maxItems":25}'
```

#### JavaScript

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/tennis-abstract-player-match-history-scraper').call({
    playerSlugs: ['NovakDjokovic', 'CarlosAlcaraz'],
    surfaces: ['Grass'],
    maxItems: 100,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("automation-lab/tennis-abstract-player-match-history-scraper").call(
    run_input={"playerSlugs": ["IgaSwiatek"], "maxItems": 25}
)
items = client.dataset(run["defaultDatasetId"]).list_items().items
print(items)
```

For asynchronous pipelines, start a run and poll its status or use an Apify webhook instead of waiting synchronously.

### Use with Apify MCP

#### Claude Code

Add this Actor to Claude Code and ask your MCP client to run player-history workflows:

```bash
claude mcp add --transport http apify \
  "https://mcp.apify.com?tools=automation-lab/tennis-abstract-player-match-history-scraper"
```

#### Claude Desktop, Cursor, and VS Code

Claude Desktop, Cursor, and VS Code can use this MCP server configuration:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=automation-lab/tennis-abstract-player-match-history-scraper"
    }
  }
}
```

#### Example prompts

After setup, ask your MCP client prompts such as:

- “Use the Tennis Abstract MCP Actor to export Novak Djokovic's latest 25 matches.”
- “Get Iga Swiatek clay matches since 2024 and summarize opponents by round.”
- “Build a CSV of grass matches for Djokovic, Alcaraz, and Sinner.”

### Reliability, limits, and proxies

Tennis Abstract is a third-party public source and can change without notice.
The Actor validates recognizable match data so a challenge page is not mistaken for an empty result.
Transient network failures and rate limits receive bounded retries with backoff.

Processing is deliberately conservative and sequential by player.
This reduces source load and avoids unnecessary session churn.
The global limit is 10,000 rows per run.

Direct access is the default and cheapest mode.
If your runs receive source access errors, enable Apify Proxy in `proxyConfiguration`.
Proxy availability, geography, and transfer costs depend on your Apify plan.
The Actor does not require a Tennis Abstract login or private cookie.

### Tips for accurate analysis

- Use `matchId` plus `playerSlug` as your upsert key.
- Treat ranking fields as rankings at the tournament, not necessarily today's rank.
- Keep outcome and score together; retired and walkover matches can have unusual scores.
- Expect `null` statistics for matches where the source lacks point totals.
- Filter by ISO date after export if you need rolling windows not known at run time.
- Start with one player before running a large multi-player backfill.
- Store `sourceUrl` and `scrapedAt` for lineage and refresh decisions.

### Responsible use and legality

Tennis Abstract match statistics are publicly viewable, but public access does not remove all obligations.
Review Tennis Abstract's terms and applicable database, copyright, privacy, and sports-data rules for your jurisdiction and use case.

Use reasonable limits and schedules.
Do not use the Actor to disrupt the source, circumvent access controls, republish protected databases wholesale, or misrepresent the data as official live scoring.
You are responsible for your inputs, downstream processing, and compliance.

### Troubleshooting

#### Why did the run say the player slug is invalid?

Use the exact value from the Tennis Abstract page's `p=` query parameter without spaces.
For example, use `NovakDjokovic`, not `Novak Djokovic`.
You can paste the full player URL instead.

#### Why did the run fail with an HTTP or challenge error?

The source may be rate-limiting the current network.
Wait before retrying, reduce repeated requests, or enable an appropriate proxy configuration.
Do not launch many concurrent retries.

#### Why are there no rows?

Confirm that the player slug exists and loosen date or surface filters.
A valid no-result filter returns an empty dataset; an unrecognized page shape fails with an explicit error.

#### Why are some statistics null?

Older, lower-level, retired, or walkover matches may not have complete point statistics on the source.
The Actor preserves those gaps rather than inventing values.

#### Why did I receive fewer rows than `maxItems`?

`maxItems` is an upper bound, not a guaranteed count.
The player may have fewer published matches satisfying your filters.

### Related Automation Lab Actor

- [Livesport Scores & Fixtures Scraper](https://apify.com/automation-lab/livesport-scores-fixtures-scraper) — use it for date-based live/recent scoreboards and fixtures across sports. Use this Tennis Abstract Actor when the job is player-centric historical tennis analysis.

### FAQ

#### Does this Actor support ATP and WTA players?

Yes.
It identifies the correct men's or women's complete-history endpoint from the player's public data fragment.

#### Can I provide multiple players?

Yes.
Combine multiple slugs and URLs in one run; duplicate slugs are removed.
The global `maxItems` limit applies across all players.

#### Does it scrape head-to-head summaries?

No.
The primary product is one row per published player match.
You can calculate head-to-head records downstream by grouping on opponent.

#### Is the data live?

The Actor returns what Tennis Abstract publishes at run time.
It is not a point-by-point live score feed, and source updates may lag completed matches.

#### Can I export to CSV or Excel?

Yes.
Open the default dataset after a run and choose JSON, CSV, Excel, XML, or RSS.

#### Can I run it on a schedule?

Yes.
Use Apify schedules and webhooks for recurring refreshes and downstream notifications.

#### Is a proxy required?

No for the normal working public route.
Proxy settings are optional recovery for networks challenged by the source.

#### Are filtered-out matches charged?

No item event is emitted for filtered, duplicate, malformed, or failed rows.
The one-time start event still applies to a valid started run.

# Actor input Schema

## `playerSlugs` (type: `array`):

Tennis Abstract player identifiers from the p= parameter, for example NovakDjokovic or IgaSwiatek.

## `startUrls` (type: `array`):

Optional Tennis Abstract player or classic-player URLs. Filters apply exactly as they do to player slugs.

## `maxItems` (type: `integer`):

Maximum total match rows saved across all players.

## `surfaces` (type: `array`):

Optional surface filter. Leave empty to include every surface published by Tennis Abstract.

## `fromDate` (type: `string`):

Optional inclusive earliest match date in YYYY-MM-DD format.

## `toDate` (type: `string`):

Optional inclusive latest match date in YYYY-MM-DD format.

## `proxyConfiguration` (type: `object`):

Optional proxy settings for networks where Tennis Abstract blocks direct requests. Direct public access is used by default.

## Actor input object example

```json
{
  "playerSlugs": [
    "NovakDjokovic"
  ],
  "startUrls": [
    {
      "url": "https://www.tennisabstract.com/cgi-bin/player.cgi?p=NovakDjokovic"
    }
  ],
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

Open the default dataset in its player-match table view.

# 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 = {
    "playerSlugs": [
        "NovakDjokovic"
    ],
    "startUrls": [
        {
            "url": "https://www.tennisabstract.com/cgi-bin/player.cgi?p=NovakDjokovic"
        }
    ],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/tennis-abstract-player-match-history-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 = {
    "playerSlugs": ["NovakDjokovic"],
    "startUrls": [{ "url": "https://www.tennisabstract.com/cgi-bin/player.cgi?p=NovakDjokovic" }],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/tennis-abstract-player-match-history-scraper").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "playerSlugs": [
    "NovakDjokovic"
  ],
  "startUrls": [
    {
      "url": "https://www.tennisabstract.com/cgi-bin/player.cgi?p=NovakDjokovic"
    }
  ],
  "maxItems": 20
}' |
apify call automation-lab/tennis-abstract-player-match-history-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=automation-lab/tennis-abstract-player-match-history-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/iMBhCFPaFGnejK2co/builds/fn4w92gaVEeG7KrgC/openapi.json
