# Tennis Abstract Scraper: Player Stats & Match Results (`accountable_eel/tennis-abstract-stats-lookup`) Actor

Tennis Abstract scraper: one row per match (date, tournament, surface, round, opponent, score, rankings, serve and return stats) plus a player summary with win % by surface. ATP and WTA. New-match alerts in Slack, Discord or Telegram. No login; unknown players are free.

- **URL**: https://apify.com/accountable_eel/tennis-abstract-stats-lookup.md
- **Developed by:** [Adriana Voss](https://apify.com/accountable_eel) (community)
- **Categories:** Sports, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 match row returneds

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Tennis Abstract Scraper: Player Stats & Match Results

Type a player's name, get their **recent matches from Tennis Abstract**, one row per match: date, tournament, surface, round, opponent, result, score, both players' rankings, and the serve and return numbers the site publishes (aces, double faults, first-serve %, break points, dominance ratio). Add a **player summary row** with current and peak rank, country, and win % overall and by surface. ATP and WTA players both work.

Switch on monitoring and the actor remembers which matches each player had last time, returns only the **new ones**, and posts them to Slack, Discord or Telegram.

### Who uses this

- **Bettors and modellers** who need recent form, surface splits and serve / return rates per player, as rows they can load into a spreadsheet or a model.
- **Fantasy players and tipsters** who want to know when a player's tournament run moves on, without opening a page.
- **n8n / Make / Clay builders and sports-data hobbyists** who want Tennis Abstract's numbers inside a workflow, without a login or an API key.

### Try it in 30 seconds

Press **Start** with the pre-filled input. It reads Novak Djokovic's page and returns his player summary plus his most recent matches:

```json
{ "players": ["Novak Djokovic"], "maxMatchesPerPlayer": 50 }
```

Write your own lines like this, one player per line:

```text
Novak Djokovic
Iga Swiatek
Felix Auger-Aliassime
https://www.tennisabstract.com/cgi-bin/player.cgi?p=CarlosAlcaraz
```

Names are matched the way Tennis Abstract spells them: first and last name, accents and hyphens dropped (Auger-Aliassime becomes FelixAugerAliassime on the site, and you can type either). A pasted player link is read as text for the name.

### What you get

Two kinds of row, told apart by `rowType`:

- **`match-row`**: `player`, `tour`, `date`, `tournament`, `surface`, `level`, `round`, `result` (W or L), `opponent`, `opponentCountry`, `score` (winner's games first), `bestOf`, `playerRank`, `opponentRank`, `minutes`, `aceRatePct`, `doubleFaultRatePct`, `firstServeInPct`, `firstServeWonPct`, `secondServeWonPct`, `breakPointsSaved`, `breakPointsFaced`, `dominanceRatio`, and for players with a current history file also `aces`, `doubleFaults`, `servePoints` and `returnPointsWonPct`. `source` says where the row came from.
- **`player-row`**: `player`, `tour`, `country`, `birthYear`, `hand`, `currentRank`, `peakRank`, `eloRating`, `eloRank`, then arithmetic over the matches in the sample: `matchesPlayed`, `wins`, `losses`, `winPct`, `sampleFrom`, `sampleTo`, and matches and win % on hard, clay, grass and carpet.

Filters are applied after the fetch: `since` (a date), `surface`, `maxMatchesPerPlayer`. The player summary then covers exactly the matches those filters leave.

### Example output

```json
{
  "rowType": "match-row",
  "player": "Novak Djokovic",
  "tour": "atp",
  "date": "2026-10-07",
  "tournament": "Shanghai Masters",
  "surface": "Hard",
  "round": "R64",
  "result": "L",
  "opponent": "Hubert Hurkacz",
  "opponentCountry": "POL",
  "score": "6-4 6-3",
  "playerRank": 8,
  "opponentRank": 34,
  "minutes": 87,
  "aceRatePct": 3.6,
  "firstServeInPct": 53.6,
  "dominanceRatio": 0.72,
  "source": "recent-results"
}
```

### Monitoring: new-match alerts in three steps

1. Tick **Track players across runs** and give the watchlist a name (`deltaName`), for example `my-players`.
2. Tick **Only return new matches** so a run with nothing new costs nothing, and paste a Slack, Discord or Telegram link into **Send alerts to**.
3. Save the run as a task and schedule it daily. The first run stores a baseline (tick **Seed silently** to also skip billing it); from the second run on, every new match arrives as a row and as one message per player.

The watchlist is kept in a named store under your account, so it survives between scheduled runs.

### Limits, stated plainly

- **Data as Tennis Abstract publishes it.** Matches, rankings and serve numbers are the site's own. There are no live scores, no betting odds, no point-by-point.
- **How far back depends on the player.** Every player comes with about the last 20 matches from the site's Recent Results table. Some players also have a current match-history file that reaches back further (up to roughly 140 matches); for others that file is out of date, and the row says `historyDepth: "recent-only"` rather than mixing in years-old matches across a gap.
- **Dates are tournament start dates.** Tennis Abstract dates every round of a tournament with the day it started.
- **Serve and return stats** come with each match when the site has them; older or lower-level matches can have blanks.
- **Names must match the site.** A player Tennis Abstract does not list, or a misspelled name, comes back as not found (free). Paste the player link if a name does not match.
- **About 25 seconds per player.** Each player takes three requests to the site, one second apart at the fastest, and the platform's proxy adds a few seconds to each. A run holds up to 150 players, but the run's time limit decides how many it gets through: players it has no time for come back as free "not checked" rows, so run them again.
- **Misses are free.** Not found, blocked, layout change: nothing is charged.

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `players` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~tennis-abstract-stats-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"players":["Novak Djokovic"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

### Input

```json
{
  "players": [
    "Novak Djokovic"
  ]
}
```

One player per line: the first and last name ("Novak Djokovic", "Iga Swiatek"; accents and hyphens are fine) or a tennisabstract.com player link. ATP and WTA players both work.. Plan on about 25 seconds per player; players the run has no time for come back as free "not checked" rows. Accepted formats: Novak Djokovic, Iga Swiatek, Felix Auger-Aliassime, https://www.tennisabstract.com/cgi-bin/player.cgi?p=CarlosAlcaraz.

```json
{
  "items": ["<identifier-1>", "<identifier-2>"],
  "maxConcurrency": 5
}
```

`items` is a list of ATP or WTA player names or Tennis Abstract player links. One dataset row is returned
per item; rows with `"found": false` are never charged.

### Output

One row per item, for example:

| query | found | status | rowType | player | tour | country | birthYear | hand | currentRank | peakRank | eloRating | eloRank | matchesPlayed | wins | losses | winPct | sampleFrom | sampleTo | hardMatches | hardWinPct | clayMatches | clayWinPct | grassMatches | grassWinPct | carpetMatches | carpetWinPct | historyDepth | date | tournament | surface | level | round | result | opponent | opponentCountry | score | bestOf | playerRank | opponentRank | minutes | aces | doubleFaults | servePoints | aceRatePct | doubleFaultRatePct | firstServeInPct | firstServeWonPct | secondServeWonPct | breakPointsSaved | breakPointsFaced | returnPointsWonPct | dominanceRatio | source | isNewMatch | playerUrl | checkedAt | changeType | newMatchCount | daysSincePrevious | notifyErrors | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Novak Djokovic | true | OK | <row type> | <player> | <tour> | <country> | <birth year> | <plays> | <current rank> | <peak rank> | <elo rating> | <elo rank> | <matches in sample> | <wins> | <losses> | \<win %> | <sample from> | <sample to> | <hard-court matches> | \<hard-court win %> | <clay matches> | \<clay win %> | <grass matches> | \<grass win %> | <carpet matches> | \<carpet win %> | <history depth> | <date> | <tournament> | <surface> | <level> | <round> | <result> | <opponent> | <opponent country> | <score> | <best of> | <player rank> | <opponent rank> | <minutes> | <aces> | <double faults> | <serve points> | \<ace %> | \<double fault %> | \<first serve in %> | \<first serve points won %> | \<second serve points won %> | <break points saved> | <break points faced> | \<return points won %> | <dominance ratio> | <source> | <new since last check> | <tennis abstract link> | <checked at> | <change since last check> | <new matches> | <days since last check> | <alert errors> | 1970-01-01T00:00:00.000Z |

A miss comes back as a row with `"found": false` and is never charged.

### Pricing

Pay-per-event. A flat per-run fee covers session/proxy warmup; you're billed
per item only when data is actually found and returned — see
`.actor/pay_per_event.json` for exact prices. A miss is never charged.

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~tennis-abstract-stats-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"players":["Novak Djokovic"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~tennis-abstract-stats-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"players":["Novak Djokovic"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~tennis-abstract-stats-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"players":["{{player}}"]}`, mapping the row's player into the `players` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "Tennis Abstract Scraper | Apify" — the agent will find and run this actor.

### FAQ

**Is this an official Tennis Abstract API?** No. It reads the public player pages and the data files those pages load, without logging in. It is not affiliated with Tennis Abstract.

**ATP and WTA?** Both. `tour` on every row says which. Type the name; there is no tour setting to get wrong.

**Why do some players have only about 20 matches?** The site's Recent Results table holds roughly the last 20 matches for everyone. A deeper history file exists for many players, but for some of them it has stopped updating; the actor only uses it when it joins up with the recent matches.

**What does the dominance ratio mean?** Tennis Abstract's DR: the share of return points won divided by the share of serve points lost. Above 1, the player won more return points than serve points they lost.

**Why is the date the same for every round of a tournament?** The site dates each match with the tournament's start date. Use `round` to order matches inside a tournament; rows are already sorted newest first.

**What costs money?** Each match row and each player summary row you receive. Players that are not found, blocked or unreadable cost nothing, and with **Only return new matches** a player with nothing new costs nothing.

# Actor input Schema

## `players` (type: `array`):

One player per line: the first and last name ("Novak Djokovic", "Iga Swiatek"; accents and hyphens are fine) or a tennisabstract.com player link. ATP and WTA players both work.. Plan on about 25 seconds per player; players the run has no time for come back as free "not checked" rows. Accepted formats: Novak Djokovic, Iga Swiatek, Felix Auger-Aliassime, https://www.tennisabstract.com/cgi-bin/player.cgi?p=CarlosAlcaraz. You're only charged for the ones we actually find — a miss costs nothing.

## `since` (type: `string`):

Optional. Keeps matches dated on or after this day (Tennis Abstract dates a tournament by its start date). Applied after the fetch. Leave empty for every match the site holds. The player summary counts only these matches.

## `surface` (type: `string`):

Keeps only matches on this surface. Applied after the fetch; the player summary then covers that surface only.

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

Newest first. About 20 recent matches come for every player; players whose history file is current go back further (up to ~140 matches). 0 returns only the player summary row. Each match row is billed at the match-row price.

## `includePlayerSummary` (type: `boolean`):

One extra row per player: rank, country, win % overall and by surface over the matches in the sample. Billed at the player-row price.

## `deltaMode` (type: `boolean`):

Turns the actor into a match monitor. Each run compares every player's matches with the previous run and fills in "New since last check", the change type and the number of new matches. Schedule it daily and combine with "Only return new matches" and "Send alerts to".

## `deltaName` (type: `string`):

Leave empty and one is derived from your filters. Type your own name to keep one memory across changes, or to run several independent watchlists.

## `alertOnChangeOnly` (type: `boolean`):

With monitoring on, a player with no new match writes no row and is not billed; a player with new matches returns only those matches (and the summary row). The first check of a player returns everything as the baseline.

## `skipFirstRun` (type: `boolean`):

The first check of a player has nothing to compare with. Turn this on to remember it without returning or billing it, so alerts start from the second check.

## `notifyUrl` (type: `string`):

Optional. After a player is billed, one message lists that player's new matches (date, round, tournament, opponent, score) with a Tennis Abstract link. Discord or Slack: paste the channel's webhook URL. Telegram: https://api.telegram.org/bot<token>/sendMessage?chat_id=<chat id>. Any other URL receives JSON. Messages are free, and a failed message never fails the run (the row's notifyErrors says why). Stored encrypted, never logged or written to the results.

## `notifyFormat` (type: `string`):

Leave on 'Detect from the link': discord.com links get Discord messages, hooks.slack.com Slack, api.telegram.org Telegram, anything else JSON. Pick one only to override that.

## `maxNotificationsPerRun` (type: `integer`):

Up to 100. After this, one "more alerts" message says the rest are in the run's dataset. 0 sends only that one message.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

## `maxResults` (type: `integer`):

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

## `maxConcurrency` (type: `integer`):

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

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

Apify Proxy config. Residential recommended for anti-bot-sensitive targets.

## Actor input object example

```json
{
  "players": [
    "Novak Djokovic"
  ],
  "since": "",
  "surface": "any",
  "maxMatchesPerPlayer": 50,
  "includePlayerSummary": true,
  "deltaMode": false,
  "deltaName": "",
  "alertOnChangeOnly": false,
  "skipFirstRun": false,
  "notifyFormat": "auto",
  "maxNotificationsPerRun": 20,
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "columns": [
    "rowType",
    "player",
    "tour",
    "country",
    "birthYear",
    "hand",
    "currentRank",
    "peakRank",
    "eloRating",
    "eloRank",
    "matchesPlayed",
    "wins",
    "losses",
    "winPct",
    "sampleFrom",
    "sampleTo",
    "hardMatches",
    "hardWinPct",
    "clayMatches",
    "clayWinPct",
    "grassMatches",
    "grassWinPct",
    "carpetMatches",
    "carpetWinPct",
    "historyDepth",
    "date",
    "tournament",
    "surface",
    "level",
    "round",
    "result",
    "opponent",
    "opponentCountry",
    "score",
    "bestOf",
    "playerRank",
    "opponentRank",
    "minutes",
    "aces",
    "doubleFaults",
    "servePoints",
    "aceRatePct",
    "doubleFaultRatePct",
    "firstServeInPct",
    "firstServeWonPct",
    "secondServeWonPct",
    "breakPointsSaved",
    "breakPointsFaced",
    "returnPointsWonPct",
    "dominanceRatio",
    "source",
    "isNewMatch",
    "playerUrl",
    "checkedAt",
    "changeType",
    "newMatchCount",
    "daysSincePrevious",
    "notifyErrors"
  ],
  "maxConcurrency": 1,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (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 = {
    "players": [
        "Novak Djokovic"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/tennis-abstract-stats-lookup").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 = {
    "players": ["Novak Djokovic"],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/tennis-abstract-stats-lookup").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 '{
  "players": [
    "Novak Djokovic"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/tennis-abstract-stats-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,accountable_eel/tennis-abstract-stats-lookup"
        }
    }
}
```

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/ev8d4ubcLmJ42ZlAI/builds/XoDNJ3Vo6NiaGTDfO/openapi.json
