Tennis Data API: Live Scores, ATP WTA ITF Results & Rankings avatar

Tennis Data API: Live Scores, ATP WTA ITF Results & Rankings

Pricing

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

Go to Apify Store
Tennis Data API: Live Scores, ATP WTA ITF Results & Rankings

Tennis Data API: Live Scores, ATP WTA ITF Results & Rankings

Tennis data API and sports data API: tennis live scores for today's ATP and WTA matches (ITF opt-in), plus tennis match results (ATP results, WTA results, ITF results), match stats, tennis head to head records, tennis player stats and rankings. Search players and tournaments by name.

Pricing

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

Rating

0.0

(0)

Developer

Scott Helvick

Scott Helvick

Maintained by Community

Actor stats

0

Bookmarked

3

Total users

1

Monthly active users

11 hours ago

Last modified

Share

Sports data for tennis: the singles matches on court right now, plus completed ATP, WTA and ITF singles match results with scores, match statistics, head-to-head records, player statistics and ranking tables, from one index, refreshed several times a day. Comparing tennis results often means joining separate match histories, rankings and statistics with incompatible identifiers. This Actor returns the current live board, completed singles matches, head-to-head records, player statistics, ranking tables and incremental changes from one cross-tour index.

What this does

  • Live matches (default): live returns the matches on court now across the ATP and WTA boards, with the set score, the game in progress, the server, the court and the round. It needs no IDs, no dates and no other field, and its rows carry no data charge. Rows update within about a minute of the tours publishing them.
  • Find IDs by name: lookup turns a player or tournament name into the index ID the other modes take. Every mode below also accepts a name directly in place of an ID. Lookup rows are free.
  • 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.

Typical uses: pulling ATP results or WTA results for a tournament week, assembling historical tennis match data for a model, computing tennis player stats against a band of opponents, or keeping a local tennis dataset of match results and tennis rankings in step through the change feed.

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 sources

ApproachTypical fitIntegration boundary
Single-source scrapersOne tour or tournamentCaller reconciles identifiers and output formats across sources.
Static historical datasetsFixed research samplesFresh results and corrections require another update process.
Subscription sports-data APIsContracted coverage, possibly live dataAvailable history, statistics and redistribution rights depend on the product.
This ActorThe current board plus completed-match analysis across toursOne row contract for both, refreshed several times a day, revision feed; coverage and statistics remain source-dependent.

live reports the matches on court now; it is a board, not a point-by-point feed, and the other modes remain completed-results queries.

Input

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

FieldTypeRequiredDefaultDescription
modestringNolivelive, matches, h2h, player_stats, rankings, changes or lookup.
tourstringNo-atp, wta, challenger, itf; unfiltered for match history when omitted. Rankings defaults to ATP. live returns the ATP and WTA boards together when omitted; itf is opt-in.
dateFromstringNo-Inclusive match-date lower bound.
dateTostringNo-Inclusive upper bound; not before dateFrom.
tournamentIdstringNo-Index ID, or an exact tournament name: atp:540 or US Open. Combine with dates for an edition.
surfacestringNo-Stored label, matched exactly and case-sensitively; values are lowercase, such as grass, hard or clay.
roundsarray of stringsNo-R128, R64, R32, R16, QF, SF, F, RR, Q1, Q2, Q3; empty means unfiltered.
stagestringNo-main or qual; omission includes either where indexed.
statusesarray of stringsNo["completed","retired"]Status filter; statistics mode requires this default pair.
playerAstringFor h2h-Index ID, or an exact player name: atp:s0ag or Jannik Sinner.
playerBstringFor h2h-Index ID, or an exact player name: atp:a0e2 or Carlos Alcaraz. Must differ from playerA.
playerstringFor player_stats-Index ID, or an exact player name: wta:326408 or Iga Swiatek.
querystringFor lookup-Name text to search for in lookup mode; case- and accent-insensitive, no wildcards. Rejected in other modes.
lookupKindstringNoallplayers, tournaments or all; which catalogue lookup searches.
opponentRankMinintegerNo-Positive minimum numeric opponent rank; statistics only.
opponentRankMaxintegerNo-Positive maximum numeric opponent rank; not below the minimum.
rankingDatestringNo-Indexed ranking snapshot date, not a match-date filter.
rankMaxintegerNo-Top N: 100 returns positions 1 to 100. Not a row budget.
sinceintegerNo-Nonnegative revision checkpoint for changes; omission starts from zero.
cursorstringNo-Opaque continuation token; copy without editing. Rejected in changes mode, which resumes from since.
limitintegerNo5000Data-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.

Matches and h2h rows default to newest-first (order: "newest"); set order: "oldest" for oldest-first and retain that order when continuing with a cursor.

The status vocabulary also accepts walkover, default, bye, scheduled, in_progress and cancelled. These labels filter completed history only. live mode ignores statuses and reports its own state on every row: scheduled, live, completed, retired, walkover or suspended.

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. On the live board the same applies to the request, but every live row carries level, so Challenger matches can be filtered out after delivery.

Per-mode requirements and ignored fields

  • live: no required selectors and no ID needed. Uses tour and limit; ignores every match-history filter, player selector, ranking selector, statuses and since.
  • 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.
  • lookup: requires query. Uses lookupKind and tour; ignores every match-history filter, player selector, ranking selector, since and cursor.
  • 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.

Find IDs by name

The index keys players and tournaments by stable IDs such as atp:s0ag and wta:905. Two ways to get one:

Search for it. lookup returns up to 25 candidates per catalogue, ranked exact name first, then prefix, then substring. Matching ignores case and accents. Lookup rows carry no data charge.

{"mode":"lookup","query":"Sinner"}
{"record_type":"lookup", "mode":"lookup", "kind":"player", "id":"atp:s0ag",
"name":"Jannik Sinner", "tour":"atp", "country":"ITA",
"matches_indexed":412, "last_match_date":"2025-09-07"}

Set lookupKind to players or tournaments to search one catalogue, and tour to narrow to atp, wta or itf. A tournament candidate carries years and editions in place of the match counters.

Or pass the name itself. playerA, playerB, player and tournamentId each accept an exact name wherever they accept an ID. A value containing a tour prefix is treated as an ID; anything else is resolved by name before the query runs, and the run summary records what it resolved to.

Resolution is exact and deterministic — there is no nearest-match guessing:

  • Exactly one indexed record carries that name: it is used, and resolutions in OUTPUT reports the ID it resolved to.
  • Several records carry it, one of them on a main tour: a player who competes on the ATP or WTA tour normally also holds an ITF ID from juniors and futures, so a single ATP or WTA candidate alongside any number of ITF ones resolves to the main-tour ID, reported in OUTPUT as a tour_default resolution. Two main-tour candidates, or two on the same tour, remain a real choice and are not resolved.
  • Several records carry it, with no single main-tour candidate: the run succeeds without charging, and a free ambiguous_name notice lists up to ten candidates with their IDs. Choose one and pass the ID, or add tour.
  • None carries it exactly: the run succeeds without charging, and a free no_match notice points back at lookup.

Passing IDs is still the faster path: it skips a resolution step per field, and an ID never becomes ambiguous.

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 seven 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. Every row includes retry, derived from error_class: null on data rows, later to retry after a short delay, with_changes to correct or resume the request, or never to avoid repeating the same input.

Live match

{
"record_type": "live_match", "mode": "live",
"tour": "atp", "level": "challenger",
"tournament": "Phan Thiet", "tournament_id": "atp:3139",
"edition_id": "atp:3139:2026",
"round": "F", "round_label": "Final", "draw": "MS", "stage": "main",
"player_a": {
"name": "Rodrigo Pacheco Mendez", "player_id": "atp:p0j1",
"country": "MEX", "seed": null, "resolution": "source"
},
"player_b": {
"name": "Ilia Simakin", "player_id": "atp:s0o6",
"country": "RUS", "seed": 4, "resolution": "source"
},
"sets": [{"a": 6, "b": 3, "tiebreak": null}], "score": "6-3 3-1",
"game_score": {"a": "40", "b": "15"}, "server": "a",
"status": "live", "status_detail": null,
"court": "Center Court", "scheduled_start": null,
"started_at": "2026-09-20T02:32:24Z", "duration_s": 2740,
"source": "atp", "fetched_at": "2026-09-20T03:18:04Z",
"stale": false, "retry": null
}

player_a and player_b are sides, not outcomes: a match in progress has no winner, so winner and loser stay null on a live row. Each side carries the index player_id built from the identifier the tour itself publishes — a live match is never matched up by name — and player_id is null with resolution: "unresolved" when the index does not hold that player yet. The name is still delivered, and no extra row is raised for it.

status is scheduled, live, completed, retired, walkover or suspended, with status_detail preserving any finer or unfamiliar marker the board publishes. game_score, server, court and scheduled_start are null whenever the board does not publish them, which is routine: several boards give only a local date or free text for a start time, and a guessed timestamp is never emitted in its place. tournament_id is the same identifier tournamentId takes in the other modes, so a match on court joins straight to that tournament's history.

stale: true marks a row served from the last good read of its board rather than dropped from the answer.

Match

{
"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 a 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

{
"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

{
"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

{
"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.

Lookup

{
"record_type": "lookup", "mode": "lookup", "kind": "tournament", "id": "wta:905",
"name": "US Open", "tour": "wta", "years": [2020, 2021, 2022, 2023, 2024, 2025], "editions": 6
}

kind says which input field id belongs in: player for player, playerA or playerB, tournament for tournamentId. Player candidates carry country, matches_indexed and last_match_date instead of years and editions. Lookup rows are never charged.

Notice

{
"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. An ambiguous_name notice additionally carries candidates, each with id, name, tour and country. On the live board there is exactly one notice per tour that could not be refreshed, and its tour field names that tour; a tour that simply had no match on court raises none.

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, coverage, sources, resolutions, resolved_names and ambiguous_names. Delivered and charged counts describe data rows; notices are counted separately. resolutions lists each name this run turned into an ID — the field it came from, the name supplied and the record that answered — so the next run can pass the ID directly. Event counts are not monetary totals.

sources is populated in live mode only and reports, per tour asked for, a status and a matches count plus the age of the reading. It is what separates a quiet day from an outage: a tour with status: "ok" and matches: 0 simply had nothing on court and produces no notice, while a tour with status: "error" produces exactly one. A run with no matches at all and no failed tour delivers a single no_live_matches notice, which is worth repeating later rather than terminal.

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

What is on court right now — the default, so an empty input does the same:

{"mode":"live"}

The live ATP board only, Challenger matches included and separable on level:

{"mode":"live","tour":"atp"}

Turn a name into an ID:

{"mode":"lookup","query":"Sinner","lookupKind":"players"}

A head-to-head by name — the two players are resolved before the query runs:

{"mode":"h2h","playerA":"Jannik Sinner","playerB":"Carlos Alcaraz"}

A tournament window by name, narrowed with tour so the name resolves to one event:

{"mode":"matches","tournamentId":"US Open","tour":"wta","dateFrom":"2025-08-25","dateTo":"2025-09-07"}

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

{"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:

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:

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"])

Use from AI agents and MCP clients

  • Apify MCP: 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. lookup rows and live rows carry no data charge at all, so finding an ID and checking the current board each cost nothing beyond the platform's start event. 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_classMeaning and routing
source_errorTemporary source failure; the only class to retry immediately, allowing a few minutes between attempts.
no_matchValid filters matched no indexed rows, or a supplied name matches no indexed record exactly; reconsider scope, or search with lookup.
ambiguous_nameA supplied player or tournament name matches several indexed records. Nothing was charged; the notice lists the candidates with their IDs. Pass one ID, or add tour.
out_of_coverageThe 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_inputCorrect the input using the notice.
not_configuredRun configuration unavailable; no lookup or data charge.
invalid_requestRequest rejected; inspect and correct it.
not_separableChallenger-only filtering unavailable; results include ATP main tour.
no_live_matchesEvery tour asked for answered and none had a match on court. A quiet hour, not a fault: worth repeating later.
budget_exhaustedMaximum charge reached; resume with a suitable budget.
row_limitData-row limit reached; resume or raise the limit.
internal_errorA 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 and no_live_matches are worth retrying later — as data is added and as the day's play begins — 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. A 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.

Does an empty live board mean something is broken? No. Check sources in OUTPUT: a tour with status: "ok" and matches: 0 had nothing on court, and only status: "error" is an outage — which also adds a notice naming that tour.

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 the current board plus completed matches, refreshed several times a day. 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 point-level live detail. live reports the state of a match on court — sets, the game in progress, the server — not every point as it is played, and it covers what the tours publish on their own boards.
  • No odds. Betting markets and prices are outside the output.
  • No point-by-point history. Scores and available match statistics do not reconstruct rallies or individual points.
  • No doubles. Version one covers singles only.
  • No junior, masters, wheelchair or beach events. ITF coverage is the professional men's and women's World Tennis Tour; a player who has competed only in those other ITF events is not in the index.
  • No universal statistics guarantee. Availability varies by tour and era; absent match statistics use stats: null with stats_reason.

Use an odds feed for markets, or a point-level sports dataset for sequence analysis. Use a doubles-specific results feed for doubles coverage.