# Tennis Abstract Scraper — Score Checks, Model Features & Elo (`apivault_labs/tennis-abstract-bulk-scraper`) Actor

ATP/WTA histories, H2H, rankings, Elo and charted points. Optional ESPN score checks, pre-tournament model features and versioned correction history. No source key/login. $0.19/1K matches plus charting and usage.

- **URL**: https://apify.com/apivault_labs/tennis-abstract-bulk-scraper.md
- **Developed by:** [Apivault Labs](https://apify.com/apivault_labs) (community)
- **Categories:** Sports, Developer tools
- **Stats:** 2 total users, 1 monthly users, 80.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.19 / 1,000 match returneds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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 — ATP/WTA Matches, Elo & Rankings

Read public Tennis Abstract pages with exact player names, slugs or player-page URLs. Collect career singles results, both players' serve/return statistics, head-to-head match rows, current published ranking and Elo tables, and optional point-by-point details for charted matches. No source account, login, cookies or external data-provider key is required.

### Tennis datasets for reproducible match and player research

Use Tennis Abstract Scraper to prepare source-traceable ATP/WTA match tables, rankings/Elo observations or charted-match detail. Analysts can review a player history, compare a defined head-to-head sample or build a feature worksheet with explicit time boundaries. This is published historical/statistical data, not a live scoring or betting service.

| Research task | Start with | Preserve with the export |
|---|---|---|
| Player match history | Exact player and small maxResults | Player perspective, source_match_id and tournament-start date |
| Head-to-head rows | Exact opponent and optional surface | Filters and the distinction from a complete H2H aggregate |
| Rankings or Elo table | rankings or elo | Published source date, tour and row type |
| Feature research | modelFeatures with bounded opponents | Pre-tournament feature cutoff and unproven historical as-of availability |
| Correction observations | Named sequential campaign | Your baseline date and revision context |

Keep record_type with every row. Match, ranking and Elo records use different fields; a single global result cap can be consumed by earlier player matches before table rows are reached.

### Start with 20 matches

```json
{"players":["Novak Djokovic"],"maxResults":20}
```

Names are resolved against Tennis Abstract's own player list and validated against the returned profile. Unknown or ambiguous players produce an explicit error in the run's ERRORS record; they are never silently substituted.

### Modes and filters

| Input | Behavior |
|---|---|
| `players` | Up to 100 exact names, source slugs or Tennis Abstract HTTPS player URLs; ATP/WTA detected automatically |
| `surfaces` | hard, clay, grass, carpet; empty means all |
| `levels` | Source tournament codes such as G (Grand Slam), M (Masters), A, C, Q, P, W |
| `rounds` | Exact source round codes such as F, SF, QF, R16 |
| `results` | win and/or loss |
| `opponent` | Exact opponent name/slug/URL; returns head-to-head **match rows**, not an invented aggregate |
| `maxOpponentRank` | Include opponents ranked this high or better; unknown ranks excluded |
| `fromDate`, `toDate` | Inclusive YYYY-MM-DD **tournament start dates**, not guaranteed actual match days |
| `rankings` | atp, wta or both; no players required |
| `elo` | atp, wta or both; no players required |
| `includePointByPoint` | Optional charted points and 15 source shot-statistics sections |
| `maxResults` | One global budget: matches in player input order, newest first, then rankings, then Elo. Omit for all available rows |

At least one of players, rankings or elo must be supplied. Duplicate requested players are skipped. Each match row retains a player perspective; requesting both opponents can return two perspectives of a match. `match_id` lets downstream workflows identify the shared match.

```json
{"players":["Iga Swiatek"],"opponent":"Aryna Sabalenka","surfaces":["clay"],"maxResults":20}
```

```json
{"rankings":"both","elo":"both","maxResults":5000}
```

```json
{"players":["Novak Djokovic"],"rounds":["QF"],"fromDate":"2026-09-30","toDate":"2026-09-30","includePointByPoint":true,"maxResults":1}
```

### Output

`record_type` separates match, ranking and elo records.

**Matches** include tournament context, score, win/loss, both players' rankings/seeds/entry routes, available country/birthdate/height/hand/backhand data, current and peak player rank/Elo, raw serve/return counts, calculated rates for both players, parsed sets and tiebreaks, retirement/walkover/default markers, source URL and extraction time. Rate fields ending `_pct` are fractions **0–1**, not percentage points. Missing statistics remain null. Current profile rank/Elo are separate from match-time rank; never treat today's Elo as historical match-time Elo.

**Rankings** include published rank, name, country, birthdate, age calculated as of the published ranking date, player link/slug and source ranking date.

**Elo** includes overall rank/rating, hard/clay/grass rank/rating, peak rating/month, tour rank and published log difference.

**Charted matches** can additionally contain `points` (server, set/game/point score, source narrative, rally length when stated and recognized outcome) and `shot_statistics`. Those 15 sections preserve source tables with their original column labels and values: serve, first/second serve, first/second return, key points, rally outcomes, overview, both players' shots, shot direction, net points and serve-neutralization. Availability depends on volunteer charting; an uncharted match has no fabricated point log. A charting fetch failure is reported independently while the useful match row remains available.

Download JSON, CSV or Excel from the Dataset. JSON preserves sets, points and nested shot tables; tabular exports serialize/flatten nested structures. Use the Apify API, client libraries or hosted MCP for programmatic access. Run-completion webhooks and schedules are Apify platform integrations.

### Source behavior and limits

- Ordinary HTTP reads only; remote JavaScript is parsed as data, never executed.
- ATP uses the source's embedded match array. WTA combines current and career files; a failed archive fetch is an error, never advertised as complete history.
- Only completed W/L results are returned. Future fixtures are excluded. RET/W-O records can still be valid results.
- The date often identifies the tournament's start. Several rounds legitimately share one date.
- Source histories, ranking dates and profile snapshots can be stale or disagree; expose their dates rather than claim live-world freshness. The checked Sinner source ended at June 2026.
- Source arrays are the coverage boundary. Availability of lower-tier/older stats varies; no external complete-career guarantee.
- No live scores, betting odds, continuous stream, SofaScore data or doubles extraction. This Actor implements historical Tennis Abstract modes.
- Throttling and unavailable pages are reported. Runs can partly succeed; inspect SUMMARY and ERRORS. Read responses are capped and requests paced.

### Pricing

The Pricing tab is authoritative for active rates, plan discounts, start/accounting events and platform usage. Match records, ranking rows, Elo rows and delivered charted-match detail use separate event units; errors are not custom result events. Resource costs and source requests must be included when budgeting large careers or charting-heavy runs. Set a low maxResults on your first run.

### Local verification and deployment status

The implementation is independently written from public source formats and the competitor's documented behavior. It does not use a competitor's scraper/API or copied source code. Local QA reports are separate from actual Apify build/run evidence; publication is confirmed only in RELEASE_2026-10-14.json.

This is an unofficial tool, unaffiliated with Tennis Abstract, ATP or WTA. Use public data in accordance with the source's terms and applicable requirements. Do not infer authorization for data redistribution from technical accessibility.

### Independent score checks, model features and correction history

These optional modes add no new custom event: normal delivered match pricing still applies, with platform/proxy/storage usage charged separately. They are not advertised as globally exclusive features.

- `verifyMatches: true`: compare each returned match to ESPN's public scoreboard JSON. Participants, winner, set games and available tie-break points are checked. `verification` records matched/conflict/not_found/source_error/request_limit/ambiguous/partial_match; `verified` only means the listed fields matched a second publisher, not all statistics or independent primary provenance. Both sources may share upstream data. Original records are preserved. Secondary actual-match timestamps are separate from primary tournament-start dates. `maxVerificationRequests` defaults to 10 (maximum 50); cache is per tour/tournament date. VERIFICATION_COVERAGE lists secondary records not matched within the fetched primary histories, never automatically asserts a missing real match. ESPN coverage is limited and unofficial; an empty or unsupported window is not proof of absence.
- `modelFeatures: true`: both-player last5/10 wins, surface record and H2H using full loaded historical arrays, strictly before the target tournament's start. Current rank/Elo, the target outcome and same-tournament results are excluded from features. `rest_days` remains null; `days_since_prior_tournament_start` is a distinct proxy, not actual rest. `maxFeatureOpponents` defaults to 10 (maximum 50); unresolved/unloaded opponent history is explicit. MODEL_DATASET separates input features from the result label and keeps one row per source match ID, using the first requested player perspective and listing all returned perspectives. Normal Dataset match rows retain their original perspective behavior. Training rows derive from currently observed corrected histories, so **historical as-of availability is not proven**; do not describe this as an entirely leakage-free historical backtest dataset. Within tied earlier tournament dates source match IDs define deterministic ordering; exact chronological match order is not verified.
- `monitorCorrections: true` + `monitoringKey`: stores observations in your own named `tennis-observations-<key-hash>` key-value store. First run establishes a baseline, identical replay has no corrections, changed tracked result/stat fields create before/after entries and increment revisions. Immutable snapshots link to their predecessors; OBSERVATION provides store/state/snapshot identifiers. CORRECTIONS contains changes observed in this run. Tracking begins at your first run, not before it. Current profile age/rank/Elo and scrape timestamps are excluded from correction comparison to avoid false result revisions. Only delivered rows update the baseline; missing/filtered/budgeted/unavailable records are never treated as deletions. Run each monitoring key sequentially: optimistic overlap detection exists, but the store provides no atomic compare-and-swap or full concurrency guarantee. Snapshot history consumes storage; retention/deletion is under your control.

```json
{"players":["Novak Djokovic"],"maxResults":5,"verifyMatches":true,"maxVerificationRequests":3}
```

```json
{"players":["Jannik Sinner","Carlos Alcaraz"],"opponent":"Carlos Alcaraz","maxResults":3,"modelFeatures":true,"maxFeatureOpponents":2}
```

```json
{"players":["Iga Swiatek"],"maxResults":3,"monitorCorrections":true,"monitoringKey":"my_swiatek_observations"}
```

New modes are validated by UPGRADE_RELEASE_2026-10-14.json, separate from the original 0.1.7 release evidence. No synthetic QA correction is claimed as a real correction made by Tennis Abstract.

### Small tennis scraping recipes

Begin with three completed WTA results and leave charting, verification, modeling and persistent correction tracking off unless needed.

```json
{
  "players": [
    "Iga Swiatek"
  ],
  "maxResults": 3,
  "includePointByPoint": false,
  "verifyMatches": false,
  "modelFeatures": false,
  "monitorCorrections": false
}
```

For rankings alone, omit players so player matches cannot consume the global cap.

```json
{
  "rankings": "wta",
  "maxResults": 10
}
```

For a small Elo observation, select the tour and retain the published table date when available. This is not a historical match-time Elo series.

```json
{
  "elo": "atp",
  "maxResults": 10
}
```

#### Match identity and time are part of the data

Requesting both opponents can return two player perspectives of the same match. Keep match_id/source_match_id and perspective when joining rows; do not count those perspectives as two separate sporting events. Source tournament start dates can repeat across rounds and are not guaranteed actual match dates. Match-time ranks, current profile ranks and today's Elo are different observations.

Rate fields ending \_pct are fractions: multiply by 100 only when presenting percentage points. Null statistics do not become zero. Retirements, walkovers and defaults need their markers retained when comparing performance, because a completed result does not imply a fully played match.

#### Optional verification and model output

verifyMatches compares a listed subset of fields with a second publisher. A matched flag does not certify every statistic or independent primary provenance. Keep unmatched/unsupported coverage and secondary timestamps visible. MODEL_DATASET separates features and outcome labels; features derived from currently corrected histories cannot prove what was knowable at an earlier historical date.

Correction monitoring records changes relative to your retained observations. A missing or filtered row is not a deletion. Use each monitoring key sequentially and retain snapshot links; optimistic overlap detection is not a transactional concurrency guarantee. Storage and source work can consume resources even when no new correction appears.

### Tennis API cost and integration questions

**What does a three-match sample cost?** Base match/ranking/Elo rows are $0.00019 each. Three matches cost $0.00057 plus the minimum $0.00005 startup at 256 MB, before platform usage. A successfully delivered charted-match detail adds its $0.00999 event; requesting the option does not guarantee charting exists.

**Does maxResults guarantee a complete career?** No. It is one global output ceiling across requested players, then rankings, then Elo. Loaded source histories and archive availability define the coverage boundary.

**Can I get a Python example?** Use the [existing Python client and examples](https://github.com/apivault-labs/apivault-labs/tree/main/python-clients/apivault-tennis-abstract-bulk-scraper), standard Apify clients or the community n8n integration below. A public GitHub client does not by itself establish availability on PyPI.

**Will an agent get nested point tables in CSV?** JSON preserves nested sets, points and shot statistics. Tabular formats serialize/flatten them; inspect the JSON source before assuming that a flat CSV contains every nested table in separate columns.

### n8n and developer integrations

Use this Actor in your automation or application with the existing community integrations below. An Apify account and API token are required; Actor and platform charges follow the [current pricing](https://apify.com/apivault_labs/tennis-abstract-bulk-scraper/pricing).

- **n8n community node:** [n8n-nodes-apivault-tennis-abstract-bulk-scraper](https://www.npmjs.com/package/n8n-nodes-apivault-tennis-abstract-bulk-scraper). Install package name `n8n-nodes-apivault-tennis-abstract-bulk-scraper` in a self-hosted n8n instance that permits community packages.
- **n8n source and usage:** [GitHub](https://github.com/apivault-labs/apivault-labs/tree/main/community-nodes/n8n-nodes-apivault-tennis-abstract-bulk-scraper).

These are community packages. Publication on npm does not establish n8n verification or availability in n8n Cloud. Review the linked package README for supported inputs and setup.

# Actor input Schema

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

Exact names, Tennis Abstract slugs or HTTPS player URLs; up to 100.

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

Leave empty for every surface.

## `levels` (type: `array`):

Source codes: G, M, A, F, O, D, C, S, Q, J, P, PM, I, W, T1-T4.

## `rounds` (type: `array`):

Source codes such as F, SF, QF, R16.

## `results` (type: `array`):

Include win and/or loss. Empty means both.

## `opponent` (type: `string`):

Exact name, slug or Tennis Abstract player URL.

## `maxOpponentRank` (type: `integer`):

Include only opponents ranked this high or better. Unknown ranks excluded.

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

YYYY-MM-DD; inclusive tournament START date.

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

YYYY-MM-DD; inclusive tournament START date.

## `includePointByPoint` (type: `boolean`):

Read charted matches only; increases requests and billing. No detail is invented for uncharted matches.

## `rankings` (type: `string`):

Current published Tennis Abstract ranking tables.

## `elo` (type: `string`):

Published Elo leaderboard for ATP, WTA or both.

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

One global limit: player matches first, then ATP/WTA rankings, then Elo. Omit for all available rows.

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

Apify-hosted runs can need proxy routing because Tennis Abstract blocks direct datacenter requests. Proxy and platform usage are extra.

## `verifyMatches` (type: `boolean`):

Compare participants, winner, sets and available tie-breaks against a second public source. Unknown/error/conflict are explicit; no blanket accuracy guarantee.

## `modelFeatures` (type: `boolean`):

Both-player last5/10, surface and H2H features strictly before tournament start. Current Elo and same-tournament results excluded; true rest days remain null.

## `monitorCorrections` (type: `boolean`):

Save observations in your named store and report before/after changes since your baseline. Run the same monitoringKey sequentially; storage usage extra.

## `maxVerificationRequests` (type: `integer`):

Global limit of ESPN requests per run; cached per tour and tournament date. Unchecked rows marked request_limit.

## `maxFeatureOpponents` (type: `integer`):

Cap extra historical player loads for opponent features. Missing coverage is explicit.

## `monitoringKey` (type: `string`):

Required when tracking corrections: 1-40 letters, digits or underscore. Same key and player/filter scope reuse your baseline.

## Actor input object example

```json
{
  "players": [
    "Novak Djokovic"
  ],
  "includePointByPoint": false,
  "rankings": "",
  "elo": "",
  "maxResults": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "verifyMatches": false,
  "modelFeatures": false,
  "monitorCorrections": false,
  "maxVerificationRequests": 10,
  "maxFeatureOpponents": 10
}
```

# Actor output Schema

## `records` (type: `string`):

No description

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

No description

## `corrections` (type: `string`):

No description

## `observation` (type: `string`):

No description

## `verification_coverage` (type: `string`):

No description

## `model_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 = {
    "players": [
        "Novak Djokovic"
    ],
    "maxResults": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("apivault_labs/tennis-abstract-bulk-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 = {
    "players": ["Novak Djokovic"],
    "maxResults": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("apivault_labs/tennis-abstract-bulk-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 '{
  "players": [
    "Novak Djokovic"
  ],
  "maxResults": 20
}' |
apify call apivault_labs/tennis-abstract-bulk-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,apivault_labs/tennis-abstract-bulk-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/AHdDQLHk0r0omhAMX/builds/KIQCAo9Hsg9JQSF7p/openapi.json
