GuideHistorical tennis data

The data behind a tennis prediction model.

Win probabilities come from history done right: rankings as they stood on the day, results split by surface, form windows and serve stats. Every input a tennis model needs, with no look-ahead built in.

  • Rankings as of any week
  • Surface and level splits
  • No odds, by design
GET /tennis/players/atp_206173/stats?surface=Clay200 OK
$ curl "https://api.citoapi.com/api/v1/tennis/players/atp_206173/stats?surface=Clay" \ -H "x-api-key: $CITO_API_KEY"
{  "player_name": "Jannik Sinner",  "filters": {    "surface": "Clay"  },  "career_summary": {    "matches_played": 150,    "matches_won": 108,    "matches_lost": 42,    "win_percentage": 72,    "titles_count": 5  },  "serving_stats": {    "total_aces": 441,    "first_serve_in_pct": 59.3,    "first_serve_points_won_pct": 71.3,    "break_points_saved_pct": 60.7  }}

Seamless Integration with our MCP Server

Give Claude, Cursor, and ChatGPT the tennis archive. One command writes the MCP config, agents call match, ranking and head-to-head tools with real player IDs instead of guessing, and your key stays on your machine.

MCPInstall the Cito MCP server
Claude
Cursor
OpenAI
Python
Cito API
Next.js
Discord
Slack
GitHub

Model inputs, ready to query

The features that move tennis win probabilities, each one available without scraping or reconciling sources.

Point-in-time rankings

Ask for the list as of any date, so a backtest only sees what was known before the match.

Surface splits

Win-loss and serve stats filtered by Hard, Clay, Grass or Carpet, and by level or year range.

Head-to-head

Every meeting between two players with surface, level and best-of splits.

Form windows

Recent results and win rate per player, and the full match log to build your own windows.

Serve and return stats

Aces, first-serve %, points won on serve, break points: from 1991 for the men, 2016 for the women.

Live state

Set, game and server during play for in-match models, over REST or WebSocket.

What data do you need to predict tennis matches?

Most tennis models start from a rating (Elo or a ranking), then adjust for surface, recent form, head-to-head and serve and return strength. Each input must be measured as of the match date, or the model learns from the future.

Model inputEndpointNote
Results to build Elo/tennis/matches?tour=ATP&year=2015Page through seasons; order by match_date, then round
Ranking on the day/tennis/rankings?tour=ATP&date=2019-06-03Any published week; points are null where the tour did not publish them
One player's ranking path/tennis/rankings/history/{player_id}Week-by-week rank and points
Surface win rate/tennis/players/{player_id}/stats?surface=ClayAlso filters by level and year range
Head-to-head/tennis/h2h?player1_id=&player2_id=Surface, level, format, tiebreak and deciding-set splits
Recent form/tennis/players/{player_id}/formOr build windows from /players/{player_id}/matches
Serve and return/tennis/matches/{match_id}/statsPer-match box score

How do I build tennis Elo ratings from the API?

Read results in date order and update both players after every match. The example below rates every ATP match from 2015 onward with a simple K-factor; surface-specific Elo is the same loop keyed by surface.

Python: ATP Elo from the match archive
import requests
from collections import defaultdict

API = "https://api.citoapi.com/api/v1/tennis/matches"
HEADERS = {"x-api-key": "YOUR_API_KEY"}
ROUND_ORDER = {"Q1": 0, "Q2": 1, "Q3": 2, "R128": 3, "R64": 4, "R32": 5, "R16": 6, "QF": 7, "SF": 8, "F": 9}

def season(year):
    page = 1
    while True:
        r = requests.get(API, headers=HEADERS, params={"tour": "ATP", "year": year, "page": page, "page_size": 100}).json()
        yield from r["items"]
        if not r["has_next"]:
            return
        page += 1

elo = defaultdict(lambda: 1500.0)
K = 32
for year in range(2015, 2027):
    matches = sorted(season(year), key=lambda m: (m["match_date"], ROUND_ORDER.get(m["round"], 5)))
    for m in matches:
        w, l = m["winner_id"], m["loser_id"]
        expected = 1 / (1 + 10 ** ((elo[l] - elo[w]) / 400))
        elo[w] += K * (1 - expected)
        elo[l] -= K * (1 - expected)

top = sorted(elo.items(), key=lambda kv: -kv[1])[:10]
print(top)

Before 2004 many matches carry their tournament's start date (date_is_tournament_start: true), which is why the sort falls back to round order inside a tournament.

How do I avoid look-ahead bias in a tennis backtest?

Use inputs as they stood before the match. Request the ranking list dated on or before the match date rather than today's ranking, compute form from matches strictly earlier than the one you predict, and use career stats filtered by year_to rather than totals that include the future.

Does the API include betting odds?

No. We do not sell bookmaker odds. Models that price matches pair this data with a licensed odds feed: the odds tell you the market's price, and this data tells you what the price should be.

Endpoints you can ship with

Start on players: profiles, filtered career stats, form and the full match log.

Players

Player profiles with country, hand and birth date, career serve and return stats, recent form, and every match played.

View docs
  • GET/api/v1/tennis/players/search?q={name}

    Find a player by name and get their id.

  • GET/api/v1/tennis/players/{player_id}

    Name, country, hand, height, birth date and ranking.

  • GET/api/v1/tennis/players/{player_id}/stats

    Career and season serve, return and win-loss stats.

  • GET/api/v1/tennis/players/{player_id}/form

    Recent results and win rate.

  • GET/api/v1/tennis/players/{player_id}/matches

    The player's match log, newest first.

Figures on this page were checked against the live API on 2026-10-24.

FAQs

Model inputs, backtests and coverage

Can't find what you're looking for? Contact our customer support team

See plans

Feed your Elo model

Pull a decade of results on a free key, then scale to live data and higher limits when the model goes live.