Install livetennisapi-export
The package is livetennisapi-export on PyPI. It needs Python 3.9 or later and installs the official Python client with it.
pip install livetennisapi-export
pip install "livetennisapi-export[parquet]"
The second line adds Parquet support. The parquet extra brings in pandas and pyarrow.
Set the API key
The command reads your key from the LIVETENNISAPI_KEY environment variable.
Get a free key.
export LIVETENNISAPI_KEY=twjp_...
There is no flag for the key, so the key is never part of the export command. Without the variable the command prints where to get a key and exits with status 2. It sends no request.
Which format should I pick?
The file name picks the format. All three hold the same rows.
| Format | File name | What a row is | Pick it when |
|---|---|---|---|
| CSV | .csv | Flat columns, one header line, every value as text | You want to open the file in a spreadsheet or load it into a tool that reads nothing else. |
| JSON Lines | .jsonl or .ndjson | One JSON object per line, exactly as the API returned it, nested objects included | You want the record unchanged, for a pipeline, a document store or a later reshape. |
| Parquet | .parquet or .pq | Flat columns with a type per column | You analyse the data in pandas, DuckDB or R and want numbers and booleans to stay typed. |
CSV is the one every tool opens. A reader has to work out the types again, because a CSV cell is text. JSON Lines is the only format that keeps nested objects as objects. Parquet keeps a type for each column, so a rank stays a number and a flag stays a boolean.
Use -o - with -f csv or -f jsonl to write to stdout.
Parquet is binary and needs a file name.
Which datasets can I export, and which plan does each need?
There are six datasets. Each one reads a single endpoint through the official Python client. The plans are the ones the package states.
| Dataset | Endpoint | Plan | Requests |
|---|---|---|---|
matches | GET /matches | FREE for live and upcoming. completed and cancelled are part of History and need BASIC or a History plan | One per page |
fixtures | GET /fixtures | FREE | One per page |
players | GET /players?search= | FREE | One per page |
player | GET /players/{playerId} | FREE | One per player id |
rankings | GET /rankings | PRO. --system elo needs ULTRA | One per page |
tournaments | GET /tournaments | FREE | One per page |
If your plan does not cover a request, the API answers 403. The command then prints the refusal and exits with status 1. Plans and prices are on the pricing page. The errors page explains each status.
The spec also gives a FREE key 20 history calls each calendar month, which completed
matches can use.
rankings exports one ranking table in rank order, so it needs --system.
The choices are atp, wta, itf_jt, itf_mt,
itf_wt, elo, atp_doubles and wta_doubles.
elo also needs --tour atp or --tour wta.
matches takes --status, and it defaults to upcoming.
# Upcoming fixtures on the ATP tour
livetennisapi-export fixtures --tour atp -o fixtures.csv
# Every match completed on one day, as Parquet
livetennisapi-export matches --status completed --date 2026-11-08 -o results.parquet
# The same day as JSON Lines, rows unchanged
livetennisapi-export matches --status completed --date 2026-11-08 -o results.jsonl
# Full profiles for a list of player ids
livetennisapi-export player 101 202 303 -o players.csv
# The current ATP ranking table
livetennisapi-export rankings --system atp -o atp.csv
# Player search, written to stdout
livetennisapi-export players alcaraz -o - -f jsonl
livetennisapi-export DATASET --help lists every option for a dataset.
How does paging work?
The command asks for pages of 200 rows, the largest page the API allows. It moves
the offset forward by the rows it received and stops when meta.has_more is false.
You never page by hand.
- Each page is one request against your quota.
--max-rows Nstops after N rows and sends no further request. --page-sizetakes 1 to 200. Smaller pages mean more requests for the same rows.playeris not paged. It sends one request for each player id you list.
Quotas for each plan are on the authentication, quota and status page.
How do nested fields become columns?
JSON Lines needs no change, so each line is the object the API returned. CSV and Parquet need flat columns. The writer applies the same rules to both.
- A nested object becomes one column for each field, named with dots.
- A list becomes JSON text in one column.
- An empty value is an empty cell in CSV and a null in Parquet.
- A boolean is
trueorfalsein CSV.
Here is a match row cut down to a few fields. The values are made up.
{"id": 5001, "tournament": "Example Open",
"players": {"p1": {"name": "Test Player A", "ranking": 12}},
"score": {"sets": [2, 0], "games": [[6, 6], [3, 4]]}}
It becomes six columns.
| Column | Value in the CSV |
|---|---|
id | 5001 |
tournament | Example Open |
players.p1.name | Test Player A |
players.p1.ranking | 12 |
score.sets | [2, 0] |
score.games | [[6, 6], [3, 4]] |
The columns follow the fields the API sent, in the order they first appear. A field added to the API shows up as a new column with no upgrade. These are some of the columns each export has. Every name is a field the reference documents.
| Export | Some of its columns |
|---|---|
| fixtures, as CSV | id, event_date, start_time, player1_id, player1_name, player2_id, player2_name, tournament |
| matches, completed, as Parquet | players.p1.itf_ranking.rank_date, score.timestamp |
rankings, --system atp | effective_date, observed_at, player_id, player_name, points, previous_rank, rank, rank_movement |
A match file is wide, because each player object and the score object spread into their own columns. In Parquet a whole-number column that has an empty value is stored as a decimal number.
When is the file written?
Nothing is written until every page has arrived. A request that fails halfway leaves no half-written file. A file of the same name from an earlier run stays as it was.
- A refused API key writes no file. The command prints the refusal and exits with status 1.
- A missing key, an unknown file extension with no
-fand Parquet to stdout are caught before any request is sent. The command exits with status 2. - On success the command exits with status 0. When it writes a file, it prints the row count and the file name to stderr.
- A request that matches no rows still writes the file. The CSV is then empty, with no header line.
How do I read an export in pandas?
A dotted name is an ordinary column label in pandas.
import json
import pandas as pd
results = pd.read_parquet("results.parquet")
print(results[["tournament", "round", "players.p1.name", "players.p2.name", "winner"]])
# Lists are stored as JSON text. Decode a column when you need the values.
results["sets"] = results["score.sets"].map(json.loads, na_action="ignore")
fixtures = pd.read_csv("fixtures.csv", parse_dates=["event_date"])
print(fixtures.groupby("tournament")["id"].count())
How do I query an export in DuckDB?
DuckDB reads all three formats from the file name.
-- A dotted column needs double quotes in CSV and Parquet files.
SELECT tournament, "players.p1.name", "players.p2.name", winner
FROM 'results.parquet'
WHERE surface = 'hard';
-- Lists are JSON text. Cast one to read it as a list.
SELECT id, CAST("score.sets" AS INTEGER[]) AS sets
FROM 'results.parquet';
-- JSON Lines keeps the nesting, so the same field is a struct path.
SELECT tournament, players.p1.name, score.sets
FROM read_json_auto('results.jsonl');
SELECT rank, player_name, points
FROM 'atp.csv'
ORDER BY rank;
How do I read an export in R?
Base R reads the CSV. The arrow package reads the Parquet file.
fixtures <- read.csv("fixtures.csv")
table(fixtures$tournament)
library(arrow)
results <- read_parquet("results.parquet")
results[, c("tournament", "players.p1.name", "players.p2.name", "winner")]
# Lists are stored as JSON text.
library(jsonlite)
sets <- lapply(results$score.sets, function(x) if (is.na(x)) NULL else fromJSON(x))
Can I export from Python code?
The same package is a small Python API. fetch returns the rows, write
writes them and export does both.
from livetennisapi_export import export, fetch, to_dataframe, write
rows = fetch("fixtures", tour="atp") # a list of dicts
write(rows, "fixtures.jsonl")
export("matches", "results.csv", status="completed", from_="2026-11-01", to="2026-11-07")
df = to_dataframe(fetch("rankings", system="atp")) # the same flat columns as the CSV
from_ is the from parameter, renamed because from is a Python keyword.
fetch and export take api_key= or read LIVETENNISAPI_KEY.
to_dataframe needs the parquet extra.
Links
- livetennisapi-export on PyPI
- Source on GitHub
- Free API key
- Live scores, the
/matchesendpoint - Players, tournaments and rankings