Daniel Freitag - Daniel@football-data.org - Version 2.0 | June 31 | 2018
Changelog v1 to v2
- Most apparently I introduced paid tiers and added lots of competitions and more in-depth data like squads, scorers, assists and so on. This data is only available through v2.
-
I added several Subresources and even renamed some existing Resources, namely:
- a Season-Subresource to Competition to act like a catalogue for historical data.
- an Area-Resource to be able to categorize competitions by continent / country.
- a Person-(Sub)Resource to represent players, coaches, referees and so on... (thus: persons)
- I renamed the `Fixture`-Resource to `Match`-Resource.
- I renamed the `LeagueTable`-Subresource to `Standings`.
- A friend of mine redesigned the frontend (hope you like it :-))
- I removed the HAL-headers. They were convenient for browsing the API but however. Hardly anyone did make use of it.
- I removed the X-Response-Control header.
Vocabulary
Technical wording
Main Resources are main building blocks of the API and most likely also appear as entities in clients' applications. Subresources on the other hand generally don’t make sense without the Main Resource they are based on. You can also think of that a Main Resource actually is composed of Subresources. However often there are good arguments to justify one or the other design to be right. Trying to offer a flexible API you will see some "things" appear as both. Last but not least there are Filters to narrow down result sets. A Filter always describes an attribute and it’s value must be passed in an adequate format, which is declared by a Data Type. I usually describe these Data types using a loose regex-dialect.
Domain wording
A Competition represents a football league (e.g. Premiere League) or a tournament (e.g. FA-Cup) or a combination of both (e.g. Champions League, having playoffs, a group stage and knock-out rounds). All Competitions can be accessed via a particular id or a human readable code. A Competition consists of Seasons, that hold a number of scheduled games named Matches. A certain number of Teams participate in one particular Season. Since v2 there's even more Resources: Persons appear in squads and lineups as Players and Coaches or in matches as Referees. See all Resources listed on the left and click for example responses and available filters.
I omitted to implement particular Rounds or Matchdays as hierarchical elements by purpose. They are implemented as attributes of a Match, because I think this is more intuitive and separation can be achieved by using Filters.
Resource design
URI’s, Resources and Subresources are all written lower case. Enums use uppercase so a status is IN_PLAY and not in_play. All resources are only accessible using their plural, thus a resource by default responds with the list representation. Adding an id to the endpoint gives access to one particular resource.
Resources are represented as JSON-objects. In list representations you typically find a node 'count' that shows the number of results.
Requesting a Resource
For a first glimpse you can use your webbrowser to browse the API. To make the responses human-readable, install a browser-plugin (mentioned on the the landing page) that beautifies JSON output, or better yet use Postman. You can also use curl or Powershell to easily fetch resources, depending on your platform.
To implement the API there are lots of libraries and plugins that ease implementing RESTful APIs. I personally can recommend HTTP Requests Library by Bud Bird for Groovy/Java and Guzzle for PHP. But most probably you already got your favorite REST library at hand.
Also have a look at the open-sourced football-data.org API-libraries, which offer high level functions, that directly map to API resources. These are available in PHP, Ruby, Python, Golang and Perl6 (though not ported yet to v2).
The API follows the Query-string composition standard. So if you want to use Filters read more about query definition on wikipedia.
Please implement smart requests and always try to think about a good tradeoff between payload and the number of requests. Please don’t write loops to crawl resources from id 0 to id 1000. Don’t pull (thus: poll) resources too often, they usually do not change within a second.
Request Headers
Header-Name | Possible values | Description |
---|---|---|
X-Auth-Token |
[a-z1-9]+ |
Your authentication token |
Response Headers
Examine the underneath HTTP response headers to debug responses that do not look like you expected.
Header-Name | Example value | Description |
---|---|---|
X-API-Version |
v2 |
indicates the version you are using |
X-Authenticated-Client |
Jimbo Jones |
Shows the detected API-client or 'anonymous' |
X-RequestCounter-Reset |
23 |
Defines the seconds left to reset your request counter. |
X-Requests-Available-Minute |
28 |
Shows the remaining requests before being blocked. |
Resources
Competition
The list representation of this resource is the de-facto entry point to the API and returns all available competitions.
Calling one particular resource basically serves a catalogue to see what seasons are available for that competition. In most cases however, you will use the Subresources to retrieve data.
Competition.json
{
"id": 2003,
"area": {
"id": 2163,
"name": "Netherlands"
},
"name": "Eredivisie",
"code": null,
"plan": "TIER_ONE",
"currentSeason": {
"id": 4,
"startDate": "2017-08-11T19:00:00Z",
"endDate": "2018-05-20T16:45:00Z",
"currentMatchday": 34
},
"seasons": [
{
"id": 4,
"startDate": "2017-08-11T19:00:00Z",
"endDate": "2018-05-20T16:45:00Z",
"currentMatchday": 34
}
],
"lastUpdated": "2018-06-05T00:17:50Z"
}
Available filters:
-
competitions
Available Subresources:
-
Matches (shows all matches of that competition. Defaults to the current season; use ?season=YYYY to retrieve former seasons)
-
Teams (shows all teams of that competition. Defaults to the current season by default; use ?season=YYYY to retrieve former seasons))
-
Standings (shows latest tables (total, home, away) for the current season; not yet available for former seasons)
-
Scorers (shows all goal scorers by shot goals descending for the current season)
Match
The Match resource reflects a scheduled football game. A match typically belongs to a
competition and season and is played on a certain matchday.
In list representation, the Match Main Resource responds with all matches of the current day, whereas
the Match Subresource of a Competition responds with all matches of the active
season.
{
"head2head": {
"numberOfMatches": 1,
"totalGoals": 4,
"homeTeam": {
"wins": 1,
"draws": 0,
"losses": 0
},
"awayTeam": {
"wins": 0,
"draws": 0,
"losses": 1
}
},
"match": {
"id": 200282,
"competition": {
"id": 2001,
"name": "UEFA Champions League"
},
"season": {
"id": 2,
"startDate": "2017-06-27",
"endDate": "2018-05-26",
"currentMatchday": 6,
"availableStages": [
"1ST_QUALIFYING_ROUND",
"2ND_QUALIFYING_ROUND",
"3RD_QUALIFYING_ROUND",
"PLAY_OFF_ROUND",
"GROUP_STAGE",
"ROUND_OF_16",
"QUARTER_FINALS",
"SEMI_FINALS",
"FINAL"
]
},
"utcDate": "2018-05-26T18:45:00Z",
"status": "FINISHED",
"minute": null,
"attendance": 61561,
"venue": "NSK Olimpijs'kyj",
"matchday": null,
"stage": "FINAL",
"group": "Final",
"lastUpdated": "2018-06-22T10:00:12Z",
"homeTeam": {
"id": 86,
"name": "Real Madrid CF",
"coach": {
"id": 79,
"name": "Zinedine Zidane",
"countryOfBirth": null,
"nationality": null
},
"captain": {
"id": 3192,
"name": "Sergio Ramos",
"shirtNumber": 4
},
"lineup": [
{
"id": 43,
"name": "Luka Modrić",
"position": "Midfielder",
"shirtNumber": 10
},
{
"id": 44,
"name": "Cristiano Ronaldo",
"position": "Attacker",
"shirtNumber": 7
},
{
"id": 3192,
"name": "Sergio Ramos",
"position": "Defender",
"shirtNumber": 4
},
{
"id": 47,
"name": "Toni Kroos",
"position": "Midfielder",
"shirtNumber": 8
},
{
"id": 48,
"name": "Marcelo",
"position": "Defender",
"shirtNumber": 12
},
{
"id": 3194,
"name": "Dani Carvajal",
"position": "Defender",
"shirtNumber": 2
},
{
"id": 3360,
"name": "Raphaël Varane",
"position": "Defender",
"shirtNumber": 5
},
{
"id": 66,
"name": "Isco",
"position": "Midfielder",
"shirtNumber": 22
},
{
"id": 3231,
"name": "Casemiro",
"position": "Midfielder",
"shirtNumber": 14
},
{
"id": 49,
"name": "Karim Benzema",
"position": "Attacker",
"shirtNumber": 9
},
{
"id": 51,
"name": "Keylor Navas",
"position": "Goalkeeper",
"shirtNumber": 1
}
],
"bench": [
{
"id": 3876,
"name": "Gareth Bale",
"position": "Midfielder",
"shirtNumber": 11
},
{
"id": 45,
"name": "Lucas Vázquez",
"position": "Midfielder",
"shirtNumber": 17
},
{
"id": 65,
"name": "Mateo Kovačić",
"position": "Midfielder",
"shirtNumber": 23
},
{
"id": 67,
"name": "Kiko Casilla",
"position": "Goalkeeper",
"shirtNumber": 13
},
{
"id": 50,
"name": "Nacho",
"position": "Defender",
"shirtNumber": 6
},
{
"id": 52,
"name": "Marco Asensio",
"position": "Midfielder",
"shirtNumber": 20
},
{
"id": 71,
"name": "Theo Hernández",
"position": "Defender",
"shirtNumber": 15
}
]
},
"awayTeam": {
"id": 64,
"name": "Liverpool FC",
"coach": {
"id": 9344,
"name": "Jürgen Klopp",
"countryOfBirth": "Germany",
"nationality": "Germany"
},
"captain": {
"id": 3320,
"name": "Jordan Henderson",
"shirtNumber": 14
},
"lineup": [
{
"id": 7861,
"name": "James Milner",
"position": "Defender",
"shirtNumber": 7
},
{
"id": 3320,
"name": "Jordan Henderson",
"position": "Midfielder",
"shirtNumber": 14
},
{
"id": 7859,
"name": "Dejan Lovren",
"position": "Defender",
"shirtNumber": 6
},
{
"id": 7868,
"name": "Andrew Robertson",
"position": "Defender",
"shirtNumber": 26
},
{
"id": 7871,
"name": "Georginio Wijnaldum",
"position": "Midfielder",
"shirtNumber": 5
},
{
"id": 3754,
"name": "Mohamed Salah",
"position": "Attacker",
"shirtNumber": 11
},
{
"id": 7856,
"name": "Loris Karius",
"position": "Goalkeeper",
"shirtNumber": 1
},
{
"id": 3233,
"name": "Roberto Firmino",
"position": "Midfielder",
"shirtNumber": 9
},
{
"id": 3626,
"name": "Sadio Mané",
"position": "Midfielder",
"shirtNumber": 19
},
{
"id": 7869,
"name": "Virgil van Dijk",
"position": "Defender",
"shirtNumber": 4
},
{
"id": 7867,
"name": "Trent Alexander-Arnold",
"position": "Defender",
"shirtNumber": 66
}
],
"bench": [
{
"id": 3640,
"name": "Simon Mignolet",
"position": "Goalkeeper",
"shirtNumber": 22
},
{
"id": 3318,
"name": "Adam Lallana",
"position": "Midfielder",
"shirtNumber": 20
},
{
"id": 7863,
"name": "Nathaniel Clyne",
"position": "Defender",
"shirtNumber": 2
},
{
"id": 3183,
"name": "Emre Can",
"position": "Midfielder",
"shirtNumber": 23
},
{
"id": 7860,
"name": "Alberto Moreno",
"position": "Defender",
"shirtNumber": 18
},
{
"id": 7866,
"name": "Ragnar Klavan",
"position": "Defender",
"shirtNumber": 17
},
{
"id": 7878,
"name": "Dominic Solanke",
"position": "Attacker",
"shirtNumber": 29
}
]
},
"score": {
"winner": "HOME_TEAM",
"duration": "REGULAR",
"fullTime": {
"homeTeam": 3,
"awayTeam": 1
},
"halfTime": {
"homeTeam": 0,
"awayTeam": 0
},
"extraTime": {
"homeTeam": null,
"awayTeam": null
},
"penalties": {
"homeTeam": null,
"awayTeam": null
}
},
"goals": [
{
"minute": 51,
"extraTime": null,
"type": "REGULAR",
"team": {
"id": 86,
"name": "Real Madrid CF"
},
"scorer": {
"id": 49,
"name": "Karim Benzema"
},
"assist": null
},
{
"minute": 55,
"extraTime": null,
"type": "REGULAR",
"team": {
"id": 64,
"name": "Liverpool FC"
},
"scorer": {
"id": 3626,
"name": "Sadio Mané"
},
"assist": {
"id": 7859,
"name": "Dejan Lovren"
}
},
{
"minute": 64,
"extraTime": null,
"type": "REGULAR",
"team": {
"id": 86,
"name": "Real Madrid CF"
},
"scorer": {
"id": 3876,
"name": "Gareth Bale"
},
"assist": {
"id": 48,
"name": "Marcelo"
}
},
{
"minute": 83,
"extraTime": null,
"type": "REGULAR",
"team": {
"id": 86,
"name": "Real Madrid CF"
},
"scorer": {
"id": 3876,
"name": "Gareth Bale"
},
"assist": {
"id": 48,
"name": "Marcelo"
}
}
],
"bookings": [
{
"minute": 82,
"team": {
"id": 64,
"name": "Liverpool FC"
},
"player": {
"id": 3626,
"name": "Sadio Mané"
},
"card": "YELLOW_CARD"
}
],
"substitutions": [
{
"minute": 30,
"team": {
"id": 64,
"name": "Liverpool FC"
},
"playerOut": {
"id": 3754,
"name": "Mohamed Salah"
},
"playerIn": {
"id": 3318,
"name": "Adam Lallana"
}
},
{
"minute": 37,
"team": {
"id": 86,
"name": "Real Madrid CF"
},
"playerOut": {
"id": 3194,
"name": "Dani Carvajal"
},
"playerIn": {
"id": 50,
"name": "Nacho"
}
},
{
"minute": 61,
"team": {
"id": 86,
"name": "Real Madrid CF"
},
"playerOut": {
"id": 66,
"name": "Isco"
},
"playerIn": {
"id": 3876,
"name": "Gareth Bale"
}
},
{
"minute": 83,
"team": {
"id": 64,
"name": "Liverpool FC"
},
"playerOut": {
"id": 7861,
"name": "James Milner"
},
"playerIn": {
"id": 3183,
"name": "Emre Can"
}
},
{
"minute": 89,
"team": {
"id": 86,
"name": "Real Madrid CF"
},
"playerOut": {
"id": 49,
"name": "Karim Benzema"
},
"playerIn": {
"id": 52,
"name": "Marco Asensio"
}
}
],
"referees": [
{
"id": 9371,
"name": "Milorad Mažić",
"nationality": null
},
{
"id": 9372,
"name": "Milovan Ristić",
"nationality": null
},
{
"id": 9373,
"name": "Dalibor Djurdjević",
"nationality": null
},
{
"id": 9374,
"name": "Clément Turpin",
"nationality": null
},
{
"id": 9375,
"name": "Nenad Djokic",
"nationality": null
},
{
"id": 9376,
"name": "Danilo Grujić",
"nationality": null
},
{
"id": 56231,
"name": "Dalibor Đurđević",
"nationality": null
}
]
}
}
Available filters:
-
competitions
-
status
-
stage
-
group
-
dateFrom + dateTo
Last but not least see the underneath state diagram for possible values of the STATUS field.
Figure 1. Possible values of the STATUS field.
Team
The Team resource gives access to all team details including squad and staff.
{
"id": 18,
"area": {
"id": 2088,
"name": "Germany"
},
"name": "Borussia Mönchengladbach",
"shortName": "M'gladbach",
"tla": "BMG",
"address": "Hennes-Weisweiler-Allee 1 Mönchengladbach 41179",
"phone": "+49 (02161) 92930",
"website": "http://www.borussia.de",
"email": "info@borussia.de",
"founded": 1900,
"clubColors": "Black / White / Green",
"venue": null,
"squad": [
{
"id": 3176,
"name": "Matthias Ginter",
"position": "Defender",
"dateOfBirth": "1994-01-03T00:00:00Z",
"countryOfBirth": "Germany",
"nationality": "Germany",
"role": "PLAYER"
},
{ ... one more player ... },
{ ... one more player ... },
{ ... even more players ... }
],
"lastUpdated": "2018-05-31T12:49:47Z"
}
Standings
{
"season": {
"id": 4,
"startDate": "2017-08-11T19:00:00Z",
"endDate": "2018-05-20T16:45:00Z",
"currentMatchday": 34
},
"filters": {},
"standings": [
{
"stage": "REGULAR_SEASON",
"type": "TOTAL",
"group": null,
"table": [
{
"position": 1,
"team": {
"id": 674,
"name": "PSV",
"crestURI": null
},
"playedGames": 34,
"won": 26,
"draw": 5,
"lost": 3,
"points": 83,
"goalsFor": 87,
"goalsAgainst": 39,
"goalDifference": 48
},
{
"position": 2,
"team": {
"id": 678,
"name": "AFC Ajax",
"crestURI": null
},
"playedGames": 34,
"won": 25,
"draw": 4,
"lost": 5,
"points": 79,
"goalsFor": 89,
"goalsAgainst": 33,
"goalDifference": 56
},
{
"position": 3,
"team": {
"id": 682,
"name": "AZ",
"crestURI": null
},
"playedGames": 34,
"won": 22,
"draw": 5,
"lost": 7,
"points": 71,
"goalsFor": 72,
"goalsAgainst": 38,
"goalDifference": 34
},
{ ... another rank ... },
]
},
{
"stage": "REGULAR_SEASON",
"type": "HOME",
"group": null,
"table": [
{
"position": 1,
"team": {
"id": 674,
"name": "PSV",
"crestURI": null
},
"playedGames": 17,
"won": 15,
"draw": 2,
"lost": 0,
"points": 47,
"goalsFor": 44,
"goalsAgainst": 9,
"goalDifference": 35
},
{ ... another rank ... }
]
},
{
"stage": "REGULAR_SEASON",
"type": "AWAY",
"group": null,
"table": [
{
"position": 1,
"team": {
"id": 678,
"name": "AFC Ajax",
"crestURI": null
},
"playedGames": 17,
"won": 11,
"draw": 3,
"lost": 3,
"points": 36,
"goalsFor": 44,
"goalsAgainst": 21,
"goalDifference": 23
},
{ ... another rank ... }
]
}
]
}
Available filters: None
Player
{
"id": 44,
"name": "Cristiano Ronaldo",
"firstName": "Cristiano Ronaldo",
"lastName": null,
"dateOfBirth": "1985-02-05",
"countryOfBirth": "Portugal",
"nationality": "Portugal",
"position": "Attacker",
"lastUpdated": "2018-08-09T05:07:03Z"
}
Available filters:
-
-
Available Subresources:
-
Matches (shows all matches of that player in all active competitions)
Filters
Filter | Possible value(s) | Description |
---|---|---|
id |
Integer /[0-9]+/ |
The (unique) id of a resource. |
matchday |
Integer /[1-4]*[0-9]*/ |
For completed seasons the last matchday is taken. For the match resource, it’s unset. |
status |
Enum: [SCHEDULED | LIVE | IN_PLAY | PAUSED | FINISHED | POSTPONED | SUSPENDED | CANCELLED] |
Define the status of the matches to be returned. |
venue |
Enum: [HOME|AWAY] |
Define the venue of the matches to be returned. |
dateFrom |
\d\d\d\d-\d\d\-\d\d |
The start date of the resources to be returned, e.g. 2021-08-01 |
dateTo |
\d\d\d\d-\d\d\-\d\d |
The end date of the resources to be returned, e.g. 2021-08-08 |
stage |
String /[A-Z]+/ |
Check the season node for available stages of a particular competition/season. |
season |
\d\d\d\d-\d\d\-\d\d |
Takes the (starting) year of a season as argument, e.g. 2021 |
plan |
The backend representation of the pricing plans, e.g. TIER_ONE |
|
limit |
Limit your result set, e.g. 15 |
API Design
Request-Throttling
To protect the API from unnecessary load it is rate limited. Non-authenticated clients are allowed for 100
requests per 24 hours and can only access the area and competition list resources.
Registered clients are allowed for 10 requests/minute in the free plan, 30 requests/minute in Standard plan
and 60 requests/minute in all plans above. If the limit is not enough for your use-case, it's possible to increase it further.
Please drop me a line.
Attributes and values
The API embraces null as a valid value, meaning all attributes can be null. The most obvious examples
are values that are not known yet (the score of a game before it has ended, for instance).
Empty lists are valid as well. The important point here is, that within one resource, attributes are there or they aren't.
They will never be there only sometimes, but in other cases not.
Notice however, on the contrary that I often use different resource representations, if these are
embedded into list resources. I don't think every information is useful in every resource (representation),
which is why I often limit embedded objects to a subset of their attributes.
I know this is not ideal for strongly typed languages, but I also know there are ways around that and I
think the advantages outbalance here.
Season and matchday
The API always defaults to now, which basically is reflected by using an active season and a current matchday to pre-filter responses.
Active season
An active season is determined by it's start and end-date. In case the start date is no more than 30 days in the future and the end date is not more than 30 days in the past, a season is considered to be active. That means that if you e.g. request the matches subresource of a team, it will include all upcoming matches of a new season 30 days before it actually starts (premise is, that all matches are scheduled at this point). All matches of that season will vanish as soon as the last match is 30 days in the past.
Current matchday
The current matchday is determined by the following algorithm: from now (as of the time it is run every 3 hours), take the last and the next match of the season.If their matchday is equal set the matchday to that matchday. If the gap between now and the next match is less that 36 hours or the gap between the last game and now is more than 60 hours, set the matchday to the matchday of the next game.
CORS handling
CORS (Cross-Origin-Resource-Sharing) is a mechanism that allows browsers to not load malicious code from a different servers than the original page was served from. There is an excellent article on SpringSource explaining CORS,^ so there’s no need to describe that further at this point. However, if you implement requests directly from Javascript, you need to add your X-Auth-Token correctly so the API gives you permission to do so. The basic workflow is as follows:
Before your Ajax request takes place your browser will automatically fire something like that:
OPTIONS http://api.football-data.org/v1/competitions/355/leagueTable
So that is your intentionally implemented request but not fired with GET but with the OPTIONS method. The API will respond with
204 No Content
response indicating there is no message body but also return the headers that are allowed. It reads as follows:
Access-Control-Allow-Methods "GET";
Access-Control-Allow-Origin "*";
Access-Control-Allow-Headers "x-auth-token, x-response-control";
Content-Length 0;
Content-Type text/plain;
Your browser now interprets this as "I am allowed to make that intended request" and fires the same request again with the GET method resulting in your desired response.
Errors
If something goes wrong you will likely face one of the following HTTP error codes. If the error is caused by the client the API will try to give you a hint with a small JSON encoded error message that looks like this:
{"error": "Parameter 'id' is expected to be an integer in a specific range."}
HTTP error codes returned
400 Bad Request | Your request was malformed. Most likely the value of a Filter was not set according to the Data Type that is expected. |
403 Restricted Resource |
You tried to access a resource that exists, but is not available to you. This can be due to the
following reasons:
|
404 Not Found | You tried to access a resource that doesn't exist |
429 Too Many Requests | You exceeded your API request quota. See Request-Throttling for more information. |
Appendix
Table of League-Codes
League-Code | Country | League |
BL1 | Germany | 1. Bundesliga |
BL2 | Germany | 2. Bundesliga |
BL3 | Germany | 3. Bundesliga |
DFB | Germany | Dfb-Cup |
PL | England | Premiere League |
EL1 | England | League One |
ELC | England | Championship |
FAC | England | FA-Cup |
SA | Italy | Serie A |
SB | Italy | Serie B |
PD | Spain | Primera Division |
SD | Spain | Segunda Division |
CDR | Spain | Copa del Rey |
FL1 | France | Ligue 1 |
FL2 | France | Ligue 2 |
DED | Netherlands | Eredivisie |
PPL | Portugal | Primeira Liga |
GSL | Greece | Super League |
CL | Europe | Champions-League |
EL | Europe | UEFA-Cup |
EC | Europe | European-Cup of Nations |
WC | World | World-Cup |