# Tennis Scraper API: ATP WTA ITF Results, Stats, Rankings, H2H (`shelvick/tennis-data-index`) Actor

Tennis data API for completed ATP, WTA and ITF singles matches. Query tennis results by tournament, match stats, head-to-head records, player stats and tennis rankings. Hourly updates, stable player IDs and a change feed. Coverage varies by tour and is expanding; no live scores.

- **URL**: https://apify.com/shelvick/tennis-data-index.md
- **Developed by:** [Scott Helvick](https://apify.com/shelvick) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.55 / 1,000 match record (standard)s

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?

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 Scraper API: ATP WTA ITF Results, Stats, Rankings, H2H

A tennis data API for completed ATP, WTA and ITF singles matches: tennis results, match statistics, head-to-head records, player stats and tennis rankings from one index, with hourly updates. Comparing tennis results often means joining separate match histories, rankings and statistics with incompatible identifiers. This Actor returns completed singles matches, head-to-head records, player statistics, ranking tables and incremental changes from one cross-tour index, refreshed hourly.

### What this does

- **Match results:** `matches` filters rows by dates, tour, tournament, surface, round, stage and status. Rows carry player names alongside stable IDs, and per-side match statistics where the source published them. Suitable for tournament analysis or a season results panel.
- **Head-to-head:** `h2h` returns every indexed meeting between two player IDs within the filters, plus one summary row containing the record, wins for each player and a by-surface split.
- **Player statistics:** `player_stats` returns one aggregate of raw serve/return sums and derived percentages, optionally restricted by opponent-ranking band. Only completed and retired matches count.
- **Rankings:** `rankings` returns a tour's indexed ranking table for a date, optionally bounded by ranking position.
- **Incremental updates:** `changes` returns revisions after an integer checkpoint, including tombstones for merged-away rows, so a caller can keep a local analytical copy in step.

Coverage expands continuously as historical seasons are backfilled; depth differs by tour. Every run reports the exact coverage it ran against in its summary. A request outside current coverage returns a free `out_of_coverage` notice naming the covered span rather than a silent empty result. Coverage is not a promise that every event or statistics field exists throughout that span.

Coverage is queryable through the run summary, so an agent can distinguish "no data yet" from "no such match": requests outside coverage receive `out_of_coverage`, while queries inside coverage that match nothing receive `no_match`. Neither notice is charged.

All modes write one dataset using a superset row shape selected by `record_type`. Rows stream page by page; data charges happen only after delivery. The `OUTPUT` key-value record describes counts, freshness and continuation.

### Why re-aggregatable statistics matter

Averaging percentages weights short and long matches equally. Raw sums let callers combine disjoint samples before calculating percentages.

The Actor derives percentages from raw sums rather than averaging percentages. `matches_count` and `matches_with_stats` distinguish the result sample from the statistics sample. Missing statistics are not zero performance: availability varies by tour and era, and a match side without usable statistics carries `stats: null` and a `stats_reason` explaining why.

### How it compares to separate tennis data feeds

| Approach | Typical fit | Integration boundary |
|---|---|---|
| Single-source scrapers | One tour or tournament | Caller reconciles identifiers and output formats across sources. |
| Static historical datasets | Fixed research samples | Fresh results and corrections require another update process. |
| Subscription sports-data APIs | Contracted coverage, possibly live data | Available history, statistics and redistribution rights depend on the product. |
| This Actor | Completed-match analysis across tours | One row contract, hourly refresh, revision feed; coverage and statistics remain source-dependent. |

This is a completed-results query tool, not a substitute for a live sports feed.

### Input

Dates use `YYYY-MM-DD`. A dash means no schema default; examples shown in the Store form are prefills, not additional defaults.

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `mode` | string | No | `matches` | `matches`, `h2h`, `player_stats`, `rankings` or `changes`. |
| `tour` | string | No | - | `atp`, `wta`, `challenger`, `itf`; unfiltered for match history when omitted. Rankings defaults to ATP. |
| `dateFrom` | string | No | - | Inclusive match-date lower bound. |
| `dateTo` | string | No | - | Inclusive upper bound; not before `dateFrom`. |
| `tournamentId` | string | No | - | Source-prefixed tournament ID, such as `atp:540`; combine with dates for an edition. |
| `surface` | string | No | - | Stored label, matched exactly and case-sensitively; values are lowercase, such as `grass`, `hard` or `clay`. |
| `rounds` | array of strings | No | - | `R128`, `R64`, `R32`, `R16`, `QF`, `SF`, `F`, `RR`, `Q1`, `Q2`, `Q3`; empty means unfiltered. |
| `stage` | string | No | - | `main` or `qual`; omission includes either where indexed. |
| `statuses` | array of strings | No | `["completed","retired"]` | Status filter; statistics mode requires this default pair. |
| `playerA` | string | For h2h | - | First player ID, such as `atp:s0ag`. |
| `playerB` | string | For h2h | - | Different second player ID, such as `atp:a0e2`. |
| `player` | string | For player\_stats | - | One player ID, such as `wta:326408`. |
| `opponentRankMin` | integer | No | - | Positive minimum numeric opponent rank; statistics only. |
| `opponentRankMax` | integer | No | - | Positive maximum numeric opponent rank; not below the minimum. |
| `rankingDate` | string | No | - | Indexed ranking snapshot date, not a match-date filter. |
| `rankMax` | integer | No | - | Top N: `100` returns positions 1 to 100. Not a row budget. |
| `since` | integer | No | - | Nonnegative revision checkpoint for changes; omission starts from zero. |
| `cursor` | string | No | - | Opaque continuation token; copy without editing. Rejected in `changes` mode, which resumes from `since`. |
| `limit` | integer | No | `5000` | Data-row cap, from 1 to 100000; the run's maximum charge may stop delivery sooner. A six-figure pull can take most of the run's time budget. |

The status vocabulary also accepts `walkover`, `default`, `bye`, `scheduled`, `in_progress` and `cancelled`. These labels do not imply live coverage; this index covers completed matches.

Challenger events are indexed under ATP and **cannot currently be filtered apart from ATP main tour**. Requesting `challenger` returns both, with a free `not_separable` notice.

#### Per-mode requirements and ignored fields

- **matches:** no required selectors. Uses match-history filters and pagination; ignores player selectors, ranking selectors and `since`.
- **h2h:** requires distinct `playerA` and `playerB`. Uses match-history filters and pagination; ignores `player`, ranking selectors and `since`.
- **player\_stats:** requires `player`. Uses match-history filters and optional opponent-rank bounds; `statuses` must remain completed plus retired. Ignores `playerA`, `playerB`, ranking selectors and `since`.
- **rankings:** uses `tour`, `rankingDate`, `rankMax` and pagination. Ignores match-history filters, player selectors and `since`.
- **changes:** uses `since` and `limit`; ignores tour, match-history, player and ranking selectors, and REJECTS `cursor` — the revision feed resumes from `since`. It is unfiltered.

Opponent-ranking bounds outside `player_stats` are rejected, not ignored. Ignored fields still undergo input validation. The schema is strict: an unknown key produces a free correction record, never a failed run.

### Output

One dataset contains five `record_type` variants. These illustrative JSON excerpts omit unrelated null fields; numbers illustrate the contract, not a verified historical extract. `mode` always identifies the requested query.

#### Match

```json
{
  "record_type": "match", "mode": "matches",
  "match_id": "atp:540:2025:MS:ms001", "edition_id": "atp:540:2025",
  "tour": "atp", "year": 2025, "draw": "MS", "stage": "main",
  "round": "F", "date": "2025-07-13", "status": "completed",
  "winner_id": "atp:s0ag", "loser_id": "atp:a0e2",
  "winner": {
    "player_id": "atp:s0ag", "name": "Jannik Sinner", "country": "ITA",
    "qid": null, "resolution": "source", "redirect_to": null,
    "stats": {
      "aces": 8, "double_faults": 2,
      "serve_points": 120, "first_in": 75, "first_won": 60,
      "second_won": 25, "second_points": 45,
      "bp_saved": 4, "bp_faced": 6, "service_games": 20,
      "return_points": 130, "first_return_won": 25, "first_return_points": 80,
      "second_return_won": 25, "second_return_points": 50,
      "bp_converted": 4, "bp_opportunities": 9, "return_games": 20,
      "total_points_won": 135, "total_points": 250
    },
    "stats_reason": null
  },
  "loser": {
    "player_id": "atp:a0e2", "name": "Carlos Alcaraz", "country": "ESP",
    "qid": null, "resolution": "source", "redirect_to": null,
    "stats": null, "stats_reason": "source_missing"
  },
  "score": "4-6 6-4 6-4 6-4", "sets": null,
  "source": "atp", "source_url": null,
  "db_built_at": "2026-09-10T12:00:00Z"
}
```

`winner` and `loser` accompany match rows in `matches`, `h2h` and match data in `changes`. Each side has an always-present, source-namespaced `player_id`; `name` and `country` can be null when identity is unresolved. `qid` is a Wikidata Q-number or null. `resolution` records how identity was established: `source`, `crosswalk`, `wikidata` or `unresolved`. It distinguishes resolved identities from stored unresolved answers, never guesses from names. `redirect_to`, normally null, identifies the surviving player ID after a merge; the merged-away ID keeps answering so clients can follow it. `stats` holds that player's per-match raw sums, never percentages, so callers can re-aggregate before dividing. Null means unusable statistics, never zero play; `stats_reason` explains absence with `source_missing`, `zero_serve_points`, `zero_service_games`, `inconsistent_serve_counts` or `unfetchable`, and is null when statistics are present.

`match_id` is the stable match key, `<tour>:<tournament>:<year>:<draw>:<source key>`; `edition_id` is its first three segments and groups one running of a tournament. Merged-away match rows are excluded from listings. `draw` is `MS`/`WS` for singles and `MQ`/`WQ` for the qualifying draws. `draw`, `stage`, `round` and numeric `round_seq` describe draw placement. `date`, optional `completed_at`, and `duration_s` describe timing. `score` preserves source notation; `sets` is structured when available, with source-dependent layout.

`winner_seed`/`loser_seed` are tournament seeds, `winner_rank`/`loser_rank` are associated rankings, and `winner_entry`/`loser_entry` hold entry designations. `conflict` flags a conflicting record when known. Nullable values must not be treated as zero.

`source` provides attribution, `source_url` the verification reference when available, `fetched_at` the retrieval timestamp, and `db_built_at` the index snapshot the row's page was served from — a run that spans an hourly refresh can carry more than one value. These fields may be null; aggregates and notices do not represent individual source matches.

In `changes`, match-shaped rows also carry integer `revision` and operation `op`. An upsert supplies match data; a tombstone identifies a removed match and may leave other fields null. When a tombstone arrives because two sources were merged into one match, `merged_into` names the surviving match id — repoint a local row at it rather than simply deleting the row, or the history disappears from your copy. Merged matches are filtered out of `matches` and `h2h`, so `merged_into` is null there. Apply revisions in order rather than appending every change as a new match.

#### Head-to-head summary

```json
{
  "record_type": "h2h_summary", "mode": "h2h", "matches_count": 2,
  "summary": {
    "player_a": "atp:s0ag", "player_b": "atp:a0e2",
    "matches": 2, "wins_a": 1, "wins_b": 1,
    "by_surface": {"grass": {"matches": 2, "wins_a": 1, "wins_b": 1}}
  }
}
```

`summary` covers the full filtered indexed set, not just the delivered page. One summary is delivered per run, subject to budget, alongside the meeting rows. `matches_count` repeats its match count.

#### Player statistics

```json
{
  "record_type": "player_stats", "mode": "player_stats",
  "player_id": "atp:s0ag", "matches_count": 4,
  "matches_with_stats": 3, "wins": 3, "losses": 1,
  "stats": {"serve_points": 200, "first_in": 120, "first_won": 90},
  "derived": {"first_serve_pct": 0.6, "first_won_pct": 0.75, "win_pct": 0.75},
  "stats_reason": null
}
```

Top-level `stats` and `stats_reason` describe the cohort aggregate on `player_stats` rows only; per-match statistics live inside `winner` and `loser`. The abbreviated `stats` object contains raw sums, including aces, double faults, serve points, first serves in/won, second serves won/points, break points saved/faced, service games, return points, first/second return points won and played, break points converted/opportunities, return games and total points won/played. `derived` contains ratios calculated from sums; for example `first_serve_pct` is `first_in / serve_points`.

`wins` and `losses` describe the filtered result sample; `matches_with_stats` measures statistics completeness. A `matches_count` of zero arrives free, with a `no_match` notice for queries inside coverage or `out_of_coverage` for requests naming data not yet indexed. Within coverage, an unknown player ID and a player with no qualifying matches look identical from here. Missing aggregate components remain null. Statistics availability varies by tour and era; WTA statistics are not promised.

#### Ranking

```json
{
  "record_type": "ranking", "mode": "rankings", "tour": "atp",
  "rank_date": "2025-07-14", "rank": 1, "player_id": "atp:s0ag",
  "points": 12030, "tournaments_played": 18,
  "source": "atp", "source_url": null
}
```

`rank_date` selects the snapshot; `rank` is position, not points. `points` and `tournaments_played` are source-reported and nullable.

#### Notice

```json
{
  "record_type": "notice", "mode": "matches", "error_class": "row_limit",
  "notice": "This run reached the `limit` you set before delivering every matching row. Everything above is complete. To continue, start another run with the resume cursor from this run's summary record, or raise `limit`."
}
```

`notice` explains a correction, missing result or early stop. Route on `error_class`, not prose. Notices are free.

#### Run summary and continuation

The `OUTPUT` key-value record contains `mode`, `delivered`, `charged`, `charged_by_event`, `notices`, `pages`, `truncated`, `resume_cursor`, `resume_field`, `stopped_by`, `db_built_at` and `coverage`. Delivered and charged counts describe data rows; notices are counted separately. Event counts are not monetary totals.

`coverage` reports each tour's `first_date`, `last_date`, total match count and `years` map of year to match count, plus ranking-snapshot spans and the snapshot's `db_built_at`. It also carries `statuses` — which match statuses those counts include — and, where the index qualifies how a tour is filed, a `note`. Coverage is read once per run, free of charge. If that read fails, `coverage` is null and empty queries use the plain `no_match` notice; delivery and billing are unaffected.

When truncated, copy `resume_cursor` into the input named by `resume_field`. For `cursor`, preserve the opaque string. For `since`, convert a numeric-string checkpoint to an integer. Retain the original mode and filters. A missing continuation value is not permission to invent a token: `resume_cursor` and `resume_field` are both null when the run stopped inside its first page, which means there is nothing to continue from — raise the run's maximum charge and run the same input again. Page-boundary replay can occur after a partial page: merge by stable IDs, and handle repeated h2h summaries separately.

### Example

Request the indexed US Open men's singles semifinals and final in a bounded window:

```json
{"mode":"matches","tour":"atp","tournamentId":"atp:560","dateFrom":"2025-08-25","dateTo":"2025-09-07","rounds":["SF","F"],"limit":50}
```

The same input through REST:

```bash
curl -X POST 'https://api.apify.com/v2/acts/shelvick~tennis-data-index/run-sync-get-dataset-items' \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"mode":"matches","tour":"atp","tournamentId":"atp:560","dateFrom":"2025-08-25","dateTo":"2025-09-07","rounds":["SF","F"],"limit":50}'
```

Python, including the summary needed for continuation:

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("shelvick/tennis-data-index").call(run_input={
    "mode": "matches", "tour": "atp", "tournamentId": "atp:560",
    "dateFrom": "2025-08-25", "dateTo": "2025-09-07",
    "rounds": ["SF", "F"], "limit": 50,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row)
summary = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("OUTPUT")
print(summary["value"])
```

### Calling from an AI agent

- **Apify MCP server:** expose `shelvick/tennis-data-index` as a tool. Select a mode, construct schema-valid inputs, and inspect notices before treating a successful run as complete coverage.
- **Apify SDK (Python):** the `apify_client` example returns dataset rows and the run summary. An orchestrator can route notices and persist continuation checkpoints.
- **REST API:** use the synchronous endpoint above for bounded queries. For larger pulls, use `POST /v2/acts/shelvick~tennis-data-index/runs`, poll `GET /v2/actor-runs/{runId}`, then read the default dataset and `OUTPUT` from the default key-value store.

### Pricing

Pay-per-event: match rows use `match-record`, with the cheaper `match-record-bulk` volume rate after a threshold within the same run. H2h summaries and player aggregates use `aggregate-row`; ranking records use `ranking-row`. An aggregate covering zero matches is delivered free — the index cannot tell an unknown player ID from a player who never played, so a mistyped ID is never billed.

Data charging occurs only after records are pushed. Notices, input corrections and failures are always free; previously delivered data remains billable if a later page fails. The maximum-charge cap stops delivery cleanly and reports continuation details in `OUTPUT`. The platform also defines an `apify-actor-start` event.

See the **Pricing** tab on this Store page for the current per-event rates and any active subscriber discounts.

### Behavior

Source or configuration problems do not end the run as FAILED: the Actor delivers a labeled notice and exits successfully. Success describes execution, not guaranteed coverage.

| `error_class` | Meaning and routing |
|---|---|
| `source_error` | Temporary source failure; the only class to retry immediately, allowing a few minutes between attempts. |
| `no_match` | Valid filters matched no indexed rows; reconsider scope or IDs. |
| `out_of_coverage` | The requested date range, season or tour names data the index does not hold yet; the free notice names the covered span. Unlike other terminal classes, this is worth repeating later as coverage expands. |
| `invalid_input` | Correct the input using the notice. |
| `not_configured` | Run configuration unavailable; no lookup or data charge. |
| `invalid_request` | Request rejected; inspect and correct it. |
| `not_separable` | Challenger-only filtering unavailable; results include ATP main tour. |
| `budget_exhausted` | Maximum charge reached; resume with a suitable budget. |
| `row_limit` | Data-row limit reached; resume or raise the limit. |
| `internal_error` | A fault stopped delivery; inspect the partial output. |

Every class except `source_error` is terminal for that input, not an automatic immediate retry instruction. `out_of_coverage` is worth retrying later as data is added, not immediately. Notices may accompany usable data.

Latency scales with page count: a one-page query is the shortest path; multi-page histories require sequential retrieval and delivery per page. Allow startup time plus page processing, and use asynchronous calls for broad pulls. No measured wall-clock guarantee is offered. Hourly refresh does not guarantee a source publishes every completed result immediately.

**Telemetry:** to improve coverage and reliability, this Actor reports anonymous usage metrics and diagnostic events to the developer — mode, delivered and charged counts, page counts, notice counts, delivered-field completeness counts and stop reasons (counts and enum keys only), and, only when something goes wrong, the relevant input fields. No account identifiers are collected, and telemetry never affects a run.

### FAQ

**Can percentages be combined across runs?** Combine raw sums from disjoint samples, then recalculate. Averaging percentages or combining overlapping samples changes the denominator incorrectly.

**Does a limit shorten the h2h record?** The summary covers the full filtered set, while the delivered meeting rows can be truncated. Check `OUTPUT` before assuming all meetings arrived.

**Can changes be restricted to one tournament?** Not currently. Consume the revision feed and maintain any desired analytical subset locally, including removals.

### Source and acceptable use

The records are normalized facts drawn from official tour and tournament sources. Match and ranking records carry source attribution and a per-record source reference where available; nullable references and statistics are explicit rather than fabricated.

The scope is completed matches, refreshed hourly. Output is intended for analysis, modelling and integration, not republication as a bulk mirror of any source. Retain source references and coverage qualifications when incorporating the facts into downstream analysis.

### What this doesn't do

- **No live or in-play scores.** Hourly completed-match refresh is not match monitoring.
- **No odds.** Betting markets and prices are outside the output.
- **No point-by-point.** Scores and available match statistics do not reconstruct rallies or individual points.
- **No doubles.** Version one covers singles only.
- **No universal statistics guarantee.** Availability varies by tour and era; absent match statistics use `stats: null` with `stats_reason`.

Use a licensed live-score feed for in-play monitoring, an odds feed for markets, or a point-level sports dataset for sequence analysis.
Use a doubles-specific results feed for doubles coverage.

# Actor input Schema

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

Choose match rows, head-to-head results, player aggregates, rankings or a revision feed. Values: matches, h2h, player\_stats, rankings, changes. Default matches; h2h requires two player IDs and player\_stats requires one. Select one mode per run; for example matches for a tournament-sized query.

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

Scope the query to atp, wta, challenger or itf; omit for no tour filter. Challenger events are indexed under the ATP tour, so selecting challenger currently returns ATP main tour as well; the run reports this. Use one tour to narrow a season or tournament query; for example atp.

## `dateFrom` (type: `string`):

Set the inclusive start of the match date window using YYYY-MM-DD. Optional match-history filter; omit for no lower bound, and do not set it after dateTo. A tournament query typically spans a few weeks; for example 2025-06-23. Rankings use rankingDate instead.

## `dateTo` (type: `string`):

Set the inclusive end of the match date window using YYYY-MM-DD. Optional match-history filter; omit for no upper bound, and do not set it before dateFrom. A tournament query typically spans a few weeks; for example 2025-07-14. Rankings use rankingDate instead.

## `tournamentId` (type: `string`):

Restrict match history to one tournament using its source-prefixed index ID. Use an ID rather than a tournament name; omit for no tournament filter. Combine with dates to select one edition, typically a few weeks; for example atp:540 for Wimbledon.

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

Restrict match history to the surface label held in the index. Matched exactly and case-sensitively against the stored label, which is lowercase: grass, hard, clay or carpet; omit for no surface filter. Labels depend on source coverage. Use one surface for a tournament or season comparison; for example grass.

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

Restrict match history to one or more draw rounds rather than the full draw. Values: R128, R64, R32, R16, QF, SF, F, RR, Q1, Q2, Q3. Empty means no round filter; availability depends on the draw. Typically select one to three rounds; for example \["SF", "F"].

## `stage` (type: `string`):

Restrict match history to the main draw or qualifying stage. Values: main, qual; omit for both where indexed. This filter does not add unavailable qualifying coverage. Select one stage for a tournament-sized query; for example main.

## `statuses` (type: `array`):

Select which match statuses count in the query; completed and retired are the default. Values: completed, retired, walkover, default, bye, scheduled, in\_progress, cancelled. Availability depends on the source; this is not a live-score feed. player\_stats requires the default pair. Typically use \["completed", "retired"].

## `playerA` (type: `string`):

Identify the first player in a head-to-head query using a source-prefixed index player ID. Required only for h2h together with a different playerB; names are not IDs. Supply one ID for the entire comparison, for example atp:s0ag.

## `playerB` (type: `string`):

Identify the second player in a head-to-head query using a source-prefixed index player ID. Required only for h2h together with a different playerA; names are not IDs. Supply one opponent for the entire comparison, for example atp:a0e2.

## `player` (type: `string`):

Identify the player whose match statistics should be aggregated using an index player ID. Required for player\_stats; names are not IDs and statistics depend on source availability. Supply one player per aggregate query, for example wta:326408.

## `opponentRankMin` (type: `integer`):

Set the lowest numeric opponent rank included in a player statistics query. Positive integer, player\_stats only; omit for no lower bound and keep it at or below opponentRankMax. This filters by available opponent ranks, not ranking points. Typically pair 1 with a maximum of 10; for example 1.

## `opponentRankMax` (type: `integer`):

Set the highest numeric opponent rank included in a player statistics query. Positive integer, player\_stats only; omit for no upper bound and keep it at or above opponentRankMin. Rank 1 is strongest; unranked opponents lack a numeric rank. Typically use a top-ten or top-hundred cutoff; for example 10.

## `rankingDate` (type: `string`):

Select a rankings snapshot by its ISO date in YYYY-MM-DD form. Optional rankings-mode selector, not a match-date filter; only indexed snapshots can be returned. Use one snapshot per query, for example 2025-07-14, with rankMax to bound its size.

## `rankMax` (type: `integer`):

Limit rankings results to the top N players — rankMax 100 returns positions 1 to 100. Positive integer used in rankings mode; omit for no rank cutoff. This is not a row budget and ties may affect counts. Typically request the top 100; for example 100.

## `since` (type: `integer`):

Resume the changes feed after a nonnegative integer revision number. Used in changes mode, not as a date or a match-page cursor. Copy the run summary resume\_cursor here when resume\_field is since; keep the same filters. Use one checkpoint per incremental run, for example 0 for an initial feed.

## `cursor` (type: `string`):

Continue a truncated paginated query using the opaque cursor from its run summary. Copy resume\_cursor here only when resume\_field is cursor; do not construct or edit the token. Keep the original mode and filters. Supply one token per continuation; for example the exact resume\_cursor from your preceding run.

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

Cap the number of billable data rows delivered in a run to control query size. Integer from 1 to 100000, default 5000; a six-figure history pull can take most of the run's time budget, so raise the run timeout with it; the maximum run charge can stop delivery sooner. Free notices explain truncation and the summary supplies continuation details. A tournament query typically needs hundreds of rows; for example 500.

## Actor input object example

```json
{
  "mode": "matches",
  "dateFrom": "2025-08-25",
  "dateTo": "2025-09-07",
  "tournamentId": "atp:560",
  "statuses": [
    "completed",
    "retired"
  ],
  "limit": 500
}
```

# Actor output Schema

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

Match rows, head-to-head summaries, player statistics, rankings and free explanatory notices for this run.

## `output` (type: `string`):

Delivered and charged counts, snapshot freshness and continuation instructions for truncated runs.

# 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 = {
    "mode": "matches",
    "dateFrom": "2025-08-25",
    "dateTo": "2025-09-07",
    "tournamentId": "atp:560",
    "statuses": [
        "completed",
        "retired"
    ],
    "limit": 500
};

// Run the Actor and wait for it to finish
const run = await client.actor("shelvick/tennis-data-index").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 = {
    "mode": "matches",
    "dateFrom": "2025-08-25",
    "dateTo": "2025-09-07",
    "tournamentId": "atp:560",
    "statuses": [
        "completed",
        "retired",
    ],
    "limit": 500,
}

# Run the Actor and wait for it to finish
run = client.actor("shelvick/tennis-data-index").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 '{
  "mode": "matches",
  "dateFrom": "2025-08-25",
  "dateTo": "2025-09-07",
  "tournamentId": "atp:560",
  "statuses": [
    "completed",
    "retired"
  ],
  "limit": 500
}' |
apify call shelvick/tennis-data-index --silent --output-dataset

```

## MCP server setup

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

```

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/JsDk1MWGMtDBtVF4a/builds/lUawVgHg0iF8D94QK/openapi.json
