openfootball

SKILL.md

openfootball / football.json API

openfootball is a free, open, public domain collection of football (soccer) data. The football.json repository provides pre-built JSON files for major leagues and tournaments worldwide. No API key or authentication is required.


Data Sources

There are two ways to access the data:

1. Raw GitHub URLs (Primary)

https://raw.githubusercontent.com/openfootball/football.json/master/{season}/{league}.json

2. GitHub Pages Mirror

https://openfootball.github.io/{country}/{season}/{league-name}.json

Recommendation: Use the raw GitHub URLs for the football.json repo — they use a simple, consistent naming convention and are the most reliable.


URL Structure

https://raw.githubusercontent.com/openfootball/football.json/master/{season}/{code}.json
Component Description Examples
{season} Season directory — cross-year or calendar year 2024-25, 2023-24, 2025, 2019
{code} League code in {country}.{division} format en.1, de.1, es.1, it.1, fr.1

Available Leagues

England

Code League Tier
en.1 English Premier League 1st division
en.2 English Championship 2nd division
en.3 English League One 3rd division
en.4 English League Two 4th division

Germany

Code League Tier
de.1 Deutsche Bundesliga 1st division
de.2 2. Bundesliga 2nd division
de.3 3. Liga 3rd division

Spain

Code League Tier
es.1 Primera División (La Liga) 1st division
es.2 Segunda División 2nd division

Italy

Code League Tier
it.1 Serie A 1st division
it.2 Serie B 2nd division

France

Code League Tier
fr.1 Ligue 1 1st division
fr.2 Ligue 2 2nd division

Note: Not all leagues are available for all seasons. The football.json repo is continuously updated — check the repository for the full list of available files.


Available Seasons

Season directories in the football.json repo go back to 2010-11. European leagues use cross-year format (2024-25), while some calendar-year leagues use single-year format (2025).

Format Usage Examples
YYYY-YY European club seasons (Aug–May) 2024-25, 2023-24, 2015-16
YYYY Calendar-year competitions 2025, 2020, 2019

Known season directories: 2010-11, 2011-12, 2012-13, 2013-14, 2014-15, 2015-16, 2016-17, 2017-18, 2018-19, 2019-20, 2020-21, 2021-22, 2022-23, 2023-24, 2024-25, 2025-26, 2019, 2020, 2025.


JSON Response Format

All files follow the same JSON schema:

{
  "name": "English Premier League 2024/25",
  "matches": [
    {
      "round": "Matchday 1",
      "date": "2024-08-16",
      "time": "20:00",
      "team1": "Manchester United FC",
      "team2": "Fulham FC",
      "score": {
        "ht": [0, 0],
        "ft": [1, 0]
      }
    }
  ]
}

Top-Level Fields

Field Type Description
name string Human-readable league name and season
matches array Array of match objects

Match Object Fields

Field Type Required Description
round string Yes Round/matchday name (e.g., "Matchday 1", "Round of 16")
date string Yes Match date in YYYY-MM-DD format
time string No Kick-off time in HH:MM format (24-hour, local time)
team1 string Yes Home team name
team2 string Yes Away team name
score object No Score object (absent for unplayed future matches)
status string No Special status (e.g., "awarded" for administratively decided results)

Score Object Fields

Field Type Description
ft [int, int] Full-time score [home, away]
ht [int, int] Half-time score [home, away] (may be absent for some matches)

Note: Some matches only have ft (full-time) without ht (half-time). Always check for the presence of ht before accessing it.


Common Patterns

Fetch a League Season (curl)

curl -s "https://raw.githubusercontent.com/openfootball/football.json/master/2024-25/en.1.json" | jq .

Fetch and Parse Match Data (Python)

import requests

url = "https://raw.githubusercontent.com/openfootball/football.json/master/2024-25/en.1.json"
data = requests.get(url).json()

print(f"League: {data['name']}")
print(f"Total matches: {len(data['matches'])}")

for match in data["matches"][:10]:
    ft = match.get("score", {}).get("ft")
    if ft:
        print(f"  {match['date']}  {match['team1']} {ft[0]}-{ft[1]} {match['team2']}")
    else:
        print(f"  {match['date']}  {match['team1']} vs {match['team2']} (no score)")

Build a League Table from Results (Python)

import requests
from collections import defaultdict

url = "https://raw.githubusercontent.com/openfootball/football.json/master/2024-25/en.1.json"
data = requests.get(url).json()

table = defaultdict(lambda: {"played": 0, "won": 0, "drawn": 0, "lost": 0,
                              "gf": 0, "ga": 0, "points": 0})

for match in data["matches"]:
    score = match.get("score", {}).get("ft")
    if not score:
        continue

    t1, t2 = match["team1"], match["team2"]
    g1, g2 = score

    for team, gf, ga in [(t1, g1, g2), (t2, g2, g1)]:
        table[team]["played"] += 1
        table[team]["gf"] += gf
        table[team]["ga"] += ga
        if gf > ga:
            table[team]["won"] += 1
            table[team]["points"] += 3
        elif gf == ga:
            table[team]["drawn"] += 1
            table[team]["points"] += 1
        else:
            table[team]["lost"] += 1

# Sort by points, then goal difference
sorted_table = sorted(table.items(),
                       key=lambda x: (x[1]["points"], x[1]["gf"] - x[1]["ga"]),
                       reverse=True)

print(f"{'Team':<35} {'P':>3} {'W':>3} {'D':>3} {'L':>3} {'GF':>4} {'GA':>4} {'GD':>4} {'Pts':>4}")
print("-" * 70)
for i, (team, stats) in enumerate(sorted_table, 1):
    gd = stats["gf"] - stats["ga"]
    print(f"{i:>2}. {team:<32} {stats['played']:>3} {stats['won']:>3} "
          f"{stats['drawn']:>3} {stats['lost']:>3} {stats['gf']:>4} "
          f"{stats['ga']:>4} {gd:>+4} {stats['points']:>4}")

Filter Matches by Team (Python)

import requests

url = "https://raw.githubusercontent.com/openfootball/football.json/master/2024-25/en.1.json"
data = requests.get(url).json()

team = "Arsenal FC"
matches = [m for m in data["matches"]
           if team in (m["team1"], m["team2"]) and m.get("score", {}).get("ft")]

for m in matches:
    ft = m["score"]["ft"]
    opponent = m["team2"] if m["team1"] == team else m["team1"]
    venue = "H" if m["team1"] == team else "A"
    my_goals = ft[0] if m["team1"] == team else ft[1]
    opp_goals = ft[1] if m["team1"] == team else ft[0]
    result = "W" if my_goals > opp_goals else ("D" if my_goals == opp_goals else "L")
    print(f"  {m['date']} ({venue}) {result} {my_goals}-{opp_goals} vs {opponent}")

Fetch Multiple Leagues (Python)

import requests

leagues = {
    "Premier League": "en.1",
    "Bundesliga": "de.1",
    "La Liga": "es.1",
    "Serie A": "it.1",
    "Ligue 1": "fr.1",
}

season = "2024-25"
base = "https://raw.githubusercontent.com/openfootball/football.json/master"

for name, code in leagues.items():
    url = f"{base}/{season}/{code}.json"
    resp = requests.get(url)
    if resp.status_code == 200:
        data = resp.json()
        total = len(data["matches"])
        played = sum(1 for m in data["matches"] if m.get("score", {}).get("ft"))
        print(f"{name}: {played}/{total} matches played")
    else:
        print(f"{name}: not available for {season}")

Fetch and Parse (Node.js)

const url = "https://raw.githubusercontent.com/openfootball/football.json/master/2024-25/en.1.json";

const res = await fetch(url);
const data = await res.json();

console.log(`League: ${data.name}`);
console.log(`Matches: ${data.matches.length}`);

data.matches.slice(0, 10).forEach((m) => {
  const ft = m.score?.ft;
  if (ft) {
    console.log(`  ${m.date}  ${m.team1} ${ft[0]}-${ft[1]} ${m.team2}`);
  }
});

Fetch and Parse (bash + jq)

# Get all results for a specific team
curl -s "https://raw.githubusercontent.com/openfootball/football.json/master/2024-25/en.1.json" \
  | jq -r '.matches[]
    | select(.team1 == "Liverpool FC" or .team2 == "Liverpool FC")
    | select(.score.ft)
    | "\(.date) \(.team1) \(.score.ft[0])-\(.score.ft[1]) \(.team2)"'

Compare Head-to-Head Results (Python)

import requests

url = "https://raw.githubusercontent.com/openfootball/football.json/master/2024-25/en.1.json"
data = requests.get(url).json()

team_a = "Arsenal FC"
team_b = "Liverpool FC"

h2h = [m for m in data["matches"]
       if {m["team1"], m["team2"]} == {team_a, team_b}
       and m.get("score", {}).get("ft")]

for m in h2h:
    ft = m["score"]["ft"]
    ht = m["score"].get("ht", ["?", "?"])
    print(f"{m['date']}: {m['team1']} {ft[0]}-{ft[1]} {m['team2']} (HT: {ht[0]}-{ht[1]})")

Aggregate Stats Across Seasons (Python)

import requests

base = "https://raw.githubusercontent.com/openfootball/football.json/master"
seasons = ["2022-23", "2023-24", "2024-25"]
team = "Manchester City FC"
all_results = {"W": 0, "D": 0, "L": 0, "GF": 0, "GA": 0}

for season in seasons:
    resp = requests.get(f"{base}/{season}/en.1.json")
    if resp.status_code != 200:
        continue
    data = resp.json()
    for m in data["matches"]:
        ft = m.get("score", {}).get("ft")
        if not ft:
            continue
        if m["team1"] == team:
            gf, ga = ft
        elif m["team2"] == team:
            ga, gf = ft
        else:
            continue
        all_results["GF"] += gf
        all_results["GA"] += ga
        if gf > ga:
            all_results["W"] += 1
        elif gf == ga:
            all_results["D"] += 1
        else:
            all_results["L"] += 1

print(f"{team} across {', '.join(seasons)}:")
print(f"  W{all_results['W']} D{all_results['D']} L{all_results['L']}")
print(f"  Goals: {all_results['GF']} scored, {all_results['GA']} conceded")

Find High-Scoring Matches (bash + jq)

# Find all matches with 5+ total goals in the Premier League 2024/25
curl -s "https://raw.githubusercontent.com/openfootball/football.json/master/2024-25/en.1.json" \
  | jq -r '.matches[]
    | select(.score.ft)
    | select((.score.ft[0] + .score.ft[1]) >= 5)
    | "\(.date) \(.team1) \(.score.ft[0])-\(.score.ft[1]) \(.team2) (Total: \(.score.ft[0] + .score.ft[1]))"'

Get Results for a Specific Matchday (Python)

import requests

url = "https://raw.githubusercontent.com/openfootball/football.json/master/2024-25/en.1.json"
data = requests.get(url).json()

matchday = "Matchday 38"
matches = [m for m in data["matches"] if m["round"] == matchday]

print(f"--- {matchday} ---")
for m in matches:
    ft = m.get("score", {}).get("ft")
    if ft:
        print(f"  {m['date']} {m.get('time', '')}  {m['team1']} {ft[0]}-{ft[1]} {m['team2']}")
    else:
        print(f"  {m['date']} {m.get('time', '')}  {m['team1']} vs {m['team2']}")

Other Repositories (Source Data)

The football.json files are auto-generated from plain-text Football.TXT source files in country-specific repos:

Repo Content GitHub URL
england EPL, Championship, League One, League Two openfootball/england
deutschland Bundesliga, 2. Bundesliga, 3. Liga, DFB Pokal openfootball/deutschland
espana La Liga, Segunda División openfootball/espana
italy Serie A, Serie B, Coppa Italia openfootball/italy
france Ligue 1, Ligue 2 openfootball/europe (in /europe)
worldcup FIFA World Cup (2022, 2018, 2014, etc.) openfootball/worldcup
euro Euro 2024, 2020, 2016, etc. openfootball/euro
champions-league UCL & Europa League openfootball/champions-league
clubs Club & stadium metadata openfootball/clubs
world Leagues from N. America, Asia, Africa, Australia openfootball/world

Country repos also have their own GitHub Pages JSON mirrors. For example:

https://openfootball.github.io/england/2024-25/1-premierleague.json

You can convert Football.TXT source files to JSON yourself using the fbtxt2json CLI tool:

# Convert a single league file
fbtxt2json england/2025-26/1-premierleague.txt -o en.1.json

# Convert an entire country repo at once
fbtxt2json . -o ./_site

Rate Limits

GitHub does not publish specific rate limits for raw content, but general guidelines:

Guideline Recommendation
Polling interval ≥ 60 seconds between requests for the same file
Concurrent requests Keep reasonable (< 20 concurrent)
Caching Cache responses locally — data changes infrequently
Unauthenticated GitHub API 60 requests/hour per IP (only applies to API endpoints, not raw content)

Tip: Since match data doesn't change after a game is completed, you can aggressively cache historical seasons. Only poll the current season for updates.


Error Handling

HTTP Status Meaning Action
200 Success Parse the JSON
404 File not found Check the season, league code, or URL spelling
429 Rate limited Back off and retry after a delay
5xx Server error Retry with exponential backoff

Tips

  • No auth needed — all data is fully public. Start fetching immediately.
  • Check for score before accessing — future/unplayed matches won't have a score field.
  • Check for ht separately — some matches have ft but no ht data.
  • Team names include suffixes — e.g., "Arsenal FC", "Manchester United FC", "Borussia Dortmund". Use exact string matching.
  • Team names vary by league — German teams use German names ("FC Bayern München"), English teams use English names ("Arsenal FC").
  • The status field is rare — it appears on administratively decided matches (e.g., "awarded").
  • Cache aggressively — completed seasons never change. Only the current season gets updates.
  • Build tables from the data — the dataset provides raw match results; you compute standings, form, H2H, etc.
  • Cross-reference leagues — fetch multiple league files to compare across countries.
  • Public domain (CC0) — use the data however you want with no restrictions whatsoever.
  • Don't edit the JSON directly — if contributing, edit the Football.TXT source files in the country repos; JSON is auto-generated.
  • Time field is local — the time value represents the local kick-off time for the match venue.
  • Scores are [home, away]score.ft[0] is always the team1 (home) goals, score.ft[1] is always the team2 (away) goals.

Changelog

  • 0.1.0 — Initial release with league data access, JSON schema documentation, and common usage patterns.
Weekly Installs
42
GitHub Stars
2
First Seen
Feb 22, 2026
Installed on
github-copilot40
codex40
kimi-cli40
gemini-cli40
cursor40
amp40