The official Python client for footballsoccerapi.com — 680,000 matches back to 2012 across 158 countries, with kickoff prices, team statistics, lineups and live scores.
No dependencies. Standard library only. It will still import in five years.
There are two versions in here. Start with the simple one.
One file, three functions, no classes. Copy fsapi_simple.py next to your script or notebook.
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"])That is the whole thing:
fsapi_get(path, **params) |
the data |
fsapi_call(path, **params) |
the data and the meta |
fsapi_walk(path, **params) |
every row, paging for you |
Use this in a notebook or a script. Use the package below for something that has to keep working.
Pass it directly, or leave it out and set FSAPI_KEY in the environment:
api = Client("fsa_live_...") # directly
api = Client() # reads FSAPI_KEYFSAPI_KEY=fsa_live_... python your-script.pyThe environment version is worth the extra step, especially in a notebook — a key written into a cell ends up in a screenshot, a repository or a shared file eventually, and then it has to be rotated.
A free key takes a minute and needs no card: footballsoccerapi.com/free-key. It reaches eleven endpoints on a one to seven day delay — enough to build something real before deciding whether to pay.
from footballsoccerapi import Client
api = Client()
res = api.matches(country="England", status="finished", limit=5)
for m in res["data"]:
print(f"{m['home_team_name']} {m['home_goals']}-{m['away_goals']} {m['away_team_name']}")Install by cloning, or from the repository directly:
pip install git+https://github.com/FootballSoccerAPI/footballsoccerapi-python.gitThe client does not import pandas and does not depend on it — but a response is plain dicts, so it goes into a DataFrame with no adapter:
import pandas as pd
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()A field we do not hold comes back as None rather than vanishing from the response, so a column is
always present and pandas reads it as NaN. Counting those is how you check coverage before
trusting an average — see examples/into_pandas.py.
The API never sends a bare status code, and neither does this.
from footballsoccerapi import PlanRequiredError, RateLimitError
try:
api.live()
except PlanRequiredError as e:
print(f"Needs {e.needs_plan}, you have {e.your_plan} — {e.upgrade_url}")
except RateLimitError as e:
print(f"Wait {e.retry_after}s")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. Pass retry_on_rate_limit=False to
handle it yourself.
A response that is not JSON raises TransportError saying so, rather than a confusing decode
error — that means something between you and the API answered, usually a proxy or a challenge page,
and knowing which layer failed saves an hour.
for match in api.walk_matches(league_id="lg_24T9Z0G", season=2024):
... # every match in the season, memory flatThe 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.
matches = api.matches_by_ids(["mt_0CXSZRJ", "mt_087EP2A", ...])Fifty ids in one request instead of fifty requests; longer lists are chunked rather than refused. Each id costs one call against your rate limit, the same as fetching them separately — what batching saves is round trips, not quota.
res = api.matches(season=2024)
res["data"] # the matches
res["meta"]["total"] # how many matched
res["meta"]["data_as_of"] # when the archive was last rebuiltA client that stripped the meta to hand you a bare list would be throwing away the part that stops a figure being quoted without its base.
export FSAPI_KEY=your_key
python examples/simple.py # the simple client, start to finish
python examples/quickstart.py # recent results, with prices
python examples/walk_a_season.py # walk a season with the cursor
python examples/into_pandas.py # into a DataFrame (needs pandas)Ids carry their type — mt_ for a match, lg_ for a competition, tm_ for a club, vn_ for a
ground. A club id passed where a competition belongs fails loudly instead of quietly returning the
wrong thing, and raw integers are refused.
680,916 matches from July 2012, across 928 competitions and 16,621 clubs. Kickoff prices on 53.7%, full-time results on 97.1%, team statistics on 132,441 matches, lineups and formations on 116,309.
Coverage varies by competition, and every competition and season publishes its own fill rate per field. That is the figure to build against rather than an archive-wide average — a field can be near complete in one competition and absent from another.
- Every endpoint
- OpenAPI spec and Postman collection
- The PHP client, if you are here by mistake
- Status and support
MIT.