Developer API

Build on Referee.Observer data. The public REST API exposes match officials, matches, discipline ratings and appointments. Webhooks push domain events to your systems in real time.

Authentication

The API uses bearer API keys. Every request must send an Authorization header:

Authorization: Bearer ro_your_api_key_here
  • Keys are issued from the admin panel at /admin/api-keys. Each key belongs to an organization and is shown only once at creation — store it securely.
  • Keys look like ro_<48 hex characters>. The server stores only a SHA-256 hash, so a leaked database never exposes usable keys.
  • Scopes / permissions: keys carry a permissions array (default ["read"]). The v1 endpoints are read-only.
  • A missing or malformed header returns 401 with WWW-Authenticate: Bearer. A revoked or unknown key also returns 401.

Rate limits

Each key is limited to a rolling 24-hour quota — by default 1000 requests/day (configurable per key). Every authenticated response carries rate-limit headers:

X-RateLimit-Limit:     1000     # quota for the 24h window
X-RateLimit-Remaining: 994      # requests left
X-RateLimit-Reset:     1772150400  # unix epoch seconds when the window resets

When the quota is exhausted the API responds 429 Too Many Requests with a Retry-After header (seconds until the window resets), alongside the same X-RateLimit-* headers.

API reference

The interactive reference below is generated from the live OpenAPI spec at /api/openapi. Use the Authorize button to paste your key and try requests.

Loading API reference…

Webhooks

Webhooks are configured per organization. When a subscribed event fires, we POST a signed JSON body to your endpoint. The request carries these headers:

X-Webhook-Event:     match.created          # the event name
X-Webhook-Signature: <hmac-sha256-hex>      # HMAC-SHA256 of the raw body
X-Delivery-ID:       <uuid>                 # unique per delivery
Content-Type:        application/json
User-Agent:          RefereeObserver-Webhooks/1.0

The body always has the shape { event, delivery_id, payload }. Failed deliveries (non-2xx, timeout) are retried with exponential backoff (1m, 5m, 30m, 2h, then a final 6h attempt).

Event catalog

match.created

A new match was imported or created for one of your organization’s competitions.

{
  "event": "match.created",
  "delivery_id": "d3b07384-...-a1b2c3",
  "payload": {
    "match_id": 50123,
    "competition_id": 42,
    "home_team": "Olympique Lyonnais",
    "away_team": "Paris Saint-Germain",
    "match_date": "2026-03-14"
  }
}

assignment.made

An official appointment was confirmed for one of your matches.

{
  "event": "assignment.made",
  "delivery_id": "e7c91a02-...-f4d5e6",
  "payload": {
    "match_id": 50123,
    "official_id": 123,
    "role": "referee"
  }
}

official.updated

An official’s profile linked to your organization was edited.

{
  "event": "official.updated",
  "delivery_id": "a1f2b3c4-...-998877",
  "payload": {
    "official_id": 123
  }
}

Verifying the signature

Recompute an HMAC-SHA256 over the raw request body using your webhook’s secret and compare it (constant-time) to X-Webhook-Signature.

Node.js

import crypto from "crypto";

// Express-style handler. Use the RAW request body (bytes as received).
function verifyWebhook(rawBody, headers, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody, "utf8")
    .digest("hex");
  const received = headers["x-webhook-signature"] || "";
  // constant-time compare
  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python

import hmac, hashlib

def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

Code examples

curl

curl -s https://www.referee.observer/api/v1/officials?country=FRA&limit=5 \
  -H "Authorization: Bearer ro_your_api_key_here"

JavaScript (fetch)

const res = await fetch(
  "https://www.referee.observer/api/v1/officials?country=FRA&limit=5",
  { headers: { Authorization: "Bearer ro_your_api_key_here" } }
);

console.log("remaining:", res.headers.get("X-RateLimit-Remaining"));
const { api_version, data, pagination } = await res.json();
console.log(api_version, pagination, data);

Python (requests)

import requests

resp = requests.get(
    "https://www.referee.observer/api/v1/officials",
    params={"country": "FRA", "limit": 5},
    headers={"Authorization": "Bearer ro_your_api_key_here"},
)
resp.raise_for_status()
print("remaining:", resp.headers.get("X-RateLimit-Remaining"))
body = resp.json()
print(body["api_version"], body["pagination"])
for official in body["data"]:
    print(official["full_name"], official["nationality"])