# Football Stats API documentation for developers

Build football apps and AI agents with fixture, result, competition, and team data. Explore endpoints, authentication, code examples, and OpenAPI docs.

Canonical URL: https://stats-api.com/docs

API v1 · Beta

[Open text ↗](https://stats-api.com/llms-full.txt)

## Build your first football data request.

Stats API is a football stats API for fixtures, results, competitions, teams, and match team statistics including corners, shots, possession, cards and expected goals. Statistics are available only for the competitions our sources cover, and every match response states its own coverage rather than failing. Ingestion is scheduled rather than streamed, so this is not a guaranteed live feed. Use one documented v1 contract to power apps, dashboards, research workflows, and AI agents.

**Base URL** `https://stats-api.com/api/v1`

## Authentication

Send the API key in the HTTP `Authorization` header. Keys are displayed once, stored as SHA-256 hashes, and can be revoked independently.

Keep the key in a server-side environment variable or secret manager. Never put it in a URL, browser bundle, mobile binary, log, source file, or AI prompt.

## Direct HTTP client examples

Stats API does not require a language package. These are complete direct HTTP examples, not official SDK packages. Each one reads `STATS_API_KEY` from the server-side process environment, calls the implemented competition collection, sets a timeout, validates failures, and preserves `X-Request-ID` for debugging.

Use Node.js 18+, Python 3 with `requests`, PHP with cURL, Go, Java 11+, or .NET 6+. Browser and mobile code should call your own server route so the bearer key stays private.

**cURL**

```bash
test -n "$STATS_API_KEY" || { echo "STATS_API_KEY is required." >&2; exit 1; }
curl --fail-with-body --silent --show-error --max-time 15 \
  "https://stats-api.com/api/v1/football/competitions?limit=10" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer $STATS_API_KEY"
```

**JavaScript**

```javascript
// Direct Stats API request using Node.js 18+ built-in fetch.

async function main() {
  const apiKey = process.env.STATS_API_KEY?.trim();
  if (!apiKey) {
    throw new Error('STATS_API_KEY is required.');
  }

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 15_000);
  let response;
  let body;
  let requestId = 'unknown';

  try {
    response = await fetch(
      'https://stats-api.com/api/v1/football/competitions?limit=10',
      {
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${apiKey}`,
        },
        signal: controller.signal,
      },
    );
    requestId = response.headers.get('x-request-id') || 'unknown';
    body = await response.text();
  } catch (error) {
    const reason = error.name === 'AbortError' ? 'request timed out' : error.message;
    throw new Error(`Stats API network error: ${reason}`, { cause: error });
  } finally {
    clearTimeout(timeout);
  }

  let payload;

  try {
    payload = JSON.parse(body);
  } catch (error) {
    throw new Error(
      `Stats API HTTP ${response.status} returned invalid JSON `
        + `(request ID: ${requestId}).`,
      { cause: error },
    );
  }

  if (!response.ok) {
    const code = payload?.error?.code || 'http_error';
    const message = payload?.error?.message || response.statusText;
    throw new Error(
      `Stats API HTTP ${response.status}: ${code}: ${message} `
        + `(request ID: ${requestId}).`,
    );
  }

  if (!Array.isArray(payload.data)) {
    throw new Error(
      `Stats API returned an unexpected response (request ID: ${requestId}).`,
    );
  }

  for (const competition of payload.data) {
    console.log(`${competition.id}: ${competition.name}`);
  }
}

main().catch((error) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exitCode = 1;
});
```

**PHP**

```php
<?php

declare(strict_types=1);

// Direct Stats API request using PHP's cURL extension.

$apiKey = trim((string) getenv('STATS_API_KEY'));
if ($apiKey === '') {
    fwrite(STDERR, "STATS_API_KEY is required.\n");
    exit(1);
}

$handle = curl_init(
    'https://stats-api.com/api/v1/football/competitions?limit=10'
);
if ($handle === false) {
    fwrite(STDERR, "Unable to initialize cURL.\n");
    exit(1);
}

$responseHeaders = [];
curl_setopt_array($handle, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_TIMEOUT => 15,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_HEADERFUNCTION => static function (
        mixed $curl,
        string $header
    ) use (&$responseHeaders): int {
        $length = strlen($header);
        $parts = explode(':', $header, 2);
        if (count($parts) === 2) {
            $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
        }
        return $length;
    },
]);

$body = curl_exec($handle);
if ($body === false) {
    $message = curl_error($handle);
    curl_close($handle);
    fwrite(STDERR, "Stats API network error: {$message}\n");
    exit(1);
}

$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
$requestId = $responseHeaders['x-request-id'] ?? 'unknown';

try {
    $payload = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException) {
    fwrite(
        STDERR,
        "Stats API HTTP {$status} returned invalid JSON "
        . "(request ID: {$requestId}).\n"
    );
    exit(1);
}

if ($status < 200 || $status >= 300) {
    $error = is_array($payload['error'] ?? null) ? $payload['error'] : [];
    $code = (string) ($error['code'] ?? 'http_error');
    $message = (string) ($error['message'] ?? 'Request failed.');
    fwrite(
        STDERR,
        "Stats API HTTP {$status}: {$code}: {$message} "
        . "(request ID: {$requestId}).\n"
    );
    exit(1);
}

$competitions = $payload['data'] ?? null;
if (!is_array($competitions)) {
    fwrite(
        STDERR,
        "Stats API returned an unexpected response (request ID: {$requestId}).\n"
    );
    exit(1);
}

foreach ($competitions as $competition) {
    printf("%s: %s\n", $competition['id'], $competition['name']);
}
```

**Python**

```python
"""Direct Stats API request using the generic requests HTTP client."""

import os
import sys

import requests

URL = "https://stats-api.com/api/v1/football/competitions"

def main() -> None:
    api_key = os.environ.get("STATS_API_KEY", "").strip()
    if not api_key:
        raise SystemExit("STATS_API_KEY is required.")

    try:
        response = requests.get(
            URL,
            params={"limit": 10},
            headers={
                "Accept": "application/json",
                "Authorization": f"Bearer {api_key}",
            },
            timeout=(5, 15),
        )
    except requests.RequestException as exc:
        raise SystemExit(f"Stats API network error: {exc}") from exc

    request_id = response.headers.get("X-Request-ID", "unknown")
    try:
        payload = response.json()
    except ValueError as exc:
        raise SystemExit(
            f"Stats API HTTP {response.status_code} returned invalid JSON "
            f"(request ID: {request_id})."
        ) from exc

    if not response.ok:
        error = payload.get("error", {}) if isinstance(payload, dict) else {}
        code = error.get("code", "http_error")
        message = error.get("message", response.reason)
        raise SystemExit(
            f"Stats API HTTP {response.status_code}: {code}: {message} "
            f"(request ID: {request_id})."
        )

    competitions = payload.get("data") if isinstance(payload, dict) else None
    if not isinstance(competitions, list):
        raise SystemExit(
            f"Stats API returned an unexpected response "
            f"(request ID: {request_id})."
        )

    for competition in competitions:
        print(f"{competition['id']}: {competition['name']}")

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        sys.exit(130)
```

**Go**

```go
// Direct Stats API request using Go's standard net/http package.
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

const endpoint = "https://stats-api.com/api/v1/football/competitions?limit=10"

type apiResponse struct {
	Data []struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	} `json:"data"`
	Error *struct {
		Code    string `json:"code"`
		Message string `json:"message"`
	} `json:"error"`
}

func run() error {
	apiKey := strings.TrimSpace(os.Getenv("STATS_API_KEY"))
	if apiKey == "" {
		return fmt.Errorf("STATS_API_KEY is required")
	}

	request, err := http.NewRequest(http.MethodGet, endpoint, nil)
	if err != nil {
		return fmt.Errorf("build request: %w", err)
	}
	request.Header.Set("Accept", "application/json")
	request.Header.Set("Authorization", "Bearer "+apiKey)

	client := &http.Client{Timeout: 15 * time.Second}
	response, err := client.Do(request)
	if err != nil {
		return fmt.Errorf("Stats API network error: %w", err)
	}
	defer response.Body.Close()

	body, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
	if err != nil {
		return fmt.Errorf("read Stats API response: %w", err)
	}
	requestID := response.Header.Get("X-Request-ID")
	if requestID == "" {
		requestID = "unknown"
	}

	var payload apiResponse
	jsonErr := json.Unmarshal(body, &payload)
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		if jsonErr == nil && payload.Error != nil {
			return fmt.Errorf(
				"Stats API HTTP %d: %s: %s (request ID: %s)",
				response.StatusCode,
				payload.Error.Code,
				payload.Error.Message,
				requestID,
			)
		}
		return fmt.Errorf(
			"Stats API HTTP %d returned an invalid error body (request ID: %s)",
			response.StatusCode,
			requestID,
		)
	}
	if jsonErr != nil {
		return fmt.Errorf(
			"Stats API HTTP %d returned invalid JSON (request ID: %s): %w",
			response.StatusCode,
			requestID,
			jsonErr,
		)
	}
	if payload.Data == nil {
		return fmt.Errorf(
			"Stats API returned an unexpected response (request ID: %s)",
			requestID,
		)
	}

	for _, competition := range payload.Data {
		fmt.Printf("%s: %s\n", competition.ID, competition.Name)
	}
	return nil
}

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}
```

**Java**

```java
// Direct Stats API request using Java 11+'s standard HTTP client.

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;

public final class StatsApiExample {
    private static final URI ENDPOINT = URI.create(
        "https://stats-api.com/api/v1/football/competitions?limit=10"
    );

    private StatsApiExample() {
    }

    public static void main(String[] args) {
        try {
            run();
        } catch (IOException | RuntimeException exception) {
            System.err.println(exception.getMessage());
            System.exit(1);
        }
    }

    private static void run() throws IOException {
        String apiKey = System.getenv("STATS_API_KEY");
        if (apiKey == null || apiKey.isBlank()) {
            throw new IllegalStateException("STATS_API_KEY is required.");
        }

        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(5))
            .build();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(ENDPOINT)
            .timeout(Duration.ofSeconds(15))
            .header("Accept", "application/json")
            .header("Authorization", "Bearer " + apiKey.trim())
            .GET()
            .build();

        HttpResponse<String> response;
        try {
            response = client.send(
                request,
                HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
            );
        } catch (InterruptedException exception) {
            Thread.currentThread().interrupt();
            throw new IOException("Stats API request was interrupted.", exception);
        }

        String requestId = response.headers()
            .firstValue("X-Request-ID")
            .orElse("unknown");
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            String summary = response.body().replaceAll("\\s+", " ").trim();
            if (summary.length() > 300) {
                summary = summary.substring(0, 300) + "…";
            }
            throw new IOException(
                "Stats API HTTP " + response.statusCode()
                    + " (request ID: " + requestId + "): " + summary
            );
        }
        if (response.body().isBlank()) {
            throw new IOException(
                "Stats API returned an empty response (request ID: "
                    + requestId + ")."
            );
        }
        String contentType = response.headers()
            .firstValue("Content-Type")
            .orElse("");
        if (!contentType.split(";", 2)[0].trim().equalsIgnoreCase("application/json")) {
            throw new IOException(
                "Stats API returned an unexpected content type (request ID: "
                    + requestId + ")."
            );
        }

        // The response is JSON with top-level data and meta properties.
        System.out.println(response.body());
    }
}
```

**C#**

```csharp
// Direct Stats API request using .NET 6+'s standard HttpClient.

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;

try
{
    await RunAsync();
}
catch (Exception exception)
{
    Console.Error.WriteLine(exception.Message);
    Environment.ExitCode = 1;
}

static async Task RunAsync()
{
    var apiKey = Environment.GetEnvironmentVariable("STATS_API_KEY")?.Trim();
    if (string.IsNullOrEmpty(apiKey))
    {
        throw new InvalidOperationException("STATS_API_KEY is required.");
    }

    using var client = new HttpClient
    {
        Timeout = TimeSpan.FromSeconds(15),
    };
    using var request = new HttpRequestMessage(
        HttpMethod.Get,
        "https://stats-api.com/api/v1/football/competitions?limit=10"
    );
    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);

    HttpResponseMessage response;
    try
    {
        response = await client.SendAsync(request);
    }
    catch (TaskCanceledException exception)
    {
        throw new HttpRequestException("Stats API request timed out.", exception);
    }
    catch (HttpRequestException exception)
    {
        throw new HttpRequestException(
            $"Stats API network error: {exception.Message}",
            exception
        );
    }

    using (response)
    {
        var body = await response.Content.ReadAsStringAsync();
        var requestId = response.Headers.TryGetValues("X-Request-ID", out var values)
            ? string.Join(",", values)
            : "unknown";

        JsonDocument payload;
        try
        {
            payload = JsonDocument.Parse(body);
        }
        catch (JsonException exception)
        {
            throw new InvalidDataException(
                $"Stats API HTTP {(int)response.StatusCode} returned invalid JSON "
                    + $"(request ID: {requestId}).",
                exception
            );
        }

        using (payload)
        {
            if (!response.IsSuccessStatusCode)
            {
                var code = "http_error";
                var message = response.ReasonPhrase ?? "Request failed.";
                if (
                    payload.RootElement.TryGetProperty("error", out var error)
                    && error.TryGetProperty("code", out var codeElement)
                    && error.TryGetProperty("message", out var messageElement)
                )
                {
                    code = codeElement.GetString() ?? code;
                    message = messageElement.GetString() ?? message;
                }
                throw new HttpRequestException(
                    $"Stats API HTTP {(int)response.StatusCode}: {code}: {message} "
                        + $"(request ID: {requestId})."
                );
            }

            if (
                !payload.RootElement.TryGetProperty("data", out var competitions)
                || competitions.ValueKind != JsonValueKind.Array
            )
            {
                throw new InvalidDataException(
                    $"Stats API returned an unexpected response "
                        + $"(request ID: {requestId})."
                );
            }

            foreach (var competition in competitions.EnumerateArray())
            {
                Console.WriteLine(
                    $"{competition.GetProperty("id").GetString()}: "
                        + competition.GetProperty("name").GetString()
                );
            }
        }
    }
}
```

Repository users can also copy the standalone files from `examples/direct-http/`. The OpenAPI document remains authoritative if an example and the contract ever disagree.

## Pagination

Collection endpoints accept `page` and `limit`. The default limit is 25 and the maximum is 100. Response metadata includes `total`, `has_more`, and a nullable `next_page`, so clients do not need to infer when pagination ends.

| Parameter | Type | Default |
| --- | --- | --- |
| `page` | integer | 1 |
| `limit` | integer · 1–100 | 25 |

## Rate limits and quota

Every request admitted through the per-minute limit counts once toward the billing-period quota, including a Redis cache hit and an application-level error produced after authentication. Authentication failures and requests rejected by the per-minute limiter do not consume the billing-period quota. Response headers show both allowances.

`X-RateLimit-Limit` `X-RateLimit-Remaining` `X-RateLimit-Reset` `X-Quota-Limit` `X-Quota-Remaining` `X-Quota-Reset` `Retry-After · on 429` `X-StatsAPI-Cache` `X-Request-ID`

## Errors

Errors use a consistent object with a stable machine-readable code. Undocumented query parameters return `422 unknown_parameter` instead of being silently ignored.

JSON

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "Provide an active Stats API key…",
    "details": {}
  }
}
```

## Available endpoints

Every operation is a read-only `GET` under the base URL above. Send the parameters marked required; the OpenAPI document is the complete contract, with every field.

| Operation | Summary | Parameters |
| --- | --- | --- |
| `GET /coverage/leagues` | List covered competitions | `page`, `limit` |
| `GET /coverage/leagues/{competition_id}` | Get competition coverage | `competition_id` (required) |
| `GET /coverage/summary` | Get coverage totals | — |
| `GET /football/competitions` | List competitions | `country_code`, `q`, `page`, `limit` |
| `GET /football/competitions/{competition_id}` | Get competition details | `competition_id` (required) |
| `GET /football/competitions/{competition_id}/seasons` | List competition seasons | `competition_id` (required) |
| `GET /football/competitions/{competition_id}/seasons/{season_id}/standings` | League table for one season | `competition_id` (required), `season_id` (required) |
| `GET /football/matches` | List matches | `competition_id`, `season_id`, `team_id`, `status`, `date_from`, `date_to`, `page`, `limit` |
| `GET /football/matches/{match_id}` | Get match details | `match_id` (required) |
| `GET /football/matches/{match_id}/lineups` | Confirmed lineups for one match | `match_id` (required) |
| `GET /football/matches/{match_id}/live-player-stats` | In-play per-player statistics | `match_id` (required) |
| `GET /football/matches/{match_id}/live-stats` | In-play team statistics | `match_id` (required) |
| `GET /football/matches/{match_id}/live-timeline` | In-play match timeline | `match_id` (required) |
| `GET /football/matches/{match_id}/player-stats` | Per-player match statistics | `match_id` (required) |
| `GET /football/matches/{match_id}/stats` | Team statistics for one match | `match_id` (required) |
| `GET /football/matches/{match_id}/timeline` | Settled match timeline | `match_id` (required) |
| `GET /football/players` | List players | `page`, `limit`, `q` |
| `GET /football/players/{player_id}` | Get one player | `player_id` (required) |
| `GET /football/players/{player_id}/injuries-suspensions` | Injuries and suspensions for one player | `player_id` (required) |
| `GET /football/teams` | List and search teams | `q`, `page`, `limit` |
| `GET /football/teams/{team_id}` | Get team details | `team_id` (required) |
| `GET /football/teams/{team_id}/injuries-suspensions` | Injuries and suspensions for one team | `team_id` (required) |
| `GET /football/teams/{team_id}/players` | Current squad for one team | `team_id` (required) |
| `GET /football/teams/{team_id}/standings` | Standings rows for one team | `team_id` (required) |
| `GET /health` | Check service health | — |
| `GET /football/competitions/{competition_id}/seasons/{season_id}/groups` | Groups observed in season standings | `competition_id` (required), `season_id` (required) |
| `GET /football/matches/{match_id}/referee` | Referee assignment for a match | `match_id` (required) |
| `GET /football/teams/{team_id}/stats` | Team statistics for one season | `team_id` (required), `season_id` (required) |
| `GET /football/players/{player_id}/stats` | Player statistics for one season | `player_id` (required), `season_id` (required) |
| `GET /football/matches/{match_id}/odds` | Prematch odds for a match | `match_id` (required) |
| `GET /football/matches/{match_id}/odds/live` | Live odds for a match | `match_id` (required) |
| `GET /football/matches/{match_id}/odds/players` | Observed prematch player markets | `match_id` (required) |

## 35-operation coverage map

Our roadmap uses the mature football API category as a capability benchmark. Only operations in the OpenAPI contract above are available today.

### Coverage

- `GET /coverage/summary` — available
- `GET /coverage/leagues` — available
- `GET /coverage/leagues/{competition_id}` — available

### Competitions

- `GET /football/competitions` — available
- `GET /football/competitions/{competition_id}` — available
- `GET /football/competitions/{competition_id}/seasons` — available
- `GET /football/competitions/{competition_id}/seasons/{season_id}/groups` — available
- `GET /football/competitions/{competition_id}/seasons/{season_id}/standings` — available

### Matches

- `GET /football/matches` — available
- `GET /football/matches/{match_id}` — available
- `GET /football/matches/{match_id}/lineups` — available
- `GET /football/matches/{match_id}/live-player-stats` — available
- `GET /football/matches/{match_id}/live-stats` — available
- `GET /football/matches/{match_id}/live-timeline` — available
- `GET /football/matches/{match_id}/player-stats` — available
- `GET /football/matches/{match_id}/players/{player_id}/heatmap` — planned
- `GET /football/matches/{match_id}/referee` — available
- `GET /football/matches/{match_id}/shotmap` — planned
- `GET /football/matches/{match_id}/stats` — available
- `GET /football/matches/{match_id}/timeline` — available

### Teams

- `GET /football/teams` — available
- `GET /football/teams/{team_id}` — available
- `GET /football/teams/{team_id}/injuries-suspensions` — available
- `GET /football/teams/{team_id}/players` — available
- `GET /football/teams/{team_id}/standings` — available
- `GET /football/teams/{team_id}/stats` — available

### Players

- `GET /football/players` — available
- `GET /football/players/{player_id}` — available
- `GET /football/players/{player_id}/competitions/{competition_id}/seasons/{season_id}/heatmap` — planned
- `GET /football/players/{player_id}/injuries-suspensions` — available
- `GET /football/players/{player_id}/stats` — available

### Odds

- `GET /football/matches/{match_id}/odds` — available
- `GET /football/matches/{match_id}/odds/live` — available
- `GET /football/matches/{match_id}/odds/players` — available

### Platform

- `GET /health` — available

---

Markdown version of https://stats-api.com/docs. Site index: https://stats-api.com/llms.txt · Full documentation and OpenAPI contract: https://stats-api.com/llms-full.txt · AI usage policy: https://stats-api.com/ai.txt
