Skip to content
Integration

Using a football API from Python

A dependable Python pattern for fetching football feeds with timeouts, status handling, retries and storage.

FootballAPI.net editorial team

Written and reviewed by the organisation

Published 30 Oct 2026Updated 1 Nov 20265 min read

FootballAPI.net — Using a football API from Python, illustrated by a floodlit stadium pitch

Python is a natural fit for football data work. It is popular for scheduled jobs, analysis notebooks and back-end services alike. The same client pattern serves all three: read the key from the environment, make a request with a timeout, check the status, validate the body and store it.

This guide includes a synchronous requests example for a standings snapshot, then explains scheduling, retries and transactional storage. Install requests in your server environment, set the API base URL and secret key there, and obtain a league ID from the competition directory. An async service can use the same boundaries with an asynchronous HTTP client.

Configuration

Read the key from an environment variable such as FOOTBALLAPI_KEY and the base URL from another. Raise a clear error at startup if either is missing. Do not hard-code keys in scripts or notebooks; notebooks in particular are often shared or committed with secrets inside them.

Create a session object and reuse it for all requests. A session keeps connections open between calls, which is faster and kinder to the server than opening a new connection each time.

Set the X-API-Key header and an Accept header for JSON on the session, so every request carries them.

  • Key and base URL from the environment
  • Reuse one session
  • Default headers on the session

Requests and timeouts

Always pass a timeout. Without one, a stalled connection can hang a job forever. A few seconds for connection and a little longer for reading is a sensible default. Build the URL from the customer path for the feed and pass query parameters as a dictionary so they are encoded correctly.

Check the status code before reading JSON. Treat 401 as a configuration error, 403 as missing access to that feed, 429 as a limit, and 5xx as temporary. Raise distinct exception types for each so the caller can react appropriately.

import os
from urllib.parse import urljoin
import requests

base = os.environ["FOOTBALLAPI_BASE_URL"]
key = os.environ["FOOTBALLAPI_KEY"]
league_id = os.environ["LEAGUE_ID"]  # Copy from the competition directory.
with requests.Session() as session:
    response = session.get(
        urljoin(base, "/api/v1/football/standings"),
        params={"leagueId": league_id},
        headers={"X-API-Key": key, "Accept": "application/json"},
        timeout=(5, 15),
    )
    response.raise_for_status()
    result = response.json()
    if not isinstance(result, dict) or not {"data", "meta"}.issubset(result):
        raise ValueError("Unexpected response envelope")
    # Validate the standings in result["data"] before a transactional upsert.

Retries

Use a retry helper with exponential backoff for network errors, timeouts, 429 and 5xx. Honour Retry-After. Cap the number of attempts and the total time. Do not retry 401 or 403; log them and alert, because a human needs to fix them.

If you use an adapter-level retry configuration, check that it respects Retry-After and does not retry non-idempotent methods. For this API all customer calls are GET requests, so retries are safe.

Validation and storage

Parse the JSON and validate the parts you use. A lightweight schema library or simple explicit checks are both fine. Store the validated data in your database keyed by supplied identifiers, and keep the raw response alongside it if you can, which makes debugging much easier.

Use upserts keyed on identifiers so repeated runs update rather than duplicate rows. Wrap each feed's writes in a transaction, so a failure halfway leaves the previous good state intact.

  • Validate the fields you use
  • Upsert by supplied identifiers
  • One transaction per feed refresh

Scheduling

Run jobs with your platform's scheduler, a cron entry or a task queue. Give each feed its own schedule according to how often it changes. Ensure only one instance of each job runs at a time, using a lock file, a database advisory lock or your queue's uniqueness features.

Log the start and end of each run, the number of records written and any errors. Those logs are your first stop when a page looks out of date.

Analysis work

Analysts often want to explore data in notebooks. Point notebooks at your stored data rather than calling the API repeatedly. That keeps request volume predictable and makes analysis reproducible, because everyone works from the same snapshot.

When comparing statistics, keep competition and season context beside every figure. Mixing levels produces misleading conclusions.

Testing

Record real responses as fixtures and test your parsing and storage against them. Use a mocking library to simulate each status code and network failure. Test that a failed refresh leaves the database unchanged.

Async services

If your service is asynchronous, use one shared HTTP client with explicit timeouts, status checks and bounded retries. A semaphore should limit concurrent tasks, while a separate paced queue controls requests per second. FootballAPI.net's account safety limit is 100 requests per second across all keys; do not mistake a concurrency setting of 100 for compliance with a per-second rate.

A worked example

A nightly job for league 1204 creates a session with the key and headers, requests standings and fixtures through a helper that applies timeouts, status checks and bounded retries, then validates the fields it uses. Inside a transaction it upserts teams, fixtures and table rows by supplied identifiers and records the fetch time. If any step raises, the transaction rolls back and yesterday's data stays live.

A second job runs on match days with a shorter schedule for the recent-and-upcoming window. Both jobs log counts and durations, and an alert fires if a feed has not refreshed successfully within its expected window.

Analysts query the same database from notebooks, so their charts match exactly what readers see on the site, and nobody spends allowance exploring ideas.

Common mistakes

Forgetting a timeout, committing a key inside a notebook, retrying 403 responses in a loop, and converting identifiers to integers are the four mistakes we see most. Each is easy to avoid once you know to look for it. Keep identifiers as strings, review notebooks before sharing, and let configuration errors fail loudly rather than retrying quietly.

Packaging the client

Wrap the helper in a small internal package with its own tests, so scripts, services and notebooks share one implementation. When you change retry rules or add a feed, every consumer benefits, and you avoid five slightly different clients drifting apart over a season.

Logging safely

Configure logging so request headers are never printed. Many debugging sessions end with a key pasted into a shared log; a small filter that redacts the header prevents it.

Key takeaways

  • Read the key from the environment and reuse one session with default headers.
  • Always set timeouts and retry only recoverable errors.
  • Validate, upsert by identifier and keep analysis on stored data.

Integration checklist

0/10 complete
Progress is saved in this browser only.

Frequently asked questions

Which library should I use?

The requests library is fine for synchronous jobs. Use an async client for async services. The rules are the same.

Can I call the API from a notebook?

You can, but prefer reading stored data in notebooks so request volume stays predictable and analysis is reproducible.

Should I retry on 403?

No. A 403 means your key lacks access to that feed. Contact support to request access.

How do I avoid duplicate rows?

Upsert by the supplied identifiers inside a transaction.

Sources and references

Use these implementation references alongside this guide. Current endpoint parameters and account settings take precedence over illustrative examples.