# Tennis Results, Fixtures, Live Scores & Rankings (`cleanrows/tennis-data-scraper`) Actor

Ask by date, date range or player name - no internal IDs. Ranges reach finished tournaments, not just today's. Set-by-set scorelines assembled; head-to-head and recent form as flat columns with win percentages. ATP and WTA rankings with movement. Tour level and singles/doubles as columns.

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

## Pricing

$6.00 / 1,000 record scrapeds

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?

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 Results, Fixtures, Live Scores & Rankings

Every tennis match on any date — finished results, today's order of play,
tomorrow's fixtures — with **set-by-set scores already assembled**, plus live
matches, the full ATP and WTA rankings, and any player's match history by name.

### Why this one

**No internal IDs.** Give it a date, or a player's name. The most-used tennis
scraper on this Store requires you to supply SofaScore's own `matchId`,
`tournamentId`, `seasonId` or `playerId` before it will return anything, which
means finding those ids somewhere else first. Here, `"Carlos Alcaraz"` is a
valid input.

**Any date, not just right now.** Ask for yesterday's results, today's card and
tomorrow's fixtures in one run. Most tennis scrapers only return matches that
happen to be in progress when you press go, which is useless for anything you
want to run on a schedule. The source has no date feed at all — matches are
reachable only by walking the tournaments currently running — and doing that
walk for you is most of what this Actor is.

**A readable scoreline.** The source returns games as `period1` through
`period5` on each side, plus a separate live point value, and leaves you to pair
them up. You get `scoreline: "6-4, 3-6, 7-5"`, `homeSetsWon`, `awaySetsWon`, and
the raw per-set arrays if you want to compute your own.

**Tour and format as real columns.** `tourLevel` is ATP, WTA, Challenger, ITF
Men or ITF Women; `isDoubles` is a boolean. Neither exists in the source — the
tour is buried in a category name and doubles is implied by a team type — and
both are the first things anyone filters on.

**Flat rows.** Tournament, round, surface, both players with country and
ranking, status and start time, all at the top level. No nested objects to
unwrap before a CSV export makes sense.

**Ranking movement.** `rankingChange` is computed from the current and previous
ranking, positive for places gained. The source gives you both numbers and no
difference.

**Head-to-head as columns, not a blob.** Turn on `includeHeadToHead` and every
match carries `h2hMeetings`, `h2hHomeWins`, `h2hAwayWins` and a derived
`h2hHomeWinPct`. The source nests this as a duel object keyed to the home and
away sides of whichever fixture you came in through; here it is four numbers you
can sort and filter on. Roughly half of matches have a record — the source
returns nothing at all for a first meeting rather than zeros, and doubles pairs
usually have none, so null means "no record", not "zero wins".

**Date ranges reach finished events.** `dateFrom` and `dateTo` take a window
rather than a list, and it is not limited to what is running right now — a
completed tournament from an earlier month is retrievable, because the whole
tournament list is walked and each one's season is matched on year before any
page is fetched. Leave either end empty to mean today. Explicit `dates` still
win over a range, and a range wins over `dayOffsets`.

A wide range costs more, and honestly: more tournaments have to be examined, so
raise `maxTournaments` if a long window comes back short. A window returns its
most recent end first, so when `maxItems` runs out it is the earlier days that
are missing — and the run summary reports which days were actually covered
rather than leaving a partial answer looking like a complete one. If nothing in
the window matched a season at all, the run says that too, instead of reporting
a successful empty result.

**Recent form as columns too.** `includeRecentForm` adds each player's recent
results as a readable streak plus the numbers underneath it: `homeForm` /
`awayForm` ("WLWWW", newest first), `FormPlayed`, `FormWins` and a derived
`FormWinPct` for each side. It is built from each player's own match feed, so
the match in hand is never counted as its own prior form, and unfinished
matches and walkovers are dropped rather than scored as losses. The percentage
is worked out over what actually counted, which `FormPlayed` states, so a short
record is never silently scored as a full one.

**Optional depth, priced honestly.** Head-to-head, recent form, match
statistics, bookmaker odds markets and full point-by-point are each off by
default because each one costs an extra request per match. Turn on only what
you need.

### Input

Yesterday, today and tomorrow, ATP and WTA singles only:

```json
{
  "mode": "schedule",
  "dayOffsets": [-1, 0, 1],
  "tourFilter": ["ATP", "WTA"],
  "matchFormat": "singles",
  "maxItems": 500
}
```

Completed results for specific dates:

```json
{
  "mode": "schedule",
  "dates": ["2026-08-20", "2026-08-21"],
  "statusFilter": "finished"
}
```

One player's form and what is next:

```json
{ "mode": "player", "playerQuery": "Carlos Alcaraz", "matchesPerPlayer": 20 }
```

Matches in progress right now:

```json
{ "mode": "live", "maxItems": 100 }
```

Rankings:

```json
{ "mode": "rankings", "tour": "both", "minRanking": 100 }
```

### Output

A match — the same shape in `schedule`, `live` and `player` modes:

```json
{
  "matchId": "16888857",
  "tournament": "ITF M15 Arad Men",
  "category": "ITF Men",
  "tourLevel": "ITF Men",
  "isDoubles": false,
  "round": "Semifinals",
  "surface": "Clay",
  "homePlayer": "Robert Guna",
  "awayPlayer": "Iannis Miletich",
  "homeRanking": null,
  "scoreline": "3-3",
  "homeSetsWon": 0,
  "awaySetsWon": 0,
  "homeSetGames": [3],
  "awaySetGames": [3],
  "homePoint": "40",
  "awayPoint": "30",
  "statusType": "inprogress",
  "statusDescription": "1st set",
  "startTime": "2026-08-22T07:09:48.000Z",
  "matchDate": "2026-08-22"
}
```

In `player` mode each row also carries `queriedPlayer` and `queriedPlayerId`.

A ranked player:

```json
{
  "tour": "ATP",
  "ranking": 1,
  "playerName": "Jannik Sinner",
  "country": "Italy",
  "points": 13450,
  "previousRanking": 1,
  "rankingChange": 0,
  "bestRanking": 1,
  "tournamentsPlayed": 18
}
```

### Compared to the alternatives

Checked 25 Aug 2026, and worth verifying yourself before trusting any of it.

- The highest-usage tennis Actor here needs internal ids as input. This one
  takes names and dates.
- The second-highest has not been updated in over three months. Scrapers rot
  quietly — a stale one usually still returns rows, just wrong ones.
- Several cover one source each: Tennis Abstract only, TennisExplorer only,
  point-by-point only. This covers live, scheduled, completed, rankings and
  player history from one schema.
- Head-to-head and recent form are here as flat columns. Where competitors
  return the source's nested duel and form objects, this returns numbers you can
  sort on, plus the derived win percentages nobody else computes.

### Who this is for

- **Modellers and traders** who need completed results by date and live match state
- **Sports media and newsletters** reporting results and previewing the day's card
- **Researchers** tracking ranking movement and head-to-head form over a season
- **Fantasy and prediction products** needing scores and player metadata

### Notes

- Runs are capped at 2000 records, or 300 when statistics, odds or
  point-by-point are switched on, because those cost a request per match.
- Schedule mode walks the tournaments running now, so its cost scales with
  tournaments rather than dates: asking for three days costs the same as one.
  ATP and WTA together are usually a handful of tournaments; adding Challenger
  and ITF multiplies that, which is what `maxTournaments` is for.
- Rankings return up to 500 players per tour in a single request. Player mode
  costs one search plus one request per 30 matches.
- `dates` overrides `dayOffsets`, and at most 14 dates run at once.
- Live mode returns only matches in progress at that moment. Coverage is
  broadest during European and Asian daytime and thin overnight, which is a
  property of the tennis calendar rather than the Actor. Use `schedule` with
  `statusFilter: "inprogress"` if you would rather have the whole day's context.

# Actor input Schema

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

Matches for any date past or future, matches in progress right now, the official rankings, or a single player's results and upcoming fixtures.

## `dates` (type: `array`):

Explicit dates as YYYY-MM-DD, up to 14. Leave empty to use day offsets instead.

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

Start of a date range, YYYY-MM-DD. Reaches finished tournaments, not just the ones running now, so a past event like a completed Grand Slam is retrievable. Leave dateTo empty to mean today. Ranges longer than 366 days are truncated. A wide window returns its most recent end first, so if maxItems runs out the earlier days are the ones missing - the run summary reports which days were actually covered. Raise maxItems to keep more of the window and maxTournaments to reach further back.

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

End of the range, YYYY-MM-DD, inclusive. Leave dateFrom empty to mean from today. Ignored when Dates is filled in - an explicit list of dates always wins over a range, and a range wins over day offsets.

## `dayOffsets` (type: `array`):

Days relative to today: -1 is yesterday's results, 0 today, 1 tomorrow's fixtures. Ignored when Dates is set.

## `statusFilter` (type: `string`):

Applies to schedule and player modes. Canceled and walkover matches are returned by default; filter to Finished to exclude them.

## `tourFilter` (type: `array`):

Which circuits to walk. Empty means ATP and WTA, the cheapest useful default. A named tour selects that tour only - ATP no longer drags in ATP Challenger, and WTA no longer drags in WTA 125 - so the tournament budget goes where you asked. Add Challenger or ITF explicitly to include them; each one multiplies the tournaments visited.

## `matchFormat` (type: `string`):

Doubles pairs are detected from the team type, falling back to the slash in paired names.

## `maxTournaments` (type: `integer`):

How many tournaments to examine. The default is fine for today and yesterday, which only need the ones currently running. A date range reaching further back has to look through the full list for the category - roughly two extra requests per tournament that turns out not to match - so raise this if a past window comes back short. Reaching Wimbledon from six weeks later took 37.

## `playerQuery` (type: `string`):

Searched by name, so you do not need to know any internal id. Costs one extra request.

## `playerIds` (type: `array`):

Skip the name search when you already have SofaScore player ids. Combined with any name search.

## `matchesPerPlayer` (type: `integer`):

Recent matches to return for each player, plus the same number of upcoming ones if enabled.

## `includeUpcoming` (type: `boolean`):

Add the player's scheduled matches to their completed ones. Costs one extra request per page.

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

Which tour's rankings to return. Ignored outside rankings mode.

## `minRanking` (type: `integer`):

Keep only players ranked this high or better. 0 returns all 500 per tour.

## `includeHeadToHead` (type: `boolean`):

Prior meetings between the two players as flat columns - h2hMeetings, h2hHomeWins, h2hAwayWins and a derived h2hHomeWinPct - rather than a nested object. Costs one extra request per match. Roughly half of matches have a record; first meetings and doubles pairs usually return none.

## `includeRecentForm` (type: `boolean`):

Each player's recent results as flat columns - homeForm and awayForm as a letter streak (newest first), plus FormPlayed, FormWins and a derived FormWinPct for each side. Derived from each player's own match feed, so the result of the match in hand is never counted as prior form. Costs one request per player, cached for the run, so a full draw pays once per name rather than once per match. Players with no completed matches on record return null.

## `includeStatistics` (type: `boolean`):

Aces, double faults, break points and the rest. Costs one extra request per match and lowers the per-run ceiling to 300.

## `includeOdds` (type: `boolean`):

Bookmaker markets for each match. Costs one extra request per match and lowers the per-run ceiling to 300.

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

Full rally-by-rally record. Costs one extra request per match, returns a lot of data, and lowers the per-run ceiling to 300.

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

Capped at 2000 per run, or 300 when any per-match extra is switched on.

## Actor input object example

```json
{
  "mode": "schedule",
  "dates": [],
  "dateFrom": "2026-06-29",
  "dateTo": "2026-07-12",
  "dayOffsets": [
    -1,
    0,
    1
  ],
  "statusFilter": "all",
  "tourFilter": [],
  "matchFormat": "all",
  "maxTournaments": 12,
  "playerQuery": "Carlos Alcaraz",
  "playerIds": [],
  "matchesPerPlayer": 20,
  "includeUpcoming": true,
  "tour": "both",
  "minRanking": 0,
  "includeHeadToHead": false,
  "includeRecentForm": false,
  "includeStatistics": false,
  "includeOdds": false,
  "includePointByPoint": false,
  "maxItems": 100
}
```

# Actor output Schema

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

Matches or rankings, depending on mode.

## `runSummary` (type: `string`):

Records delivered, Unblocker requests used, and why the run stopped.

# 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 = {
    "dates": [],
    "dateFrom": "2026-06-29",
    "dateTo": "2026-07-12",
    "dayOffsets": [
        -1,
        0,
        1
    ],
    "playerQuery": "Carlos Alcaraz"
};

// Run the Actor and wait for it to finish
const run = await client.actor("cleanrows/tennis-data-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 = {
    "dates": [],
    "dateFrom": "2026-06-29",
    "dateTo": "2026-07-12",
    "dayOffsets": [
        -1,
        0,
        1,
    ],
    "playerQuery": "Carlos Alcaraz",
}

# Run the Actor and wait for it to finish
run = client.actor("cleanrows/tennis-data-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 '{
  "dates": [],
  "dateFrom": "2026-06-29",
  "dateTo": "2026-07-12",
  "dayOffsets": [
    -1,
    0,
    1
  ],
  "playerQuery": "Carlos Alcaraz"
}' |
apify call cleanrows/tennis-data-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cleanrows/tennis-data-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/j4LU3yQPx5uCao87g/builds/SH8TSbml2I5jEldco/openapi.json
