# Tennis Scores: ATP, WTA and Challenger Results (`quilldata/tennis-scores`) Actor

Daily tennis results for analytics, dashboards, research and media apps. ATP, WTA and Challenger singles and doubles with set scores and tiebreaks, plus automatic failover to backup sources.

- **URL**: https://apify.com/quilldata/tennis-scores.md
- **Developed by:** [Quill Data](https://apify.com/quilldata) (community)
- **Categories:** Sports
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 matches

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?

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 Scores: ATP, WTA and Challenger Results with Source Failover

Daily tennis scoreboards as clean dataset rows: one row per match, with set-by-set games, tiebreak points, status and winner. Built for sports analytics, dashboards, research projects and media apps that need a steady feed of public results.

The Actor reads one primary scoreboard and keeps going when it breaks. If the primary errors, is rate limited, times out, changes its format, or returns nothing for a date, the run switches to backup sources automatically and marks every row it filled that way.

### What you get

- **Tours:** ATP, WTA and ATP Challenger. Singles and doubles.
- **Match states:** scheduled, live (with the current set), finished, retired, walkover, cancelled.
- **Scores:** games per set, tiebreak points, sets won, a score line such as `6-7 4-6`, winner, match duration where published.
- **Players:** public name, nationality and ranking when the source shows them. No other personal data.
- **Optional match stats** (aces, double faults, break points, serve points) when the primary source publishes them.
- **Provenance on every row:** which source served it and whether a backup was used.

### Coverage by source

| Source | Tours | Doubles | Live | Tiebreaks | Round | Stats | Requests per date |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 365scores (primary) | ATP, WTA, Challenger | yes | yes | yes | yes | yes, optional | 1 (+1 per match for stats) |
| livescore (backup 1) | ATP, WTA, Challenger | yes | yes | yes | no | no | 1 |
| espn (backup 2) | ATP, WTA | yes | yes | yes | yes | no | 2 |
| wta (backup 3) | WTA only | yes | partial | loser points only, winner side derived | yes | no | 1 + one per WTA event that week |

Challenger results come only from 365scores or livescore. If both are down, Challenger rows are missing for that date and the log says so. The WTA backup has no match stats, and stats in general exist only on 365scores rows.

### How failover works

1. Sources are tried in the order of `sources` (default `365scores, livescore, espn, wta`).
2. When the primary serves a date, no backup is contacted at all (unless `crossCheck` is on).
3. When it fails, backups fill the tours they cover, one after another, until every requested tour is covered or the list ends.
4. Rows from a backup carry `fallback_used: true` and a short `fallback_reason`, e.g. `365scores: HTTP 429`.

Triggers: HTTP errors and 5xx after retries, HTTP 429, timeouts, invalid JSON, a changed payload shape (parse errors or more than 20% of rows failing validation), and an empty scoreboard or no rows for the requested tours.

### Input

| Field | Default | Notes |
| --- | --- | --- |
| dates | today (UTC) | YYYY-MM-DD, at most 7 dates |
| circuits | ATP, WTA, Challenger | Doubles boards map onto the same tour |
| sources | 365scores, livescore, espn, wta | Order is priority. The first source is primary. Later sources are backups |
| includeDoubles | true | Set false to keep singles only |
| statuses | scheduled, live, finished | Walkovers and retirements count as finished |
| includeStats | false | 365scores only. One extra request per live or finished match that publishes stats |
| maxMatches | 200 | 1–2000, across all dates |
| playerNames | empty | Case-insensitive substring on either player |
| requestDelaySecs | 2 | Minimum 1 second from one request to the next |
| maxRequestsPerRun | 150 | 10–1000. Hard cap on HTTP requests in one run across all sources (stats requests included) |
| timezone | UTC | Sent as timezoneName on 365scores |
| crossCheck | false | Compare finished scores with the first backup that covers a selected tour |
| simulateFailure | empty | Testing only. Named sources fail before any request |
| healthCheck | false | Check sources and skip the dataset. No charge |
| healthCheckDate | yesterday UTC | YYYY-MM-DD used when healthCheck is true |

#### Input example

```json
{
  "dates": ["2026-10-21"],
  "circuits": ["ATP", "WTA", "Challenger"],
  "statuses": ["finished"],
  "includeDoubles": true,
  "maxMatches": 200
}
```

### Output example

A real row from a cloud test run on 2026-10-22:

```json
{
  "match_id": "4878233",
  "source": "365scores",
  "date_requested": "2026-10-21",
  "start_time_utc": "2026-10-21T03:03:33Z",
  "circuit": "Challenger",
  "discipline": "singles",
  "tournament": "Cali",
  "tournament_id": 6943,
  "round": "Round of 32",
  "status": "finished",
  "status_text": "Ended",
  "player1": {
    "name": "Federico Coria",
    "id": 47314,
    "country": "Argentina",
    "ranking": 746
  },
  "player2": {
    "name": "Nicolas Mejia",
    "id": 51765,
    "country": "Colombia",
    "ranking": 115
  },
  "sets_won_p1": 0,
  "sets_won_p2": 2,
  "set_scores": [
    {
      "set": 1,
      "p1": 6,
      "p2": 7,
      "tiebreak_p1": 4,
      "tiebreak_p2": 7,
      "duration": "01:11"
    },
    {
      "set": 2,
      "p1": 4,
      "p2": 6,
      "tiebreak_p1": null,
      "tiebreak_p2": null,
      "duration": "49'"
    }
  ],
  "score_text": "6-7 4-6",
  "winner": 2,
  "current_set": null,
  "match_duration": "02:00",
  "stats": null,
  "match_url": "https://www.365scores.com/en/tennis/match/cali/federico-coria-nicolas-mejia/4878233",
  "scraped_at": "2026-10-22T15:07:09Z",
  "fallback_used": false,
  "fallback_reason": null,
  "verified_by": null,
  "crosscheck": null
}
```

Extra fields:

- `source`: the source that served the row.
- `fallback_used` and `fallback_reason`: whether a backup supplied the row, and why the earlier sources were skipped.
- `verified_by` and `crosscheck`: filled only when `crossCheck` is on. `crosscheck` is `match`, `mismatch` or `not_found`; `verified_by` lists the backup that agreed.

Fields a source does not publish are `null`. `match_id`, `source`, `date_requested` and `scraped_at` are always set.

### Health check

Set `"healthCheck": true` to test every source on a recent finished date (`healthCheckDate`, default yesterday UTC). The run pushes no rows and charges nothing. It saves a report under the key `HEALTH_REPORT` in the default key-value store, and its status line reads like `health: 365scores PASS, livescore PASS, espn PASS, wta PASS`. The run fails if any checked source fails.

The same check runs outside Apify:

```
python scripts/health_check.py --date 2026-10-21
```

Exit code 0 means all sources passed. For monitoring, start the Actor once a day from an Apify schedule with `{"healthCheck": true}`, or call the script from cron.

### Pricing

Pay per event: **$0.001 per match row**. Nothing is charged for health checks, for dates where every source failed, or for empty results. A failed stats request still returns the match, with `stats` set to null.

### Request etiquette

Each source has its own queue: one request at a time, at least 1–2 seconds apart plus a random pause, and backups stay idle while the primary works. HTTP 429 honors `Retry-After` (one retry when it is 30 seconds or less), server errors get two retries with growing pauses, and 403 is never retried. `maxRequestsPerRun` caps the total requests of a run.

### Data sources

Scores come from public JSON scoreboards published by 365Scores, LiveScore, ESPN and the WTA. No login, key or browser is used. Each publisher has its own terms of use, and some of them restrict automated access or commercial reuse; you are responsible for how you use the data. This Actor is not affiliated with or endorsed by 365Scores, LiveScore, ESPN, the WTA or the ATP.

### Also from Quill Data

- **[Google Hotels Prices](https://apify.com/quilldata/google-hotels)** — nightly and total-stay hotel rates from public Google Hotels search.
- **[Google Flights Cheapest Fare Monitor](https://apify.com/quilldata/google-flights)** — cheapest public Google Flights fare for a route and date; failed or empty runs are free.

### Limits

- Live scores are a snapshot at run time. Schedule the Actor for regular updates.
- Up to 7 dates per run, UTC calendar days.
- Round names are not available from the livescore backup.

# Actor input Schema

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

Scoreboard dates in YYYY-MM-DD (UTC). At most 7. Empty means today UTC.

## `circuits` (type: `array`):

Tours to keep: ATP, WTA and Challenger. Doubles boards on the same tour stay when includeDoubles is true.

## `sources` (type: `array`):

Source order is priority. The first source is primary. Any later source is a backup, used only when an earlier source fails for that date. livescore, espn and wta have a terms risk: see README.

## `includeDoubles` (type: `boolean`):

Keep doubles matches. When false, only singles are pushed.

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

scheduled, live, and finished. Walkovers and retirements count as finished.

## `includeStats` (type: `boolean`):

365scores only. For live or finished matches that publish a stats feed, attach it. One extra request per such match.

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

Stop after this many pushed matches across all dates.

## `playerNames` (type: `array`):

Optional case-insensitive substrings. A match is kept when any player name contains any value.

## `requestDelaySecs` (type: `number`):

Pause from one scoreboard or stats request to the next. Minimum 1.

## `maxRequestsPerRun` (type: `integer`):

Hard cap on HTTP requests in one run across all sources (stats requests included).

## `timezone` (type: `string`):

IANA timezone name sent as timezoneName on 365scores. Default UTC.

## `crossCheck` (type: `boolean`):

When the primary source serves a date, compare finished scores with the first backup that covers a selected tour.

## `simulateFailure` (type: `array`):

Testing only. Named sources fail before any request.

## `healthCheck` (type: `boolean`):

Check each selected source and skip the dataset. No charge.

## `healthCheckDate` (type: `string`):

UTC date YYYY-MM-DD for the health check. Empty means yesterday UTC.

## Actor input object example

```json
{
  "circuits": [
    "ATP",
    "WTA",
    "Challenger"
  ],
  "sources": [
    "365scores",
    "livescore",
    "espn",
    "wta"
  ],
  "includeDoubles": true,
  "statuses": [
    "scheduled",
    "live",
    "finished"
  ],
  "includeStats": false,
  "maxMatches": 200,
  "requestDelaySecs": 2,
  "maxRequestsPerRun": 150,
  "timezone": "UTC",
  "crossCheck": false,
  "simulateFailure": [],
  "healthCheck": false
}
```

# Actor output Schema

## `matches` (type: `string`):

Default dataset of match rows (ATP, WTA, Challenger) with set scores, status, and source.

# 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 = {};

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

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

```

## MCP server setup

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

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/chMZkNTRGhP1FTOL1/builds/QUCHSx3dIpmTYQviY/openapi.json
