API Documentation

Complete REST API for football data including fixtures, teams, players, standings, and comprehensive statistics. Build powerful football applications with real-time data.

Ready to get started?

Sign up for a free API key and start building in minutes.

Basics

Base URL

All requests are made to:

https://api.goal-api.com/v1

Authentication

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.

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/ws

Live 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.

POST/v1/ws/token
const 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 typeDirectionDescription
authClient → ServerFirst message on every connection. Send { token } or { apiKey }.
auth_successServer → ClientAuthentication succeeded, including your plan's feature flags and limits.
subscribe / unsubscribeClient → ServerAdd or remove a match subscription: { resource: "match", matchId }.
subscribe_response / unsubscribe_responseServer → ClientResult of a subscribe/unsubscribe request.
get_subscriptionsClient → ServerList your currently subscribed match IDs.
statusClient → ServerGet your connection's plan, limits, and subscription count.
match_updateServer → ClientPushed whenever a subscribed match's score, events, or stats change.
ping / pongClient ↔ ServerOptional application-level keepalive.
errorServer → ClientSent 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
}
PlanConcurrent match subscriptions
FREE0
BASIC5
PRO20
ENTERPRISE1000

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 500 through 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:3000

Then 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:

Headers
X-RateLimit-LimitMaximum requests allowed
X-RateLimit-RemainingRequests remaining in current period
X-RateLimit-ResetUnix timestamp when limit resets

Best Practices

  • Monitor the X-RateLimit-Remaining header 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 CodeDescription
200Successful request
204No content found
400Bad request (invalid parameters)
401Unauthorized (missing or invalid API key)
429Too many requests (rate limit or plan quota exceeded)
500Server error