Docs/Tennis API/Batch & include

Fetch many matches in a few calls

Calling /tennis/matches/{id}/stats once per match is the slowest and most expensive way to read a lot of tennis. A week of results is about 1,400 matches: that is 1,400 calls one by one, or 14 calls with include=stats. Pick the row below that fits what you have.

Which one to use

You needUseCounts as
Statistics for matches you already have ids forGET /tennis/matches/stats?ids=a,b,c1 call per 10 ids (50 ids = 5 calls)
A list of matches (a day, a week, a player, a tournament) with their statisticsAdd include=stats to the list1 call per page (up to 100 matches)
Full match rows for ids you already haveGET /tennis/matches?ids=a,b,c (rows in data, not items)1 call per 10 ids
Player profiles for a rosterGET /tennis/players/batch?ids=a,b,c (unknown ids in missing)1 call (up to 50 ids)

An ids= batch on /matches/stats or /matches counts one call per started block of 10 ids, against both the monthly total and the per-minute limit. A list page counts as one call whatever its size, with or without include=stats. See Rate limits.

Statistics for up to 50 matches: /matches/stats

Send up to 50 match ids, comma-separated or as repeated ids=. Each item in items is exactly what /tennis/matches/{id}/stats returns for that match, in the order you sent. Ids with no match are listed in not_found instead of failing the call. Repeated ids are answered once.

bash
curl "https://api.citoapi.com/api/v1/tennis/matches/stats?ids={matchId},atp_2024_540_701,atp_2021_540_999" \
  -H "x-api-key: YOUR_API_KEY"
json
{
  "success": true,
  "items": [
    {
      "match_id": "{matchId}",
      "status": "COMPLETED",
      "score": "7-6 7-5",
      "stats_available": true,
      "stats": {
        "winner": { "aces": 13, "double_faults": 3, "first_serve_pct": 48.6, "first_serve_won_pct": 79.4, "service_points_won": 51, "total_points_won": 76 },
        "loser":  { "aces": 14, "double_faults": 3, "first_serve_pct": 56.2, "first_serve_won_pct": 70.7, "service_points_won": 48, "total_points_won": 67 },
        "player1": { "...": "same numbers, by draw side" },
        "player2": { "...": "same numbers, by draw side" }
      },
      "point_stats": { "winner": { "break_points_converted": 2, "break_point_opportunities": 5 }, "loser": { "...": "..." } },
      "sets": [ { "set_number": 1, "player1_games": 7, "player2_games": 6 }, { "set_number": 2, "player1_games": 7, "player2_games": 5 } ]
    },
    { "match_id": "atp_2024_540_701", "...": "..." }
  ],
  "total": 2,
  "requested": 3,
  "not_found": ["atp_2021_540_999"]
}
  • Limit: 50 distinct ids. 51 or more returns 422 asking you to split the list into calls of 50. No ids returns 422.
  • Ids: every id form the single route accepts works here, including merged ids, which resolve to the match they were merged into.
  • Gaps: a match without published statistics comes back with stats_available: false and a machine-readable stats_unavailable_code and stats_unavailable_reason, the same as the single route.
  • Cost: 50 ids count as 5 calls instead of 50.

Statistics on a match list: include=stats

Add include=stats to any of these lists and every row gains stats, stats_available, stats_unavailable_code, stats_unavailable_reason, stats_points_counted, stats_coverage and point_stats: the same values as /tennis/matches/{id}/stats. Without include, rows are unchanged.

/tennis/matches/tennis/matches/recent/tennis/matches/completed/tennis/matches/upcoming/tennis/players/{id}/matches/tennis/tournaments/{id}/schedule
bash
# Every match of one week, with statistics, 100 per page
curl "https://api.citoapi.com/api/v1/tennis/matches?date_start=2026-11-01&date_end=2026-11-07&page_size=100&include=stats&page=1" \
  -H "x-api-key: YOUR_API_KEY"

# Yesterday's results with statistics
curl "https://api.citoapi.com/api/v1/tennis/matches/completed?date=2026-11-07&page_size=100&include=stats" \
  -H "x-api-key: YOUR_API_KEY"

# One player's last 50 matches with statistics
curl "https://api.citoapi.com/api/v1/tennis/players/atp_206173/matches?page_size=50&include=stats" \
  -H "x-api-key: YOUR_API_KEY"
  • Upcoming matches: stats_available is false with the code not_published_yet.
  • Valid values: include accepts stats. Any other value returns 422 naming the valid ones. Tennis odds are not offered yet, so include=odds returns 422 too.
  • Paging: lists page with page and page_size (up to 100); walk page until has_next is false.

Pull a whole week or season

Walk the pages of one filtered list with include=stats. A week of every tour, Challenger and ITF match (about 1,400) takes 14 calls.

javascript
const BASE = "https://api.citoapi.com/api/v1/tennis";
const headers = { "x-api-key": process.env.CITO_API_KEY };

async function matchesWithStats(params) {
  const rows = [];
  for (let page = 1; ; page++) {
    const qs = new URLSearchParams({ ...params, include: "stats", page_size: "100", page: String(page) });
    const res = await fetch(`${BASE}/matches?${qs}`, { headers });
    if (res.status === 429) { await new Promise((r) => setTimeout(r, 6000)); page--; continue; }
    const body = await res.json();
    rows.push(...body.items);
    if (!body.has_next) return rows;
  }
}

const week = await matchesWithStats({ date_start: "2026-11-01", date_end: "2026-11-07" });
console.log(week.length, "matches;", week.filter((m) => m.stats_available).length, "with statistics");
python
import os, time, requests

BASE = "https://api.citoapi.com/api/v1/tennis"
HEADERS = {"x-api-key": os.environ["CITO_API_KEY"]}

def matches_with_stats(**params):
    rows, page = [], 1
    while True:
        r = requests.get(f"{BASE}/matches", headers=HEADERS,
                         params={**params, "include": "stats", "page_size": 100, "page": page})
        if r.status_code == 429:
            time.sleep(6)
            continue
        body = r.json()
        rows += body["items"]
        if not body["has_next"]:
            return rows
        page += 1

# edition_year keeps an event in its season even when it starts in late December
season = matches_with_stats(edition_year=2025, tour="ATP", level="A")
print(len(season), "ATP tour-level matches in 2025")  # about 1,960 matches in 20 calls

Statistics for ids you stored

When you already hold match ids (from your database or a webhook), send them 50 at a time.

javascript
async function statsFor(ids) {
  const out = new Map();
  for (let i = 0; i < ids.length; i += 50) {
    const chunk = ids.slice(i, i + 50);
    const res = await fetch(`${BASE}/matches/stats?ids=${chunk.join(",")}`, { headers });
    const body = await res.json();
    for (const item of body.items) out.set(item.match_id, item);
    for (const missing of body.not_found) console.warn("no match", missing);
  }
  return out;
}

Mistyped ids tell you what you meant

A 404 for an unknown player, match or tournament id carries error.suggestions: up to 5 ids that exist, each with a name, short context and a reason. Every suggested id resolves.

json
GET /tennis/players/wta_104925

{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "status": 404,
    "message": "Player with ID 'wta_104925' not found.",
    "suggestions": [
      { "id": "atp_104925", "name": "Novak Djokovic", "tour": "ATP", "ioc": "SRB", "birth_year": 1987, "reason": "other_tour" }
    ]
  }
}

reason is one of normalized, other_tour, padding, other_season, nearby_match, live_id or name_match. Match suggestions carry tournament_id, tournament_name, year, round and match_date; tournament suggestions carry year, tour, level and start_date. A typed name works too: /tennis/players/atp_djokovic suggests atp_104925.

Was this page helpful?