The Python client
Standard library only, and it goes straight into pandas.
An official Python client is published at github.com/FootballSoccerAPI/footballsoccerapi-python. Standard library only — no requests, no httpx, nothing to keep in step with anything else.
That is deliberate. A data client that drags in a stack is a client that breaks when the stack moves, and the people using this already have enough of that.
There are two versions in it, and the smaller one is the better place to start.
The one-file version
Copy fsapi_simple.py next to your script or notebook. Three functions, no classes,
nothing to install.
from fsapi_simple import fsapi_get matches = fsapi_get("/v1/matches", country="England", status="finished", limit=5) for m in matches: print(m["home_team_name"], m["home_goals"], m["away_goals"], m["away_team_name"])
fsapi_get() hands back the data. fsapi_call() hands back the data and
the meta. fsapi_walk() pages through a whole set for you. That is all of it, and the
names match the PHP client exactly, so moving between them
is not a relearn.
Straight into pandas
The client does not import pandas and does not depend on it. It does not need to — a response is plain dictionaries, so it goes into a DataFrame with no adapter and no helper.
import pandas as pd from footballsoccerapi import Client api = Client() # reads FSAPI_KEY rows = list(api.walk_matches(league_id="lg_24T9Z0G", season=2024)) df = pd.DataFrame(rows) df["total_goals"] = df["home_goals"] + df["away_goals"] df.groupby("league_name")["total_goals"].mean()
Check coverage before trusting an average. A field we do not hold comes back as null
rather than vanishing, so the column is always there and pandas reads it as NaN.
Counting those tells you what an average is actually over — prices sit at 53.7% of the
archive and the share varies a lot by competition.
df["kickoff_book_pct"].notna().sum() # how many actually had a price
Your key
Pass it directly, or leave it out and let the client read FSAPI_KEY from the
environment:
api = Client("fsa_live_...") # directly api = Client() # reads FSAPI_KEY
FSAPI_KEY=fsa_live_... python your-script.py
The environment version matters more in Python than most places, because notebooks get shared. A key written into a cell ends up in a screenshot, a repository or a colleague’s copy eventually, and then it has to be rotated.
The package
Same data, more handled for you: typed exceptions, cursor walking, and batching that chunks a long list rather than refusing it.
pip install git+https://github.com/FootballSoccerAPI/footballsoccerapi-python.git
from footballsoccerapi import Client api = Client() res = api.matches(season=2024) res["data"] # the matches res["meta"]["total"] # how many matched res["meta"]["data_as_of"] # when the archive was rebuilt
The meta comes back with the data rather than being stripped, because it is where the API says how old the archive is and how many rows there were — the part that stops a figure being quoted without its base.
What the exceptions tell you
from footballsoccerapi import PlanRequiredError, RateLimitError try: api.live() except PlanRequiredError as e: print(f"Needs {e.needs_plan}, you have {e.your_plan}") except RateLimitError as e: time.sleep(e.retry_after)
Rate limits are retried once by default using the API’s own figure, because guessing a
backoff when the response tells you the answer is worse for both sides. A reply that is not JSON
raises TransportError saying so, which means something between you and the API
answered — a proxy, or a challenge page — and knowing which layer failed saves an
hour.
Walking a season
for match in api.walk_matches(league_id="lg_24T9Z0G", season=2024): ... # every match, memory flat
The cursor pages by sort position rather than offset, so the ten-thousandth page is as quick as the first and a match arriving mid-walk cannot shift the boundary and make you skip a row.
The examples
export FSAPI_KEY=your_key python examples/simple.py python examples/quickstart.py python examples/walk_a_season.py python examples/into_pandas.py
MIT licensed. If something is wrong or missing, an issue on the repository or a ticket both reach us.