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.
https://stats-api.com/api/v1Authentication
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.
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"
// 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
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']);
}
"""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)
// 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)
}
}
// 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());
}
}
// 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-LimitX-RateLimit-RemainingX-RateLimit-ResetX-Quota-LimitX-Quota-RemainingX-Quota-ResetRetry-After · on 429X-StatsAPI-CacheX-Request-IDErrors
Errors use a consistent object with a stable machine-readable code. Undocumented query parameters return 422 unknown_parameter instead of being silently ignored.
{
"error": {
"code": "invalid_api_key",
"message": "Provide an active Stats API key…",
"details": {}
}
}Available endpoints
Loading the OpenAPI contract…
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.