ActorStack.dev
SportsDeveloper toolsv0.1.13updated 16 September 2026

TennisExplorer Results, Schedule, Players & Rankings

A match status the source never labels, and a day that does not move.

ATP, WTA, Challenger and ITF tennis from TennisExplorer: finished results with set-by-set scores, upcoming schedules, player profiles with career history, and ranking tables back to 1995. Two things it adds that the source does not have — a derived match status, and a day boundary pinned to UTC so two runs of the same date return the same matches.

oswaldocarabano/tennisexplorer-scraper

input.json
{
  "entityType": "results",
  "dateFrom": "2026-08-01",
  "dateTo": "2026-08-07",
  "tours": ["atp-single", "wta-single"],
  "includeMatchDetail": false,
  "maxItems": 5000
}
Version
v0.1.13
Memory
4096 MB
Browser
none
Proxy
Residential pool, built in

Short answer

The TennisExplorer Actor extracts ATP, WTA, Challenger and ITF match results, upcoming schedules, player profiles and ranking tables, with history back to 1995. It derives a `status` field — `completed`, `retired` or `walkover` — that the source never labels anywhere, validated against 2,364 matches across nine tournaments including Davis Cup, and affecting about 4.6% of matches. Every request is pinned to UTC because the site's own day boundary follows a timezone cookie: the same date returned 227 matches in one timezone and 269 in another. Pricing is $0.0015 per result for the first 10,000 rows of a run and $0.0003 after that, so a full season of about 148,000 matches costs roughly $56.

Key points

  • A derived `status` field — `completed`, `retired` or `walkover` — that TennisExplorer does not label anywhere. Validated against 2,364 matches across nine tournaments with different formats, including Davis Cup, which mixes best-of-3 and best-of-5 on one page. About 4.6% of matches are affected.
  • Every request is pinned to UTC. The site decides which matches belong to which day from a timezone cookie, and the same date returned 227 matches in one timezone against 269 in another during testing.
  • Full names in doubles. The visible team name is truncated to something like `Roger-Vas`; the Actor reads the full name from the underlying attribute, which is 17.4% of all rows.
  • History back to 1995 with the same input. A full season is about 148,000 matches — measured, not estimated — and one day of results costs one request.
  • Two price tiers so backfills are affordable: $0.0015 per result for the first 10,000 rows of a run and $0.0003 after that. A week of results is about $4 and a season about $56.
  • Betting odds are off by default, because odds are licensed from bookmakers while match scores are facts. Failed requests are never charged and go to the ERRORS record instead.
On this page11 sections

What it does

ATP, WTA, Challenger and ITF tennis from TennisExplorer: finished results with set-by-set scores, upcoming schedules, player profiles with career history, and ranking tables back to 1995. Two things it adds that the source does not have — a derived match status, and a day boundary pinned to UTC so two runs of the same date return the same matches.

output — one row
{
  "entity_type": "results",
  "match_id": "2026-08-03-atp-single-1184",
  "start_time_utc": "2026-08-03T14:30:00Z",
  "tour": "atp-single",
  "tournament": "Toronto",
  "home_player": "Fritz Taylor",
  "away_player": "Rublev Andrey",
  "sets": "6-4, 3-2",
  "home_sets": 1,
  "away_sets": 0,
  "status": "retired",
  "odds_home": null,
  "odds_away": null
}

Why this one

The status the source refuses to state

TennisExplorer does not label retirements or walkovers anywhere. An unfinished match simply shows a scoreline that never closes, and every consumer of that data has to guess. This Actor derives `status` from the scoreline itself, and the derivation works for best-of-3 and best-of-5 without being told which is which — which is the hard part, and why it was validated on Davis Cup, where both formats appear on the same page.

`retired` means one thing, and it is stated

`retired` means exactly that the match started and did not finish: a retirement, an injury, a default, or an abandoned dead rubber. The source does not distinguish between those, so neither does the Actor. Splitting them would be inventing a distinction the data does not carry, and a consumer who assumed injury from a `retired` flag would be wrong a good share of the time.

A day boundary that does not move

The same date returned 227 matches in one timezone and 269 in another, because the site cuts its day using a cookie. Any dataset built without pinning that is not reproducible: two runs of the same date disagree, and neither is wrong. Every request here is pinned to UTC and every time field is named `start_time_utc`, so the boundary is visible in the field name rather than assumed.

Sets recounted where the source publishes a flag

For a match that did not finish, the site's result column is a won/lost flag rather than a set count — it reads `1-0` no matter how many sets were actually played. `home_sets` and `away_sets` are recounted from the scoreline in that case, so they are never the flag, and `sets` always carries the raw scoreline for anyone who wants to check the recount.

One entity type per run

Results, schedules, players and rankings have genuinely different fields, so the Actor returns one type per run and the file you download has one clean shape. Mixing them would give every row the union of four schemas and leave the consumer to work out which columns apply.

Use cases

  • Build a match-results dataset with a usable status field, for modelling or for reporting.
  • Backfill a full season, or the whole 1995 onward archive, at the bulk row price.
  • Track upcoming schedules and in-progress matches on a fixed UTC day boundary.
  • Pull ATP or WTA ranking tables, singles, doubles or race, for a current or historical week.
  • Collect player profiles with as many past seasons of match history as you need.

Input

Every field has a default, and the defaults are deliberately small so a first run is cheap enough to inspect before you commit to a sweep. This table mirrors the Actor's own input schema field for field.

FieldDefaultWhat it does
entityTypestring"results"What to scrape`results` for finished matches, `schedule` for upcoming and in-progress ones, `players` for profiles, `rankings` for ATP or WTA tables. One type per run, so the dataset has one clean shape.
dateFromstring"yesterday"From date (UTC)`YYYY-MM-DD`, or a relative date: `today`, `yesterday`, `-7d`, `+3d`. Days are cut at 00:00 UTC always, which is what makes two runs of the same date agree. History goes back to 1995.
dateTostring"today"To date (UTC)Inclusive. One request covers one whole day and typically returns 150 to 600 matches.
toursstring[]all fourToursATP singles, ATP doubles, WTA singles, WTA doubles. Leaving all four selected is one request per day, which is the cheapest option.
includeOddsbooleanfalseInclude betting oddspersonal dataOff by design. Odds are licensed from bookmakers and are the part of the source with the weakest reuse position; match scores are facts and odds are not. Turn it on only with your own basis for using them.
includeMatchDetailbooleanfalseFetch match detail pagesAdds round, surface, full player names, both rankings and head-to-head, at one extra request per match. A day is 200 to 600 matches, so leave it off unless you need those fields.
rankingTourstring"atp-men"RankingATP (men) or WTA (women).
rankingTypestring"singles"Ranking typeSingles, doubles, or the season race. Race tables only exist for the current season.
rankingDatestring""Ranking weekA historical ranking week as `YYYY-MM-DD`. It must be one of the publication dates the site offers; empty means the latest.
playerUrlsstring[][]Player URLsTennisExplorer player pages to read. Leave empty to take the players from the selected ranking instead.
playerMatchHistoryYearsinteger0Years of match history per player0 reads the profile only. Each extra season is one extra request per player.
maxItemsinteger10000Maximum resultsA hard cap on delivered rows; you are only charged for what is delivered. Raise it for backfills — a season is about 148,000 matches and the whole 1995 to 2026 archive is around 2.4 million.
maxConcurrencyinteger8Concurrency8 is plenty for day-to-day use. Capped at 16, the highest level measured against the site, and capped again by the run's memory: a 512 MB run tops out around 10.
proxyConfigurationobjectbuilt-in poolProxy configurationOptional. The Actor already routes through its own residential pool, so leaving this empty just works. Set it only to use your own Apify proxy, or none.

Output and fill rates

A field being in the schema is not the same as it having a value. The percentages below were counted on real runs; the sample sizes are in Measurements. Anything not listed here is not promised.

FieldFilledMeaning
statusstring100%`completed`, `retired` or `walkover`. Derived: the source labels none of them. About 4.6% of matches are not `completed`.
start_time_utcdatetime100%Pinned to UTC, and the field name says so. The site's own day boundary follows a timezone cookie.
home_playerstring100%With `away_player`. Full names, read from the underlying attribute rather than the truncated visible label — which matters on the 17.4% of rows that are doubles.
setsstringnot measuredThe raw scoreline as published, always, so a recount can be checked against it.
home_setsintegernot measuredWith `away_sets`. Recounted from the scoreline on unfinished matches, where the source publishes a won/lost flag rather than a set count.
tourstring100%`atp-single`, `atp-double`, `wta-single` or `wta-double`.
tournamentstringnot measuredThe tournament the match belongs to.
roundstringnot measuredWith `surface`, both rankings and head-to-head. Only when match detail is enabled.
odds_homenumbernot measuredWith `odds_away`. Only when odds are explicitly enabled, which they are not by default.
birth_yearintegernot measuredOn player rows. Returned instead of a full birthdate for players under 18, who also carry no photo URL.
rankintegernot measuredOn ranking rows, with points and the publication week.

Every key is always present. A field that exists but is empty comes back as explicit null, so a parser never has to guess.

Datasets

Different record types go to different datasets, so the main table never carries columns that are blank on most rows.

  • defaultOne row per match, player or ranking entry, depending on the entity type the run selected.billed
  • ERRORS (key-value store)Every failed request with its reason. Failed requests are never charged.never billed

Pricing

Pay per delivered result. Charges are applied as each row is produced rather than in a lump at the end, so an aborted run bills only for what it actually gave you.

EventPriceNotes
match-resultResult$0.0015One match, player or ranking row delivered to your dataset, for the first 10,000 rows of a run.
match-result-bulkResult (bulk)$0.0003Rows beyond the first 10,000 in the same run, so historical backfills stay affordable. A full season of ~148,000 matches is about $56.
match-detailMatch detail (surcharge)$0.0024Per match enriched with round, surface, full names, both rankings and head-to-head. One extra request each.

Measurements

Each figure is shown with the method that produced it. A benchmark without a method is a marketing claim wearing a number's clothes.

Status validation

2,364 matches across nine tournaments

Formats deliberately mixed, including Davis Cup, which puts best-of-3 and best-of-5 on the same page. The derivation does not need to be told which format a match is.

Matches affected by status

about 4.6%

Retirements, walkovers and abandoned matches together, from the same validation set. The remaining 95.4% are `completed`.

Timezone drift

227 matches against 269, same date

The same requested date in two timezones, because the site cuts its day from a cookie. Every request is pinned to UTC to remove it.

Truncated names

17.4% of all rows

Doubles rows, where the visible team label is truncated. The full name is read from the underlying attribute instead.

Season size

about 148,000 matches

Measured across a full season, not extrapolated. One day of results is one request; a season is 365.

Archive depth

1995 to 2026, around 2.4 million matches

The same input shape works for any past date, so a backfill is a date range rather than a different mode.

What it will not do

Stated plainly so you can judge fit before spending anything.

  • `retired` covers retirement, injury, default and an abandoned dead rubber together, because the source does not distinguish them.
  • Players under 18 are returned with `birth_year` rather than a full birthdate, and without a photo URL.
  • One entity type per run. Results, schedule, players and rankings need separate runs.
  • Race ranking tables only exist for the current season.
  • A historical ranking week must be one of the publication dates the site offers; an arbitrary date will not resolve.
  • Match detail costs one extra request per match, and a day is 200 to 600 matches, so it is off by default.
  • `maxConcurrency` is capped at 16, which is the highest level actually measured against the site, and is capped again by the memory the run was given — a 512 MB run tops out around 10.

Privacy

  • Match results, schedules and ranking tables are sporting facts published for the public to read.
  • Players under 18 are returned with `birth_year` instead of a full birthdate, and without a photo URL. That is a deliberate reduction, not a gap in the source.
  • Betting odds are off by default: they come from bookmakers rather than from the site, and redistributing them sits in a different position from redistributing scores.
  • This Actor is independent and is not affiliated with, endorsed by, or connected to TennisExplorer or its operator.
  • Removal requests: privacy@actorstack.dev

See also the data removal process.

Frequently asked questions

How do I tell a retirement from a completed match?
With the `status` field, which this Actor derives because TennisExplorer does not label retirements or walkovers anywhere — an unfinished match just shows a scoreline that never closes. `status` is `completed`, `retired` or `walkover`, it works for best-of-3 and best-of-5 without being told which is which, and it was validated on 2,364 matches across nine tournaments including Davis Cup. About 4.6% of matches are affected.
Does `retired` mean an injury?
Not specifically. It means the match started and did not finish: a retirement, an injury, a default, or an abandoned dead rubber. The source does not distinguish between those, so neither does the Actor — inventing the distinction would put a fact in your dataset that nobody measured.
Why are match counts different between tools?
Because TennisExplorer decides which matches belong to which day from a timezone cookie. The same date returned 227 matches in one timezone and 269 in another during testing. This Actor pins every request to UTC and names every time field `start_time_utc`, so two runs of the same date return the same set of matches.
How far back does the history go?
To 1995, with the same input shape — a backfill is just a wider date range. A full season is about 148,000 matches and the whole archive is roughly 2.4 million. One day of results costs one request; a season costs 365.
What does a season of history cost?
About $56. The first 10,000 rows of a run are $0.0015 each and everything after that is $0.0003, which is what makes a backfill affordable rather than a research budget. A week of results is about $4.
Why are betting odds off by default?
Because the odds on the site come from bookmakers rather than from the site itself, and redistributing them sits in a very different position from redistributing match scores. Scores are facts. Turn `includeOdds` on only if you have your own basis for using them.
Can I get results, rankings and players in one run?
No, and that is deliberate. Those four entity types have genuinely different fields, so the Actor returns one per run and the file you download has one clean shape rather than the union of four schemas with most columns empty.

Guides for this Actor