# Tennis Scraper (`crawlstone/tennis-scraper`) Actor

Extract normalized tennis data from SofaScore and Tennis Abstract, including live matches, tournaments, point-by-point records, player statistics, and ATP, WTA, and UTR rankings.

- **URL**: https://apify.com/crawlstone/tennis-scraper.md
- **Developed by:** [Crawl Stone](https://apify.com/crawlstone) (community)
- **Categories:** Developer tools, Automation
- **Features:** Limited permissions
- **Stats:** 2,750 total users, 825 monthly users, 89.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $6.00 / 1,000 successful results

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 Scraper

We built Tennis Scraper to provide a straightforward, query-based way to collect clean tennis data. This Actor connects to **SofaScore** and **Tennis Abstract** to gather public data on-demand, resolving complex player-mapping logic internally so you can focus on data analysis.

Instead of writing custom browser automation or managing complex proxy rotation setups, you can use our pre-built modes to retrieve structured results on-demand.

> **Unofficial Actor:** This is an independent tool and is not affiliated with or endorsed by SofaScore or Tennis Abstract.

> **Looking for football data?** Use our companion [SofaScore Football Scraper](https://apify.com/crawlstone/sofascore-football-scraper) for live matches, fixtures, competitions, teams, players, and match details.

> **Looking for real-time WebSocket streaming?** This Actor is designed for run-based Dataset outputs. If you need a continuous WebSocket stream of live events for matches currently in progress rather than static snapshots, use our companion product: [SofaScore Tennis Data Stream](https://apify.com/crawlstone/sofascore-tennis-data-stream).

***

### What you can do with this Actor

We designed seven focused ways into the data so you can run specific tennis-data jobs without getting bogged down in boilerplate code. You can daisy-chain these modes together using the IDs returned in your datasets.

#### 1. Track live matches and odds (`liveMatches`)

Run the scraper with no configuration to get a current snapshot of matches currently available in the active SofaScore live tennis feed. You get the current set, game, point, and tiebreak scores, along with date-level match odds. Optionally narrow the results by match type, gender, and tournament level.

#### 2. Discover scheduled tournaments (`tournaments`)

Pass a specific date in `YYYY-MM-DD` format to find tournaments scheduled on that day. This returns the unique tournament ID, surface type, and UTC event counts.

#### 3. Explore tournament history and draws (`tournamentSeasons` and `tournamentDetails`)

Take a unique tournament ID (like Wimbledon `2361`) to:

- Find available seasons (`tournamentSeasons`).
- Pull tournament metadata, historical champions, and a chronological list of matches for a specific season (`tournamentDetails`).

#### 4. Inspect match anatomy (`pointByPoint`)

Pass a unique match ID to extract the set, game, and point-by-point progression. It identifies who was serving, who won each point, and includes period-level match statistics.

#### 5. Deep-dive into historical player statistics (`playerDetails`)

Pass a SofaScore player ID (from your match results). The Actor matches it against our built-in player map, fetches their Tennis Abstract profile, and parses their performance splits. You get:

- Historical results, rankings, and Elo ratings.
- Surface-specific splits (clay, hard, grass).
- Serving, returning, and match charting statistics.

#### 6. Pull current rankings (`rankings`)

Choose one ranking and get its current full list, in the same order SofaScore publishes it. Six rankings are available:

- ATP rankings (`atp`) and ATP live rankings (`atpLive`).
- WTA rankings (`wta`) and WTA live rankings (`wtaLive`).
- UTR men's rankings (`utrMen`) and UTR women's rankings (`utrWomen`).

Each row is one ranked player with their position, points, and country, plus the previous position, best position, and other ranking details when the list provides them. The player ID is the same one used in match results and in `playerDetails`.

***

### Quick Start

1. Click **Try for free** on the Actor page.
2. Under **Scraper mode**, choose **Live matches**.
3. Click **Start**.
4. Open the **Dataset** tab when the run finishes.

For your first run, you can use the default live match mode with no extra input:

```json
{
  "mode": "liveMatches"
}
```

If no matches are live when you run the Actor, the run succeeds with an empty dataset.

#### Filter live matches

In **Live matches** mode, choose the match types, genders, and tournament levels to include. Leave any filter blank to include all options.

| Input | Supported selections |
| --- | --- |
| `matchType` | `singles`, `doubles`, `mixedDoubles` |
| `gender` | `male`, `female`, `mixed` |
| `tournamentLevel` | `atp250`, `atp500`, `atp1000`, `wta125`, `wta250`, `wta500`, `wta1000`, `grandSlam`, `challenger`, `itf`, `davisCup`, `others` |

You can choose more than one option in each filter. For example, selecting **ITF** and **Challenger** includes either tournament level. Add **Singles** and **Female** to narrow those results to women's singles matches.

Set **Maximum live matches** (`maxMatches`) to limit how many matching results you receive. Leave it blank or use `0` to get all matching results.

Use the selections exactly as shown in the table, including capitalization. Unrecognized choices are ignored; if none are recognized, that filter includes all options.

This example requests up to 20 female singles matches in either ITF or Challenger tournaments. Keep the square brackets even when choosing just one option. To leave a filter unrestricted, remove it or use `[]`:

```json
{
  "mode": "liveMatches",
  "matchType": ["singles"],
  "gender": ["female"],
  "tournamentLevel": ["itf", "challenger"],
  "maxMatches": 20
}
```

Select `others` to include tournaments whose level cannot be classified. If no matches qualify, the run succeeds with an empty dataset and charges one `successful-result` event.

***

### Data Structure & Examples

Each dataset row represents a single tennis record. The Apify Dataset stores nested arrays and objects, and JSON exports preserve this rich hierarchical structure. (Tabular exports like CSV or Excel will flatten or serialize these fields).

#### Live Match Snippet (`liveMatches`)

*This is an abridged preview of selected fields, not a complete schema-valid Dataset row.*

```json
{
  "id": 16385412,
  "slug": "coco-gauff-karolina-muchova",
  "status": "3rd set",
  "tournamentName": "Wimbledon",
  "homePlayerName": "Karolina Muchova",
  "awayPlayerName": "Coco Gauff",
  "score": {
    "home": 2,
    "away": 1,
    "sets": [
      { "set": 1, "home": 6, "away": 2, "durationSeconds": 2357 },
      { "set": 2, "home": 1, "away": 6, "durationSeconds": 2127 }
    ]
  },
  "odds": {
    "home": { "fractional": "3/1", "decimal": 4.0 },
    "away": { "fractional": "2/9", "decimal": 1.22 }
  }
}
```

#### Player Profile Snippet (`playerDetails`)

*This is an abridged preview of selected fields, not a complete schema-valid Dataset row.*

```json
{
  "id": 14882,
  "fullName": "Jannik Sinner",
  "country": "Italy",
  "handedness": "Right-handed",
  "currentRank": 1,
  "eloRank": 1,
  "eloRating": 2245,
  "recentResults": [
    {
      "date": "2026-07-09",
      "tournament": "Wimbledon",
      "surface": "Grass",
      "result": "W",
      "opponentName": "Alexander Zverev",
      "score": "6-4 6-3 6-2",
      "dominanceRatio": 1.45,
      "acePercentage": 11.2
    }
  ]
}
```

***

### Operational & Billing Details

#### Managed Proxies

To handle rate limits and source blocks, SofaScore requests on Apify are automatically routed through residential proxies. No user-side proxy configuration or credentials are required. Note that while this helps protect runs from immediate blocking, it does not guarantee uninterrupted source availability.

#### Pay-per-Event Billing

We utilize Apify's Pay Per Event (PPE) model, which charges you based on successful operations rather than compute time or row counts:

- **One Successful Result Event:** A successful run charges exactly one custom `successful-result` event, regardless of how many rows are returned.
- **Uncharged Scrape Failures:** If a scraping operation fails due to network or source errors, the Actor writes a diagnostic error row with relevant request context when available, before failing the run. These scrape failures do not charge the `successful-result` event.
- **Empty Successes:** If a run returns zero results because no matches are live, no live matches satisfy your filters, or no tournaments are scheduled on your selected date, it is considered a valid successful operation and charges one `successful-result` event.

Check the Actor's **Pricing** tab for current rates.

***

### Use with Apify MCP

If you are integrating tennis data into AI workflows, you can run this Actor through Apify's hosted Model Context Protocol (MCP) server to connect directly to compatible AI clients (such as Claude Desktop):

```text
https://mcp.apify.com?tools=crawlstone/tennis-scraper
```

Once connected, you can run tennis-data queries using natural language through your AI assistant.

***

### Frequently Asked Questions

#### What should I do when a run returns zero rows?

This is normal. The scraper returns empty datasets when there are no live matches in progress (e.g., late at night between tournament days), no live matches satisfy your filters, or no tournaments are scheduled for a given date. For filtered live runs, add alternatives within a field or remove a filter; missing source metadata can exclude matches from explicit selections. Successful empty runs still charge one `successful-result` event. If you need regular updates, configure an Apify Schedule to run the Actor periodically.

#### Can I pass a Tennis Abstract URL directly?

No. Provide the positive SofaScore `playerId` found in match-bearing results (such as live matches or the nested `matches` array in tournament details). You cannot extract a player ID from generic tournament discovery rows. Our built-in player map automatically translates the SofaScore ID to find and parse the corresponding Tennis Abstract profile. Unmapped player IDs will return a descriptive error row and fail the run without charging the `successful-result` event.

#### Can I get continuous real-time updates?

No. This Actor is optimized for static, on-demand datasets. For live WebSocket streaming of active match events, use our companion product: [SofaScore Tennis Data Stream](https://apify.com/crawlstone/sofascore-tennis-data-stream).

***

### Support

If you need assistance, please open a ticket on this Actor's **Discussion** tab and include:

- Your Apify **Run ID**.
- The selected **Mode** and the **Input** values you provided.
- A description of what you expected versus what occurred.

Do not share your Apify API Token or other private credentials in support tickets.

# Changelog

This Actor's version history is a separate document: https://apify.com/crawlstone/tennis-scraper/changelog.md

# Actor input Schema

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

Select the data operation. The form preselects liveMatches. Known fields from another mode are accepted by the runtime and ignored; mode-specific requirements are checked when the run starts.

## `maxMatches` (type: `integer`):

Used only when Mode is liveMatches. Optional non-negative cap applied after filtering, in source order. Omit it or use 0 to return all matching live matches.

## `matchType` (type: `array`):

Used only when Mode is liveMatches. Select any matching format. Omit or leave empty for all formats. JSON values are case-sensitive.

## `gender` (type: `array`):

Used only when Mode is liveMatches. Select any matching source gender. Omit or leave empty for no gender restriction. Missing or unrecognized source gender does not match a selection; participant genders are not used to fill gaps. JSON values are case-sensitive.

## `tournamentLevel` (type: `array`):

Used only when Mode is liveMatches. Select any matching level. Omit or leave empty for all levels. Classification uses only tournament metadata in the live feed; others includes insufficient or unsupported metadata, even for recognizable tournament names. JSON values are case-sensitive.

## `date` (type: `string`):

Used only when Mode is tournaments. A real calendar date in YYYY-MM-DD format is required; this field discovers tournaments scheduled on that date.

## `matchId` (type: `integer`):

Used only when Mode is pointByPoint. A positive match ID is required; get it from a match returned by liveMatches or tournamentDetails.

## `tournamentId` (type: `integer`):

Used only when Mode is tournamentSeasons or tournamentDetails. A positive tournament ID is required; get it from tournaments or liveMatches.

## `seasonId` (type: `integer`):

Used only when Mode is tournamentDetails. Optional positive season ID; get it from tournamentSeasons, or omit it to resolve the newest season.

## `playerId` (type: `integer`):

Used only when Mode is playerDetails. A positive player ID is required; get it from match participants returned by liveMatches or tournamentDetails.

## `ranking` (type: `string`):

Used only when Mode is rankings. Select one ranking; each run returns that ranking's current rows in source order. JSON values are case-sensitive.

## Actor input object example

```json
{
  "mode": "liveMatches",
  "maxMatches": 10,
  "date": "2026-07-10",
  "matchId": 16385412,
  "tournamentId": 2361,
  "seasonId": 63966,
  "playerId": 206570,
  "ranking": "atp"
}
```

# Actor output Schema

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

The direct rows produced by the selected mode. Collection successes may produce zero or many rows, singleton successes produce one row, and failed scrape operations produce one error row when it can be saved.

# 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": "liveMatches"
};

// Run the Actor and wait for it to finish
const run = await client.actor("crawlstone/tennis-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 = { "mode": "liveMatches" }

# Run the Actor and wait for it to finish
run = client.actor("crawlstone/tennis-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 '{
  "mode": "liveMatches"
}' |
apify call crawlstone/tennis-scraper --silent --output-dataset

```

## MCP server setup

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