Goalise Football API v3 is live. It is a new major version of the same football data — live scores, fixtures and results, league tables, squads, player statistics, lineups, match events, head-to-head records, transfers, injuries, highlights, betting odds and news — returned in a shape that gives a client far more in every answer. The base URL is https://api.goalise.com/api/v3, it holds 46 endpoints, and your existing access token works on it today.
v2 is frozen, not retired. Nothing about /api/v2 changes, nothing is switched off, and no date is set for one. If your integration is happy, it keeps working. v3 is there for when you want fewer requests and more data per request.
What changed in football API v3
One thing, and everything follows from it: v2 answered with foreign keys, v3 answers with objects.
In v2 a fixture row named its competition, its two clubs and its stadium as four bare integers. Drawing a matches screen meant one call for the fixtures and then a call per distinct id to find out what those integers meant. A page of 120 fixtures could name 240 clubs. That is the fan-out every client ended up building a cache around.
In v3 the entity is already there, with its name and its crest:
v2 — a fixture row
{
"id": 372452,
"league_id": 39,
"home_team_id": 42,
"away_team_id": 33,
"venue_id": 494,
"timestamp": 1757707200,
"status": { "short": "FT", "elapsed": 90 },
"score": { "home": 1, "away": 0 }
}
v3 — the same fixture
{
"id": 372452,
"league": { "id": 39, "name": "Premier League", "logo": "https://...", "type": "League" },
"home_team": { "id": 42, "name": "Arsenal", "logo": "https://..." },
"away_team": { "id": 33, "name": "Manchester United", "logo": "https://..." },
"venue": { "id": 494, "name": "Emirates Stadium", "city": "London" },
"timestamp": 1757707200,
"start_date": "2026-09-12 19:00:00",
"timezone": "UTC",
"status": { "long": "Match Finished", "short": "FT", "elapsed": 90 },
"score": {
"home": 1, "away": 0,
"detailed": { "half_time": "1-0", "full_time": "1-0", "extra_time": "-", "penalty": "-" }
},
"winner": "home",
"has_events": true, "has_statistics": true, "has_lineup": true
}
The same move was made everywhere it mattered. A standings row names its team. An absence names the player, the club, the competition and the fixture being missed — in v2 that was four bare ids per line, so a squad with a dozen players out cost up to 48 follow-up calls. A top-scorers row names the club, which v2 never returned at all, so the chart could not be drawn without a lookup per line. A page of odds is headed by the fixture object rather than a match_id.
Three detail levels, and expand for the rest
Embedding everything at full size would just move the cost. So an entity comes back at one of three sizes:
- Reference — id, name, logo. What you get for an entity mentioned inside another one.
- Card — adds the country and one or two headline fields. What a picker or a grid draws.
- Full — what the entity's own endpoint returns.
Nesting never goes deeper than two levels: a team carries its country, and that country does not carry its leagues back.
Anything you want larger than the default is opt-in through ?expand=, comma separated, up to five paths with at most one dot each:
GET /api/v3/match?id=372452&expand=events,lineup
GET /api/v3/leagues?country_id=3&expand=coverage
GET /api/v3/league-standing?league_id=39&season=2026&expand=team
The server echoes what it honoured in meta.expanded, so a client can always tell a relation it asked for from one it did not. An unknown path is a 400 with code expand.unknown rather than a silently ignored parameter. Payload size stays a decision the client makes instead of one it inherits.
The expensive blocks work the same way. GET /matches no longer ships events and statistics by default — they were half the weight of the v2 page and most screens never opened them. Every fixture carries has_events, has_statistics and has_lineup, so you know whether a second call is worth making before you make it.
One envelope for every response
Every answer, list or single object, success or failure, has the same five keys:
{
"status": "success",
"pagination": { "total_count": 351, "page_size": 120, "page_count": 3, "current_page": 1 },
"errors": null,
"meta": {
"api_version": "3.0.0",
"timezone": "Europe/Madrid",
"expanded": ["events"],
"generated_at": 1757707200
},
"response": [ ... ]
}
pagination is null unless the endpoint returns a list. errors is null unless status is error. Paging is ?page=, 1-based, with a page size fixed per endpoint — 120 rows on the fixture list, 20 on /odds, where a single fixture carries every market of every firm.
meta also carries filters on the endpoints that take an entity id as a filter: ask for the fixtures of a club and the club comes back resolved into an object, so a filtered list can be labelled without a second call.
What v3 returns that v2 never did
Beyond the embedding, a good deal of data that was already stored is now actually sent:
- Seasons —
GET /league-seasonsgives the dates each season ran and which one is current, where v2 answered with a flat list of years. - Rounds —
GET /league-roundsgives each round's fixture count and dates instead of bare strings, so a client knows which matchday to open on without fetching the whole calendar. - Standings —
formarrives parsed into a list next to the string it was always sent as. - Search — one
resultslist ordered byscore, each row carryingentity_type, instead of v2's three separate arrays. - Transfers — the season the move was registered in.
- News — on the football news feed,
imageis always an absolute URL,reading_timelabels a row, the publication date is a UTC timestamp plus a rendering in your zone,posts_countlets you hide a category tab that would open empty, andGET /news-contentreturnsrelated: up to five further articles on the same clubs or competitions. - Team and player statistics — the team, the competition and the season are named in the response, so a cached block can be told apart from another.
- Highlights — the nested fixture is a real match object, and the provider's camelCase keys were brought in line with the rest of the API:
imgUrlisimg_url,embedUrlisembed_url.
Three new endpoints
GET /country-coverage
Every country with at least one published competition, each with those competitions and their coverage flags — standings, events, lineups, player statistics, top scorers, news and the rest. One call tells a client which countries are worth offering and which screens will have anything in them. It is the only endpoint in the API that takes no token, because a client has to be able to see what exists before it has one. See the competition coverage page for the human-readable version.
GET /quota
What is left of your allowance, per minute, per hour and per day, with the moment each counter resets. Asking for it does not count against the allowance, so you can show a reader a budget without spending one on it.
GET /logs
Your token's own recent requests, newest first, capped at 200 — useful when an integration is misbehaving and you want to see what it actually sent. Two things differ from v2: it returns only the caller's own lines, and it is bounded.
One rename to note: v2's /injures is spelled /injuries in v3.
Rate limits you can read from the response
Every endpoint takes Authorization: Bearer <token>, and every response tells you where you stand:
X-Rate-Limit-Limit: 120
X-Rate-Limit-Remaining: 118
X-Rate-Limit-Reset: 1757707260
X-Rate-Limit-minute: 120 X-Rate-Remaining-minute: 118 X-Rate-Reset-minute: 1757707260
X-Rate-Limit-hour: 3000 X-Rate-Remaining-hour: 2874 X-Rate-Reset-hour: 1757710800
X-Rate-Limit-day: 50000 X-Rate-Remaining-day: 41233 X-Rate-Reset-day: 1757721600
The ceilings above are only an illustration — yours come from your plan. The unprefixed trio reports whichever of the three windows is closest to its ceiling. Reset values are unix seconds and the windows are calendar-based, not rolling. Going over gives a 429 with code rate_limit.exceeded and a Retry-After.
Conditional requests on the dictionaries
The endpoints whose answers barely move — /countries, /venues, /league-seasons, /league-rounds, /bookmakers, /bets, /live-bets, /country-coverage — answer with an ETag and a Cache-Control. Keep the tag, send it back as If-None-Match, and while the answer has not changed you get a 304 with no body:
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H 'If-None-Match: "a3f1c9e2"' \
https://api.goalise.com/api/v3/countries
Nothing whose answer moves under a reader carries one, so a fixture list, a live price or a prediction is always fetched in full.
Errors you can branch on
Every failure names a machine-readable code. Branch on errors[].code, never on the message text:
{
"status": "error",
"pagination": null,
"errors": [ { "code": "validation.required", "message": "season is required when league_id is given" } ],
"meta": { "api_version": "3.0.0", "generated_at": 1757707200 },
"response": null
}
The codes are validation.required, validation.invalid, expand.unknown, expand.too_deep, expand.too_many, auth.required, auth.invalid, rate_limit.exceeded, not_found and internal. A few cases that used to lie now tell the truth: a club with no coach on record is a 404 instead of a 200 with a null body, and asking for a player who was not in the team sheet is a 404 instead of a 500.
Time zones
Dates render in UTC unless you ask for an IANA zone with ?timezone=, for example ?timezone=Europe/Madrid. On the fixture list the zone also decides which calendar day a date filter selects, so the rows of a page all carry the date that was asked for. The companion timestamp fields stay UTC unix seconds whatever you pass, and meta.timezone always echoes what was used.
Getting started with API v3
- Use the token you already have. It is on your account page and it authenticates v2 and v3 alike. No subscription yet? Choose a plan.
- Point at the new base URL. Swap
/api/v2for/api/v3. - Read the reference. Every endpoint, parameter and field is documented in the API reference.
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
"https://api.goalise.com/api/v3/matches?league_id=39&season=2026&timezone=Europe/Madrid"
Migrating from v2 to v3
There is no flag day and no deadline. A practical order:
- Start where the fan-out hurts. The fixture list, the standings table, injuries, transfers and the top-scorer charts are where v2 cost the most extra calls, so they pay for themselves first.
- Delete the lookup cache as you go. The client-side map of id to name and crest that every v2 integration grew is what v3 makes unnecessary.
- Read the envelope, not the array. Take the payload from
responseand the page counters frompagination. - Branch on codes. Replace any string matching on error messages with
errors[].code. - Ask for the heavy blocks by name. Where you used to receive events and statistics on every fixture row, check
has_eventsandhas_statisticsand add?expand=only on the screens that show them. - Mind the two renames.
/injuresbecame/injuries;?id_list=on the fixture list became?ids=, and the old spelling still works.
You can move one endpoint at a time — v2 and v3 accept the same token and the same ids, so a client can call both during a migration.
Frequently asked questions
What is new in Goalise Football API v3?
v3 embeds every entity a response mentions instead of naming it by id. A fixture arrives with its competition, both clubs and the stadium as objects carrying names and crests; a standings row names its team; an absence names the player, the club, the competition and the fixture. It also adds ?expand= for opting into heavier relations, a consistent {status, pagination, errors, meta, response} envelope, machine-readable error codes, ETags on the dictionary endpoints, and three new endpoints: /quota, /logs and /country-coverage.
Is API v2 being shut down?
No. v2 is frozen — it gets no new fields — but it is untouched and keeps working, and no end-of-life date has been set. Migrate when it suits you.
Do I need a new token or a new plan for v3?
No. The same access token authenticates v2 and v3, and the same plan limits apply to both. If you already have a subscription, v3 works immediately.
How do I reduce the number of requests my app makes?
That is what v3 is for. Because a response already names every entity it mentions, a screen that cost one request plus a lookup per distinct club, competition and stadium now costs one request. Add If-None-Match on the dictionary endpoints to turn unchanged answers into 304s, and use has_events, has_statistics and has_lineup to skip calls that would come back empty.
What is the base URL of the football API v3?
https://api.goalise.com/api/v3. Every endpoint takes Authorization: Bearer <token>, except GET /country-coverage, which is open so a client can see what exists before it has a token.
How does expand work?
?expand= takes a comma-separated list of relation paths, at most five, with at most one dot each. Each endpoint documents the paths it accepts; anything else is a 400 with code expand.unknown. The server echoes the paths it honoured in meta.expanded.
Which competitions does v3 cover?
The full Goalise catalogue — domestic leagues, cups and international competitions worldwide. GET /country-coverage returns every country with a published competition and exactly which data is collected for each, or you can browse the coverage page.
Can I use API v3 with an AI assistant?
Yes. The Goalise Football MCP Server exposes the same data as tools an AI assistant can call by itself, with the same token and the same plan limits.
Ready to start? Get an access token, open the v3 reference, or talk to support if you want a hand planning the migration.