Get Football Fixtures by Date in Python: Handle UTC and Local Match Days
A football calendar's “Thursday” depends on the viewer's timezone. A kickoff late on Wednesday in UTC can already be Thursday in Shanghai. Simply grouping a date-based API response under its request date can therefore produce the wrong local schedule.
This football fixtures by date API Python walkthrough uses get-matches-events-by-date, then converts kickoff timestamps with zoneinfo. It separates demonstrated response fields from coverage assumptions and avoids treating every 0–0 as a finished draw.
Endpoints and prerequisites
Get an authorized platform API key from your dashboard. Run examples deliberately from a trusted terminal or backend; never embed a production key in public client code. Query URLs contain the key, so redact URLs from logs, screenshots and shared links. No football-account cookie or session is required by these operations.
| Operation | Required input | Role |
|---|---|---|
get-search-all | search | Typed team and match suggestions |
get-matches-events-by-date | date | Daily fixtures and results |
get-match-event-detail-by-event-id | eventid | Teams, competition and kickoff context |
get-standing-all-by-league-id | leagueid | Overall league table |
get-team-detail-by-team-id | teamid | Club identity and stadium information |
The presentation's safe first example searches for Real Madrid. The fixture example uses the demonstrated date 20261008; neither guarantees the same records on a later request.
curl --get "https://api.opendata-api.com/api/project/football/get-search-all?key=YOUR_API_KEY" \
--data-urlencode "search=Real Madrid"
curl --get "https://api.opendata-api.com/api/project/football/get-matches-events-by-date?key=YOUR_API_KEY" \
--data-urlencode "date=20261008"
Use YYYYMMDD, not YYYY-MM-DD. There is no declared timezone parameter on this fixture operation.
Read status, IDs and match-state fields
Check HTTP status first, then the gateway's code/message/data: code=200 indicates gateway business success, whereas HTTP 200 alone does not. Inside data, inspect the football payload's status before reading response.matches. A missing or malformed array is not the same as an empty successful list.
The demonstrated match rows include:
idandleagueId, for event deduplication and competition joins.homeandaway, each withid,name,longNameandscore.status.utcTime, plusstarted,finishedandcancelledflags; inspect their nesting in the returned row.statusId,tournamentStageand millisecondtimeTS; finished examples also includedstatus.scoreStrandstatus.reason.
Use explicit booleans, not score values, to classify progress. Cancellation takes precedence. started=false and finished=false describe a scheduled snapshot; started=true and finished=false describe a started, unfinished snapshot, not necessarily uninterrupted play. Missing flags remain unknown. Retain cancellation or reason context where supplied.
Convert UTC kickoffs and cache deliberately
Use Python 3.9 or later with requests installed. zoneinfo uses IANA timezone data; install tzdata if your environment lacks it. Set OPENDATA_API_KEY privately to your platform key.
The cache below lasts five minutes as an application example, not a promised update interval. It caches only validated successful responses and keeps credentials out of cache keys. Requests occur only when you explicitly run the script.
import os
from datetime import date, datetime
from time import monotonic
from zoneinfo import ZoneInfo
import requests
BASE = "https://api.opendata-api.com/api/project/football"
KEY = os.environ["OPENDATA_API_KEY"]
CACHE_TTL = 300
cache = {}
def call(operation, **params):
cache_key = (operation, tuple(sorted(params.items())))
cached = cache.get(cache_key)
if cached and monotonic() - cached[0] < CACHE_TTL:
return cached[1]
try:
result = requests.get(
f"{BASE}/{operation}",
params={**params, "key": KEY}, timeout=30,
)
except requests.RequestException:
raise RuntimeError("Football request failed") from None
if not 200 <= result.status_code < 300:
raise RuntimeError(f"HTTP failure: {result.status_code}")
envelope = result.json()
if envelope.get("code") != 200:
raise RuntimeError("Gateway business failure; inspect redacted status")
payload = envelope.get("data")
if not isinstance(payload, dict) or payload.get("status") != "success":
raise RuntimeError("Football payload is not successful")
response = payload.get("response")
if not isinstance(response, dict):
raise RuntimeError("Unexpected football response structure")
cache[cache_key] = (monotonic(), response)
return response
def match_state(match):
status = match.get("status") or {}
# Read nested flags, retaining row-level flags if returned there.
flags = {name: status.get(name, match.get(name))
for name in ("cancelled", "started", "finished")}
if flags["cancelled"] is True:
return "cancelled"
if flags["finished"] is True:
return "finished"
if flags["finished"] is False:
if flags["started"] is True:
return "started, not finished"
if flags["started"] is False:
return "scheduled"
return "unknown"
local_day = date(2026, 10, 8)
viewer_zone = ZoneInfo("Asia/Shanghai")
source_dates = ["20261008"]
by_event_id = {}
for source_date in source_dates:
rows = call("get-matches-events-by-date", date=source_date)["matches"]
if not isinstance(rows, list):
raise RuntimeError("Expected response.matches to be an array")
for match in rows:
by_event_id[str(match["id"])] = match
for event_id, match in by_event_id.items():
utc_text = match["status"]["utcTime"]
kickoff = datetime.fromisoformat(utc_text.replace("Z", "+00:00"))
if kickoff.tzinfo is None:
raise ValueError("Kickoff lacks a timezone; review before grouping")
local_kickoff = kickoff.astimezone(viewer_zone)
if local_kickoff.date() != local_day:
continue
print({
"event_id": event_id,
"league_id": match["leagueId"],
"home_id": match["home"]["id"],
"away_id": match["away"]["id"],
"kickoff_local": local_kickoff.isoformat(),
"state": match_state(match),
})
The script fails visibly on unexpected structure instead of silently declaring “no fixtures.” In production, quarantine rows with unusable timestamps for review. Store retrieval timestamps with persisted snapshots and adapt refresh intervals to your quota and match lifecycle. Share identical reads across users, stop aggressive refreshing after completion, and use bounded backoff for transient failures. These are GET snapshots, not live push subscriptions or a latency guarantee.
Validate the source-date window
The observed 20261008 response included UTC kickoffs spanning October 7–8. This does not establish the source calendar timezone, a fixed UTC offset, or a timezone selector. A date request is a discovery window, not proof that every row belongs to that UTC date or the viewer's day.
Test dates near midnight against your target timezone. If needed, expand source_dates to adjacent dates such as 20261007, 20261008 and 20261009. The dictionary deduplicates event IDs, with the last fetched row retained; the subsequent local-date filter removes out-of-day matches. Validate that this window captures your intended day before relying on it. Adjacent reads cost additional requests and are not a completeness guarantee for every timezone or competition. Use named zones, such as Europe/London, rather than hardcoded offsets when daylight saving applies.
Optional enrichment and practical uses
After selecting a fixture, pass its actual id as eventid to match details. Read response.detail.leagueId for an optional overall-standings request. These are explicit follow-ups, not requests for every row. The detail sample did not include a final score; retain the fixture score and its state.
Overall standings expose response.standing rows with played, wins, draws, losses, scoresStr, goalConDiff, pts and idx. Preserve returned ordering and rank; do not invent tie-break rules. This supports supporter calendars, publisher match cards, results digests and identifier-linked research datasets, subject to actual coverage and permitted data use.
FAQ
How do I get fixtures for a specific date in Python?
Call get-matches-events-by-date with date=YYYYMMDD; read the validated response.matches array as above.
Why can one response contain two UTC dates? The observed window crossed UTC midnight. Its calendar timezone is not established; do not infer one from that observation.
How do I convert UTC to local time?
Parse the aware status.utcTime timestamp and call astimezone(ZoneInfo("Asia/Shanghai")), or another named user timezone.
How do I find a team ID?
Search with get-search-all; select a suggestion of type team and pass its ID as teamid to team details.
Where do event IDs for details or statistics come from?
Use fixture id or a validated match suggestion. Statistics responses require separate schema and availability checks.
How do I distinguish scheduled, started and finished games? Read explicit flags and cancellation context. A scheduled 0–0 is not a completed draw; missing flags mean unknown.
How do I build a Premier League table?
The demonstrated overall-table example used leagueid=47. Render returned points, results, goal difference and positions; verify scoring and deduction rules.
How do overall, home and away tables differ? Overall represents the full returned table; home and away are venue splits. Validate split schemas and never label split rank as overall rank.
Which matches include lineups, odds or detailed statistics? Availability is event- and competition-specific, not established by catalogue presence. Test target competitions and design unavailable-data states.
Does an empty search result mean failure?
No. General search demonstrated status="success" with response.suggestions=[]. Distinguish that from HTTP or business errors.
How should I avoid exhausting my quota? Cache validated reads, deduplicate adjacent dates, enrich only selected events and back off on failures. Check current pricing.
What should I compare between free and paid football APIs? Compare usable competition coverage, quotas, field availability, historical scope, update expectations and data-use rights—not endpoint counts alone.
Explore the Football Data API and its fixtures-by-date reference before choosing additional workflows.