# Tennis Stats API (`asgardlabs/tennis-abstract-api`) Actor

ATP and WTA player stats as structured tables: bio, rankings, Elo, results, head-to-head. One cent per player or head-to-head, a twentieth of a cent per ranking row.

- **URL**: https://apify.com/asgardlabs/tennis-abstract-api.md
- **Developed by:** [Asgard Labs](https://apify.com/asgardlabs) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 player profiles

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 Stats API – ATP & WTA rankings, Elo, results, head-to-head

Give it player names. Get each player's profile from Tennis Abstract: country, birth date, height, playing hand and backhand, current and peak ranking, Elo rating and rank, plus stat tables: recent results with score, surface, round and serve stats, season records, career and last-52-weeks splits by surface, year-end rankings, most frequent opponents, and the Match Charting Project serve, return, rally and tactics tables when you ask for them.

Two more modes: the full **ATP or WTA rankings and Elo tables**, and a **head-to-head** between any two players.

Static pages, no login, no browser. Runs through Apify Proxy, nothing to set up. ATP and WTA.

### Tennis stats data per player, ranking and head-to-head

- **players** – one record per player: `player` (Tennis Abstract slug), `fullName`, `tour`, `country`, `birthDate`, `heightCm`, `hand`, `backhand`, `active`, `currentRank`, `peakRank`, `peakRankFirst`, `peakRankLast`, `eloRating`, `eloRank`, ids, `photo`, `tables` (each with the site's headers and rows; results rows carry `match`, `opponent` and `opponentKey`).
- **rankings / elo** – one record per row: `tour`, `rank`, `player`, `country`, `birthdate`, `playerKey`, or the Elo columns (`elo`, `helo`, `celo`, `gelo`, `peak_elo`…).
- **h2h** – `summary` (meetings, wins, losses, tiebreaks, surface stats) and `recentMatches`. `found` is false when the two never met.

### Use cases

- Tennis apps and bots.
- Betting models.
- Fantasy tools.
- Journalism and research datasets.

### Input

| Field | Default | Meaning |
|---|---|---|
| `mode` | players | `players`, `rankings`, `elo` or `h2h`. |
| `players` | – | Names or Tennis Abstract slugs. Accents are fine ("Novak Đoković"). A unique last name works ("Alcaraz"); an ambiguous one is reported with the candidates. The same player written twice counts once. |
| `tour` | auto | `auto` reads the tour from the rankings; `atp` or `wta` to force it. |
| `tables` | 7 defaults | Table ids: recent-results, tour-years, chall-years, recent-finals, year-end-rankings, recent-events, career-splits, last52-splits, winners-errors, serve-speed, pbp-points, pbp-games, pbp-stats, mcp-serve, mcp-return, mcp-rally, mcp-tactics, head-to-heads. |
| `tours`, `limit` | atp + wta, all rows | Rankings and Elo modes. |
| `pairs` | – | "Carlos Alcaraz vs Jannik Sinner". |
| `timeoutSecs` | 120 | Time budget per player. |
| `concurrency` | 3 | Players in parallel. |

### Output example

```json
{ "player": "CarlosAlcaraz", "fullName": "Carlos Alcaraz", "tour": "ATP", "country": "ESP", "birthDate": "2003-05-05", "heightCm": 183, "hand": "right", "backhand": "two-handed",
  "currentRank": 3, "peakRank": 1, "peakRankFirst": "2022-09-12", "eloRating": 2147, "eloRank": 2, "atpId": "A0E2", "twitter": "carlosalcaraz",
  "tables": { "recent-results": { "headers": ["Date", "Tournament", "Surface", "Rd", "Rk", "vRk", "match", "Score", "DR", "A%", "DF%", "1stIn", "1st%", "2nd%", "BPSvd", "Time"],
                                  "rows": [{ "Date": "31-Aug-2026", "Tournament": "US Open", "Surface": "Hard", "Rd": "QF", "match": "(2)Alcaraz vs (8)Berrettini", "opponent": "Berrettini", "opponentKey": "MatteoBerrettini", "Score": "6-1 6-3 6-4", "Time": "2:04" }] },
             "head-to-heads": { "rows": [{ "Opponent": "Jannik Sinner [ITA]", "opponentKey": "JannikSinner", "Mtgs": "18", "W": "11", "L": "7", "TB W": "9", "TB L": "7" }] } } }
```

### How to use

1. Paste your player names, or a mode such as rankings into the input form. The example input is ready to run as is.
2. Click **Start**. Rows appear in the **Output** tab as they are found.
3. Download the table as CSV, JSON or Excel, or read it through the API.

### Pricing

Pay per event: **`player` $0.01** per profile with all requested tables, **`ranking-row` $0.0005** per rankings or Elo row, **`h2h` $0.01** per head-to-head with meetings on record. The full ATP rankings (about 2,300 rows) is about $1.15. Players that are not found, pairs that never met, and failed requests are free.

### Good to know

- **Head-to-head summary** exists when one player is among the other's 15 most frequent opponents. Otherwise you get the recent meetings only, with a note.
- **Data freshness** is Tennis Abstract's, usually within a day of a match.
- **Numbers stay as text** where the site mixes formats ("83.9%", "64-13"); ranks, Elo and dates in the bio are numbers and ISO dates.
- **Not included**: live scores, odds, draws. The official tour and score sites block automated access.
- **Errors are in plain words:** `player not found`, `ambiguous last name: …`, `timeout`, `rate limited (429)`, `ENOTFOUND (domain not found)`. Your spend limit stops the run cleanly; rows beyond it are neither delivered nor charged.

### Use from code or an AI agent

Every run can be started and read through the Apify API, the JavaScript and Python clients, or an AI agent with tool access. Open the **API** tab on this page for ready-made snippets. Pricing is per event, so an agent pays only for what it receives.

### FAQ

**Are live scores or odds included?**
No. The official tour sites block automated access. Rankings, Elo, results and head-to-head records are.

**Does it work for retired players?**
Yes, as long as the player has a page on the source site. Tour is read from the rankings directory; retired players fall back to the site's own tour label.

**What about common last names like Zverev?**
A last name that matches several players is reported as ambiguous with the candidates, free. Use the full name.

**Are pairs that never played each other charged?**
No. A head-to-head with no meetings on record is returned with `found: false` and is free.

**Where does the data come from and how fresh is it?**
Tennis Abstract, updated after each tournament week. The ranking date is in every rankings row.

# Actor input Schema

## `mode` (type: `string`):

What to fetch: player profiles with stat tables, the current rankings, the Elo ratings, or head-to-head records between pairs.

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

Names with or without accents, a unique last name, or Tennis Abstract slugs. Duplicates count once.

## `tour` (type: `string`):

ATP or WTA for player lookups. "auto" reads the tour from the rankings directory, so a WTA name is not mislabelled.

## `tables` (type: `array`):

Table ids: recent-results, tour-years, chall-years, recent-finals, year-end-rankings, recent-events, career-splits, last52-splits, winners-errors, serve-speed, pbp-points, pbp-games, pbp-stats, mcp-serve, mcp-return, mcp-rally, mcp-tactics, head-to-heads. Empty = default set.

## `tours` (type: `array`):

Which tours to pull in rankings and elo modes: atp, wta or both.

## `limit` (type: `integer`):

Top N rows for rankings and elo modes. 0 = the whole table. Each row is charged separately.

## `pairs` (type: `array`):

e.g. "Carlos Alcaraz vs Jannik Sinner"

## `timeoutSecs` (type: `integer`):

Time budget per player profile. A player that takes longer is reported as timed out and not charged.

## `concurrency` (type: `integer`):

How many player profiles to fetch at the same time. The site rate-limits bursts; three is safe.

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

Tennis Abstract blocks requests from cloud servers, so the actor goes through Apify Proxy (residential) by default. Leave as is unless you have your own proxies.

## Actor input object example

```json
{
  "mode": "players",
  "players": [
    "Carlos Alcaraz",
    "Iga Swiatek"
  ],
  "tour": "auto",
  "tables": [],
  "tours": [
    "atp",
    "wta"
  ],
  "limit": 0,
  "pairs": [],
  "timeoutSecs": 120,
  "concurrency": 3,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (type: `string`):

One row per player (bio + tables), ranking row, Elo row or head-to-head, depending on mode.

## `summary` (type: `string`):

Counts, skipped inputs with reasons, errors in plain words, charge-limit state.

# 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": [
        "Carlos Alcaraz",
        "Iga Swiatek"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("asgardlabs/tennis-abstract-api").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": [
        "Carlos Alcaraz",
        "Iga Swiatek",
    ],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("asgardlabs/tennis-abstract-api").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": [
    "Carlos Alcaraz",
    "Iga Swiatek"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call asgardlabs/tennis-abstract-api --silent --output-dataset

```

## MCP server setup

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

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/4e0vfNOy4help9IHj/builds/59IsmIR83V1219tUO/openapi.json
