# Tennis Abstract Player Elo and Surface Statistics (`automation-lab/tennisabstract-player-statistics`) Actor

Export Tennis Abstract player Elo ratings, current rankings and aggregate surface serving and returning statistics for scouting, including Alcaraz and Sinner profiles.

- **URL**: https://apify.com/automation-lab/tennisabstract-player-statistics.md
- **Developed by:** [Automation Lab](https://apify.com/automation-lab) (community)
- **Categories:** Sports, Automation
- **Stats:** 3 total users, 2 monthly users, 95.8% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.53 / 1,000 item extracteds

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 Player Elo and Surface Statistics

Refresh Tennis Abstract player Elo ratings, current tour rankings and aggregate serving/returning statistics for scouting and analytics. An **Alcaraz Tennis Abstract** lookup can return Carlos Alcaraz's player snapshot and career or last-52-week surface splits without exporting individual match histories. Jannik Sinner profiles work through the same route.

### What does this Actor export?

One default-dataset record per unique player, containing:

- the source's player name, slug and country;
- current official tour rank when available;
- current all-surface Elo rank and rounded profile rating when available;
- career and/or rolling last-52-week tour-level surface aggregates;
- serving, returning and win/loss metrics with source provenance;
- an extraction timestamp and a verified SofaScore ID when in the curated crosswalk.

It does not return match histories, live scores, odds, individual point sequences, charting tables or a complete Elo leaderboard.

### Who is it for?

Scouts can compare player performance on hard, clay and grass courts. Analysts can refresh player-level features for models without first collecting thousands of match rows. Editors can check player rankings and aggregate records against the linked source.

### Why use this Actor?

The output separates player snapshots from surface aggregates. All routes use the same filters and global player limit. Missing data remains null or absent rather than being invented. The Actor reads Tennis Abstract's own aggregate tables instead of pretending that a small recent-match sample represents an entire career.

### Getting started

1. Enter exact Tennis Abstract slugs, for example `CarlosAlcaraz` and `JannikSinner`.
2. Select `career`, `last52`, or both periods.
3. Select the surfaces you need.
4. Set a maximum number of unique player profiles.
5. Run the Actor and open the **Player statistics** dataset.
6. Export JSON for nested aggregates, or flatten the array downstream for tabular analysis.

```json
{
  "playerSlugs": ["CarlosAlcaraz", "JannikSinner"],
  "periods": ["career", "last52"],
  "surfaces": ["Hard", "Clay", "Grass"],
  "maxItems": 2
}
```

### Alcaraz Tennis Abstract and Sinner inputs

Use exact source identifiers, not fuzzy player names. Carlos Alcaraz uses `CarlosAlcaraz`; Jannik Sinner uses `JannikSinner`. Other valid source slugs, such as `NovakDjokovic`, can be supplied, but their available statistics depend on Tennis Abstract's current page.

Profile URLs also work:

```json
{
  "playerUrls": ["https://www.tennisabstract.com/cgi-bin/player.cgi?p=CarlosAlcaraz"],
  "periods": ["last52"],
  "surfaces": ["Hard"]
}
```

### Optional SofaScore mapping

`sofaScoreIds` uses a deliberately bounded, verified crosswalk. It currently supports only **206570 → JannikSinner**. This mapping was checked against SofaScore's public player response. The Actor does not call SofaScore during ordinary runs and does not return SofaScore statistics.

Unknown IDs fail clearly; they are never fuzzily assigned to another player. Use a Tennis Abstract slug or URL for Alcaraz and all players outside this crosswalk. This is not a universal SofaScore-to-Tennis-Abstract resolver.

```json
{ "sofaScoreIds": [206570], "surfaces": ["Hard"], "periods": ["career"] }
```

### Input parameters

| Parameter | Default | Behavior |
|---|---|---|
| `playerSlugs` | None | Exact, case-sensitive source identifiers; at most 100 entries |
| `playerUrls` | None | Source profile URLs on tennisabstract.com or www.tennisabstract.com only; at most 100 entries |
| `sofaScoreIds` | None | Supported verified integer IDs only; currently 206570 |
| `periods` | Both | Nonempty array containing `career` and/or `last52` |
| `surfaces` | All four | Nonempty array containing exact `Hard`, `Clay`, `Grass`, `Carpet` labels |
| `maxItems` | 10 | Global unique-player profile limit from 1 to 100; not a surface-row limit |
| `proxyConfiguration` | Apify datacenter | Set useApifyProxy false for direct HTTP or supply custom proxies; Apify RESIDENTIAL groups rejected |

At least one input route is required. Slugs, URLs and IDs are combined in that order, then deduplicated before limiting. Identical filters apply to every route. Zero and unlimited limits are unsupported. Run and billing limits can stop before the requested profile count.

### Output fields

| Field | Meaning |
|---|---|
| `playerSlug`, `playerName` | Source identity, validated against the profile |
| `country` | Published country code or null |
| `currentRank` | Official tour rank published on the profile, or null |
| `eloRank`, `eloRating` | Current all-surface profile Elo; source rating is rounded |
| `sofaScoreId` | Verified curated mapping when available; otherwise null |
| `surfaceStats` | Selected source aggregate rows, not individual matches |
| `sourceUrl` | Canonical player profile URL |
| `statisticsSourceUrl` | Exact public fragment containing aggregate tables |
| `scrapedAt` | UTC extraction timestamp, not source-update time |

Each aggregate has period, surface, matches, wins, losses, win percentage, matches with statistics, hold/break percentages, ace/double-fault percentages, first-serve-in percentage, first/second-serve points won percentages, service/return/total points won percentages and dominance ratio.

Percentage values use **0–100 percentage points**: `67.5` means 67.5%, not 0.675. Dominance ratio is not a percentage. The number of matches with statistics can be smaller than the total matches played.

### Example output

This shortened example reflects the locally observed Alcaraz record. The complete result includes all selected surface rows and metrics.

```json
{
  "playerSlug": "CarlosAlcaraz",
  "playerName": "Carlos Alcaraz",
  "country": "ESP",
  "currentRank": 3,
  "eloRank": 2,
  "eloRating": 2149,
  "sofaScoreId": null,
  "surfaceStats": [{
    "period": "career", "surface": "Hard",
    "matches": 212, "wins": 166, "losses": 46,
    "winPercent": 78.3, "statsMatches": 209,
    "servicePointsWonPercent": 67.5,
    "returnPointsWonPercent": 41.0,
    "dominanceRatio": 1.26
  }],
  "sourceUrl": "https://www.tennisabstract.com/cgi-bin/player.cgi?p=CarlosAlcaraz",
  "statisticsSourceUrl": "https://www.tennisabstract.com/jsfrags/CarlosAlcaraz.js",
  "scrapedAt": "2026-10-05T06:00:00.000Z"
}
```

Values are snapshots and will change. Never treat this sample as a current ranking feed.

### How much does it cost to export Tennis Abstract player statistics?

Pricing is pay-per-event: one `start` event per valid run and one `item` event per exported player profile. Nested surface aggregates have no separate event charge. Filtered, duplicate and failed player records are not charged as items.

The start event is **$0.005 per run**. Profile prices follow your qualifying aggregate monthly Apify Store spend tier, not a private per-Actor volume schedule:

| Tier | Per player profile |
|---|---:|
| FREE | $0.0067712 |
| BRONZE | $0.005888 |
| SILVER | $0.0045926 |
| GOLD | $0.0035328 |
| PLATINUM | $0.0035328 |
| DIAMOND | $0.0035328 |

At BRONZE, one profile is approximately **$0.010888**, five **$0.03444**, ten **$0.06388**, and 100 **$0.5938**, including one start event. At FREE, two profiles are approximately **$0.0185424**. These are Actor-event estimates, not guaranteed invoices; check the active Store pricing panel before running.

### Freshness and missing data

The Actor reports what the source publishes when fetched. The profile Elo is rounded and may differ from the more precise weekly leaderboard rating. Rankings and statistics can have different source update cadences; extraction time is not their effective ranking date.

Career and `last52` refer to source-published tour-level aggregates. No custom date filtering or historical snapshot selection is offered. A requested surface with no published row is absent. A valid profile can therefore have an empty `surfaceStats` array. Missing ranking or metric values are null rather than zero.

### Limits and failure behavior

The Actor processes players sequentially and requests only the profile and statistics fragment. Datacenter proxy sessions and request pacing limit shared-IP rate limiting. Transient network errors, rate limits and selected server errors have bounded retries. A permanent upstream block or missing/mismatched profile fails the run instead of silently returning a successful empty result.

If a later player fails, earlier exported profiles may remain in the dataset. Such a run is a failed partial result, not proof that all requested players were processed. Check run status before treating an export as complete.

### Integrations

Schedule recurring snapshots and write the default dataset to a warehouse or spreadsheet. Flatten `surfaceStats` into one row per player/period/surface for BI charts. Join snapshots on `playerSlug` and compare extraction timestamps in your own system.

The Actor does not store cross-run history, calculate alerts or perform change detection. Those functions belong in your downstream automation. Apify exports and integrations can connect datasets to Google Sheets, Make, Zapier or your own API consumer.

### API usage

Start the Actor with your Apify token stored outside source:

```bash
curl -X POST \
  'https://api.apify.com/v2/acts/automation-lab~tennisabstract-player-statistics/runs' \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"playerSlugs":["CarlosAlcaraz"],"maxItems":1}'
```

JavaScript with the Apify client:

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/tennisabstract-player-statistics')
  .call({ playerSlugs: ['CarlosAlcaraz'], maxItems: 1 });
const { items } = await client.dataset(run.defaultDatasetId).listItems({ limit: 10 });
console.log(items);
```

Python:

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('automation-lab/tennisabstract-player-statistics').call(
    run_input={'playerSlugs': ['JannikSinner'], 'maxItems': 1})
items = client.dataset(run['defaultDatasetId']).list_items(limit=10).items
```

Check terminal status and paginate dataset reads with explicit limits. A run ID is not the player data.

### MCP use

Scope discovery to this Actor:

```bash
claude mcp add --transport http apify \
  'https://mcp.apify.com?tools=automation-lab/tennisabstract-player-statistics'
```

Claude Desktop, Cursor, and VS Code HTTP MCP configuration (use the equivalent server entry for your client):

```json
{"mcpServers":{"apify":{"url":"https://mcp.apify.com?tools=automation-lab/tennisabstract-player-statistics"}}}
```

Discover actual argument schemas with scoped `tools/list`. This endpoint exposes `automation-lab--tennisabstract-player-statistics` and run/dataset/KV/abort helpers, not `call-actor`. Discovery does not authorize execution or aborting a run.

For an authorized invocation, start **once** with the exposed Actor tool:

```json
{"playerSlugs":["CarlosAlcaraz","JannikSinner"],"maxItems":2,"waitSecs":30}
```

Its `waitSecs` is 0–45 seconds (default 30), not a completion guarantee. Retain the returned `runId`, `status` and `storages.datasets.default.id` (the run API calls this `defaultDatasetId`); these are metadata, not player rows. If nonterminal, use `get-actor-run` for that **same** run:

```json
{"runId":"<returned runId>","waitSecs":0}
```

Use bounded host-side backoff of 2, 4, 8 seconds, capped at 10, with a total 120-second deadline from invocation. Set the client's request timeout above the selected server wait plus transport margin, within that deadline. After a client timeout, check the known run rather than starting another; if the run ID is unknown, report uncertainty without blindly rerunning. At the deadline, report **pending** with the run ID and stop polling. Report FAILED, ABORTED or TIMED-OUT honestly; available rows are **failed partial output**, never a successful full export.

After SUCCEEDED, call `get-dataset-items` with the returned default dataset ID:

```json
{
  "datasetId":"<storages.datasets.default.id>",
  "limit":20,
  "offset":0,
  "fields":"playerSlug,playerName,eloRank,eloRating,surfaceStats,sourceUrl"
}
```

Use untransformed JSON and pages of 20 source rows from offset 0. Continue at source offsets 20, 40, and so on, using the returned offset/limit/totalItemCount metadata to detect exhaustion; never advance by a count of flattened surface rows or model-visible summaries. Keep the same dataset ID.

Admit at most **100 source rows or 64 KiB of serialized UTF-8 result content** across all pages to model context, whichever comes first. Process pages host-side before model injection and enforce the byte ceiling even for one oversized row/page: omit or summarize it with explicit disclosure. A client without host-side interception cannot guarantee that byte ceiling and must not request unbounded results. Stop at exhaustion or budget and report returned rows, the next source continuation offset, and partial/truncated status when incomplete. Full exports belong in storage or downstream processing outside model context. This consumer context budget does not reduce Actor collection limits or fields.

Example prompt: “Export Alcaraz's career and last-52-week Hard statistics, then show the ranking, Elo and return-points-won percentages.”

### Legality and diagnostics

This independent tool is not affiliated with Tennis Abstract or SofaScore. Use public data responsibly, respect source terms and applicable laws, and do not imply that scraped ratings guarantee sporting or betting outcomes.

Failed operations send sanitized diagnostic input, exceptions and actor/build/run IDs to our private GlitchTip service for repair. Secret fields and URL queries are removed. Reports are retained for 30 days.

No AI is used at runtime or to generate output statistics. Apify hosts the run, logs and dataset and routes configured datacenter proxy traffic; those operating costs belong to the Actor operator, not an undisclosed third-party subscription. User-supplied custom proxies are the user's own dependency. Public player identifiers are sent to the source, and processed output contains public sporting names and statistics, not user-private accounts.

Run storage follows your Apify account's retention settings (31 days on our current plan); users can delete datasets and runs through Apify. The Actor keeps no persistent business cache or cross-run player history. Contact us through the Actor's Apify Store Issues tab for support. Pricing and export examples are estimates, not guaranteed invoices or payouts; adjustments, refunds, fraud, disputes, taxes, corrections and contractual clawbacks may affect final amounts.

### FAQ and troubleshooting

**Does it export individual matches?** No. Use the related match-history Actor below for that output.

**Why is a surface absent?** Tennis Abstract did not publish that aggregate row for the selected period/player. Absence does not mean zero wins or zero matches.

**Why is Elo null?** The source profile may not have an eligible/current Elo rating. No rating is inferred from match history.

**Can I pass any SofaScore ID?** No. Only explicitly verified crosswalk IDs work; currently Jannik Sinner 206570. Supply the source slug or profile URL for other players.

**Why did a valid-looking name fail?** Slugs are exact and case-sensitive. Check the `p` value in the source profile URL. Full display names are not automatically searched.

**Is there a Tennis Abstract API or GitHub dataset behind this?** The Actor parses public profile/fragment pages. It does not claim an official Tennis Abstract API and does not import a GitHub historical dataset.

**What if the source blocks my network?** Try a configured datacenter or custom proxy if appropriate. Apify RESIDENTIAL groups are rejected; there is no automatic paid fallback. Failed upstream pages are not charged as exported profiles.

### Related automation

- [Tennis Abstract Player Match History Scraper](https://apify.com/automation-lab/tennis-abstract-player-match-history-scraper): dated match rows, opponents and scores rather than player aggregates.
- [WTA Tennis Rankings Scraper](https://apify.com/automation-lab/wta-rankings): official WTA ranking snapshots rather than Tennis Abstract profile Elo and serving/returning aggregates.

# Changelog

This Actor's version history is a separate document: https://apify.com/automation-lab/tennisabstract-player-statistics/changelog.md

# Actor input Schema

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

Exact case-sensitive Tennis Abstract player slugs, such as CarlosAlcaraz or JannikSinner. Combined with player URLs and supported SofaScore IDs; duplicates removed before the global profile limit.

## `playerUrls` (type: `array`):

Tennis Abstract /cgi-bin/player.cgi?p= profile URLs only. The same surface/period filters apply as for slugs. Only tennisabstract.com and www.tennisabstract.com are accepted.

## `sofaScoreIds` (type: `array`):

Optional curated crosswalk: only 206570 (Jannik Sinner) is currently verified and supported. Unknown IDs fail; use playerSlugs or playerUrls for all other players. Does not fetch SofaScore statistics or promise universal mapping.

## `periods` (type: `array`):

Nonempty array of career and/or last52. Both by default. last52 is the source's rolling last-52-week aggregate, not a custom date range. Does not change the profile ranking/Elo snapshot.

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

Nonempty list of exact surface labels Hard, Clay, Grass and/or Carpet. All by default. Only source-published aggregate rows are included; missing surfaces remain absent rather than zero-filled. Profile identity/ranking remains even if no requested surface rows exist.

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

Global maximum unique player records (not surface rows), 1–100, default 10. Slugs first, then URLs, then IDs in input order. Billing/run limits can stop earlier. Zero and unlimited are not supported.

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

Apify datacenter proxy by default to avoid shared-IP rate limits. Set useApifyProxy false for direct HTTP, or supply custom proxies. Apify RESIDENTIAL groups are rejected; no automatic residential fallback.

## Actor input object example

```json
{
  "playerSlugs": [
    "CarlosAlcaraz",
    "JannikSinner"
  ],
  "periods": [
    "career",
    "last52"
  ],
  "surfaces": [
    "Hard",
    "Clay",
    "Grass",
    "Carpet"
  ],
  "maxItems": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Typed player records with nested aggregate surface rows.

# 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": [
        "CarlosAlcaraz",
        "JannikSinner"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/tennisabstract-player-statistics").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": [
        "CarlosAlcaraz",
        "JannikSinner",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/tennisabstract-player-statistics").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 '{
  "playerSlugs": [
    "CarlosAlcaraz",
    "JannikSinner"
  ]
}' |
apify call automation-lab/tennisabstract-player-statistics --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation-lab/tennisabstract-player-statistics"
        }
    }
}
```

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/bZX2aegJe4ZoThan1/builds/DffcKS8vx0Pevx3w6/openapi.json
