Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FootballSoccerAPI.com - Python SDK

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.


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.


Your API key

Pass it directly, or leave it out and set FSAPI_KEY in the environment:

api = Client("fsa_live_...")   # directly
api = Client()                 # reads FSAPI_KEY
FSAPI_KEY=fsa_live_... python your-script.py

The 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.


The package

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.git

Straight into pandas

The 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.

Errors that explain themselves

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.

Walking large sets

for match in api.walk_matches(league_id="lg_24T9Z0G", season=2024):
    ...   # every match in the season, 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.

Batching

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.

The meta comes back with the data

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 rebuilt

A 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.


Examples

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

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.

What is in the archive

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.

Documentation

Licence

MIT.

Releases

Packages

Contributors

Languages