API Documentation
Complete REST API for football data including fixtures, teams, players, standings, and comprehensive statistics. Build powerful football applications with real-time data.
Basics
Base URL
All requests are made to:
https://api.goal-api.com/v1Authentication
Send your API key as a Bearer token. Requests without a valid key return401.
curl "https://api.goal-api.com/v1/leagues" \
-H "Authorization: Bearer YOUR_API_KEY"Get your key from the dashboard and keep it secret — treat it like a password.
Response format
Every response is JSON with a success flag and a data payload; list endpoints add pagination.
{
"success": true,
"data": { ... },
"pagination": { "total": 100, "limit": 50, "offset": 0, "hasMore": true }
}API Resources
The GOAL API provides access to 13 main resource categories with comprehensive football data.
Webhooks
We POST match events to your URL — no connection to hold open
8 endpoints
Countries
Access country data and associated leagues
3 endpoints
Leagues
Get league information, standings, and fixtures
7 endpoints
Teams
Team data, players, fixtures, and statistics
7 endpoints
Fixtures
Match schedules, live scores, and match details
13 endpoints
Standings
League tables and team rankings
6 endpoints
Players
Player profiles, statistics, and comparisons
6 endpoints
Coaches
Coach information and team assignments
5 endpoints
Head-to-Head
Historical matchups between teams
3 endpoints
Results
Completed match results and scores
8 endpoints
Videos
Match highlights and video content
5 endpoints
Odds
Pre-match and in-play betting odds from multiple bookmakers
4 endpoints
Predictions
Model-derived outcome probabilities for upcoming matches
3 endpoints
Interactive API Playground
Every endpoint below is runnable live. Paste a key (or sign in to use your own), then hit the green Try it button on any endpoint to send a real request.
Countries
Access country data and retrieve leagues associated with specific countries.
Leagues
Get league information, standings, fixtures, teams, and top scorers for any football league.
Teams
Access team data, squad information, fixtures, results, and detailed statistics.
Fixtures
Get match schedules, live scores, lineups, events, statistics, and detailed match information.
Standings
Access league tables, team rankings, home/away standings, and standings grouped by zones.
Players
Search players, get player profiles, statistics, compare players, and view top performers.
Coaches
Get coach information, search coaches, and view coaches by team or country.
Head-to-Head
Get historical matchups and statistics between two teams.
Results
Get completed match results, today's and yesterday's results, and high-scoring matches.
Videos
Access match highlights and video content for matches and leagues.
Odds
Betting odds from multiple bookmakers. Pre-match odds cover 1X2, double chance, both-teams-to-score, Asian handicap, and over/under lines. In-play odds are refreshed every two minutes while a match is running.
Nested markets. asianHandicap and overUnder are objects keyed by line ("ah-1_1", "o+2.5") rather than fixed columns, because bookmakers publish different lines for different matches. Read the keys you need and ignore the rest.
Predictions
Outcome probabilities for upcoming matches — home win, draw, away win, double chance, over/under, both teams to score, and Asian handicap. Values are returned as strings between "0.00" and "1.00".
WebSocket (Live Data)
Subscribe to a match and receive live score, event, and statistics updates over a persistent WebSocket connection, instead of polling the REST API.
wss://api.goal-api.com/wsLive data access and the number of matches you can subscribe to at once depend on your plan. Check pricing for details.
Note: Browsers cannot set custom headers on a WebSocket handshake, so Authorization headers don't work here the way they do for REST calls. Use a short-lived connection token instead (Step 1 below) — it keeps your real API key out of the WebSocket URL entirely.
1. Get a connection token
From your backend or app startup code, exchange your API key for a short-lived, single-use connection token (valid for 60 seconds, and consumed the moment it's used). This is a normal authenticated HTTP request, so your API key is sent as a header exactly like every other endpoint on this page.
/v1/ws/tokenconst res = await fetch("https://api.goal-api.com/v1/ws/token", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` }
});
const { data } = await res.json();
// data.token, data.expiresIn (seconds)2. Connect and authenticate the socket
Open the WebSocket with the token as a query parameter, then send it again as your first message to complete authentication.
const ws = new WebSocket(`wss://api.goal-api.com/ws?wsToken=${data.token}`);
ws.onopen = () => {
ws.send(JSON.stringify({ type: "auth", token: data.token }));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log(message.type, message.data);
};Server-side clients (Node.js, Python, Go, etc.) that can set custom headers may instead send Authorization: Bearer YOUR_API_KEY directly on the handshake and skip Step 1, then authenticate the socket with {"type":"auth","apiKey":"YOUR_API_KEY"}.
3. Subscribe to a match
Once you receive auth_success, subscribe to any fixture by its match ID to start receiving updates for it.
ws.send(JSON.stringify({
type: "subscribe",
resource: "match",
matchId: "12345"
}));4. Receive live updates
While subscribed, you'll receive a match_update message whenever the match state changes.
{
"type": "match_update",
"data": {
"match_id": "12345",
"match_status": "2H",
"match_hometeam_score": "2",
"match_awayteam_score": "1",
"goalscorer": [ ... ],
"cards": [ ... ]
},
"timestamp": 1783040229469
}| Message type | Direction | Description |
|---|---|---|
| auth | Client → Server | First message on every connection. Send { token } or { apiKey }. |
| auth_success | Server → Client | Authentication succeeded, including your plan's feature flags and limits. |
| subscribe / unsubscribe | Client → Server | Add or remove a match subscription: { resource: "match", matchId }. |
| subscribe_response / unsubscribe_response | Server → Client | Result of a subscribe/unsubscribe request. |
| get_subscriptions | Client → Server | List your currently subscribed match IDs. |
| status | Client → Server | Get your connection's plan, limits, and subscription count. |
| match_update | Server → Client | Pushed whenever a subscribed match's score, events, or stats change. |
| ping / pong | Client ↔ Server | Optional application-level keepalive. |
| error | Server → Client | Sent whenever anything goes wrong — including authentication failures. See the error shape below. |
Error messages
Every error — including a failed auth, a rejected subscription, or a malformed message — arrives as a single error-typed message (or, for a failed subscribe/unsubscribe, as the matching *_response with success: false). Check error.category to branch behavior (e.g. re-authenticate on "authentication", back off on "rate_limit") instead of matching on the message text.
{
"type": "error",
"success": false,
"error": {
"code": "AUTH_FAILED",
"message": "Invalid or expired token",
"category": "authentication"
},
"timestamp": 1783040229469
}| Plan | Concurrent match subscriptions |
|---|---|
| FREE | 0 |
| BASIC | 5 |
| PRO | 20 |
| ENTERPRISE | 1000 |
Webhooks
The counterpart to the WebSocket API. Instead of holding a connection open, you register a URL and we POST to it when a match event happens — a goal, a kick-off, a full-time whistle. This suits serverless functions and backends that cannot keep a socket alive. Manage your endpoints in the dashboard.
Events
match.startedA match has kicked off.match.finishedA match has ended (including after extra time or penalties).goal.scoredThe score went up. Fires once per goal.score.changedThe score changed WITHOUT going up — the provider corrected it. Not a goal.match.status_changedAny match status transition.Filtering by competition
By default an endpoint receives events from every competition we cover. Set leagueIds to receive only the ones you follow. Filtered-out events are never queued, so they never reach your endpoint and never count against your plan.
# Only these competitions. Ids come from GET /leagues
# (either `id` or `apiId` is accepted), or from
# GET /auth/webhook-endpoints/leagues.
curl -X PATCH https://api.goalapi.io/auth/webhook-endpoints/wh_123 \
-H "Authorization: Bearer $GOAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "leagueIds": ["152", "302", "207"] }'
# An empty array turns the filter off again — every competition, the default.
-d '{ "leagueIds": [] }'Managing endpoints
These are normal authenticated REST calls — your API key as a Bearer token, exactly like every other endpoint on this page.
Verifying the signature
This is not optional. Your endpoint URL is not a secret — it appears in logs and proxies. If you accept any POST that reaches it, anyone who learns the URL can send you fake events. Every delivery carries an X-Goal-Signature header; verify it, and reject anything that does not match.
Node.js / Express// Node.js / Express
import crypto from "crypto";
app.post("/webhooks/goal-api",
express.raw({ type: "application/json" }), // the RAW bytes — a re-serialised
(req, res) => { // body will not match the signature
const header = req.get("X-Goal-Signature"); // t=1736899200,v1=abc123...
const [t, v1] = header.split(",").map(p => p.split("=")[1]);
const expected = crypto
.createHmac("sha256", process.env.GOAL_WEBHOOK_SECRET)
.update(t + "." + req.body)
.digest("hex");
// Constant-time compare: a plain === leaks the correct signature one byte at a
// time to anyone patient enough to measure how long the comparison took.
const valid = crypto.timingSafeEqual(
Buffer.from(v1, "hex"),
Buffer.from(expected, "hex")
);
if (!valid) return res.sendStatus(401);
// Reject anything older than five minutes, or a captured delivery can be replayed.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(401);
const event = JSON.parse(req.body);
console.log(event.event, event.data); // "goal.scored", { homeTeam, score, ... }
// Answer 2xx fast. We retry on anything else (1m, 5m, 25m, 2h, 10h), and we time
// out after 8s — so do the work after responding, not before.
res.sendStatus(200);
}
);Delivery & retries
- Respond 2xx quickly. We time out after 8 seconds, so do your work after responding, not before.
- A non-2xx response is retried with backoff:
1m, 5m, 25m, 2h, 10h. - An endpoint that fails 20 times in a row is disabled automatically. Fix it and re-enable it from the dashboard.
- Every attempt is logged — status code, duration, error — on the endpoint's page in the dashboard.
Deliveries and your plan quota
A webhook delivery is a request we make on your behalf, and it is metered like one: each delivery attempt counts against the same allowance as your API calls. There is no separate webhook budget. Three consequences worth knowing before you rely on it:
- A retry costs a request each time. An endpoint returning
500through all five attempts spends five. - When your allowance runs out, deliveries are dropped, not queued. A goal notification arriving a day late is worse than one that never arrives, so we do not hold them — the event is gone.
- That makes the competition filter the difference between an allowance spent on matches you follow and one spent on the worldwide feed.
GET /auth/webhook-endpoints/:id/stats breaks that spend down by competition and by event, including how much went on retries and how many events were dropped. The same breakdown is on the endpoint's card in the dashboard, where each row is a checkbox that writes your filter.
Testing locally
Deliveries are sent from our servers, so they cannot reach localhost or a private address — your machine is not on the public internet, and those URLs are rejected. Expose your local endpoint with a tunnel and register the public URL it gives you:
# ngrok
ngrok http 3000
# -> https://abc123.ngrok-free.app (register this as your webhook URL)
# or cloudflared
cloudflared tunnel --url http://localhost:3000Then hit Send test next to your endpoint in the dashboard to fire a signed sample event immediately — no need to wait for a live match.
Rate Limits
API rate limits are enforced to ensure fair usage and optimal performance for all users. Your rate limit depends on your subscription plan.
How Rate Limiting Works
Rate limits are applied per API key and reset based on your plan's billing cycle. When you exceed your limit, the API will return a 429 Too Many Requests status code.
Per API Key
Each API key has its own independent rate limit quota
Every Request Counts
Every API request you make counts toward your plan's quota
Response Headers
Every API response includes headers to help you track your usage:
X-RateLimit-LimitMaximum requests allowedX-RateLimit-RemainingRequests remaining in current periodX-RateLimit-ResetUnix timestamp when limit resetsBest Practices
- Monitor the
X-RateLimit-Remainingheader to track your usage - Implement exponential backoff when you receive a 429 response
- Cache responses on your end to reduce API calls
- Use pagination parameters to fetch data in smaller chunks
Need higher limits?
Check our pricing plans for higher rate limits and additional features.
Error Handling
The API uses standard HTTP status codes to indicate success or failure.
| Status Code | Description |
|---|---|
| 200 | Successful request |
| 204 | No content found |
| 400 | Bad request (invalid parameters) |
| 401 | Unauthorized (missing or invalid API key) |
| 429 | Too many requests (rate limit or plan quota exceeded) |
| 500 | Server error |