API Reference

API Reference

The RaceHooks API is a REST API returning JSON. All requests are authenticated via Bearer token and versioned under /v1. The API is sport-agnostic at the infrastructure level — feeds, webhooks, and simulate endpoints will serve all supported sports as the platform expands beyond F1.

Base URLhttps://api.racehooks.io/v1

Authentication

RaceHooks uses OAuth 2.0 client credentials. Exchange your client_id and client_secret for a Bearer token, then include it in the Authorization header on all other endpoints. Tokens expire after 12 hours.

New accounts sign up, verify their email, and retrieve credentials first — the full account lifecycle (signup, verify-email, login, resend-verification) is documented on the Authentication page. Get your client_id/client_secret from the console API Keys page, or programmatically via POST /v1/oauth/credentials with a session cookie.

POST/v1/oauth
bash
curl -X POST https://api.racehooks.io/v1/oauth \
  -H "Content-Type: application/json" \
  -d '{
    "client_id":     "your_client_id",
    "client_secret": "your_client_secret",
    "grant_type":    "client_credentials"
  }'

# Response — the token is an opaque string (not a JWT); treat it as a secret
{
  "access_token": "vN3xKq81tYw2LmR9bC6dF7gHjS5aU0eIPo4Z",
  "token_type":   "Bearer",
  "expires_in":   43200
}

# All subsequent requests:
curl https://api.racehooks.io/v1/webhooks \
  -H "Authorization: Bearer vN3xKq81tYw2LmR9bC6dF7gHjS5aU0eIPo4Z"
POST/v1/oauth/credentials

Returns { clientId, clientSecret } for the logged-in account. Authenticated by the session cookie set by POST /v1/auth/login or POST /v1/auth/verify-email — no Bearer token required.

Webhooks

Create, list, update, delete, and test webhook endpoint registrations.

Create a webhook

POST/v1/webhooks
bash
curl -X POST https://api.racehooks.io/v1/webhooks \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "webhookUrl": "https://your-app.com/f1",
    "feedId": "timing.data",
    "filters": {
      "driverNumbers": ["1", "4", "16"]
    }
  }'

Provide exactly one feed selection: feedId, feedIds, or subscriptions — omitting all three returns 400.

FieldTypeDescription
webhookUrlstring (required)The HTTPS endpoint to receive deliveries. Must be publicly reachable.
feedIdstring (one required)Subscribe to a single feed (the simplest form). See the feed catalog for valid IDs.
feedIdsstring[] (one required)Subscribe one endpoint to many feeds at once. Supports "*" (all feeds in your tier) and group globs like "analytics.*". All feeds share one signing secret.
subscriptionsobject[] (one required)Per-feed subscriptions with their own filters: [{ feedId, filters }]. Use instead of feedIds when different feeds need different filters.
filtersobject (optional)Restrict which deliveries are sent. Supports drivers (TLAs), driverNumbers, constructors (team keywords), positions ({min,max}), and eventTypes (events.race only). Omit for all. Unknown keys or wrong value types are rejected with 400 invalid_filters. See the Webhooks guide for the full schema.
The webhookSecret is returned in the create response and used to verify signatures. It's retrievable anytime via GET /v1/webhooks/:webhookId/secret; rotate it with POST /v1/webhooks/:webhookId/rotate-secret.

List webhooks

GET/v1/webhooks

Returns your webhook endpoints, each carrying its per-feed subscriptions — one endpoint (URL + signing secret) can subscribe to many feeds. The list is paginated via limit (default 20, max 100) and offset query parameters.

json
# Response — endpoints with nested per-feed subscriptions, paginated
{
  "data": {
    "endpoints": [
      {
        "webhookId": "wh_7a3b9c2d",
        "webhookUrl": "https://your-app.com/f1",
        "webhookMethod": "post",
        "seriesId": "f1",
        "active": true,
        "subscriptionTier": "developer",
        "feedCount": 2,
        "subscriptions": [
          { "feedId": "timing.data",    "filters": { "driverNumbers": ["1", "4"] }, "active": true },
          { "feedId": "session.status", "filters": {},                              "active": true }
        ],
        "createdAt": "2026-06-01T12:00:00Z",
        "updatedAt": "2026-06-01T12:00:00Z"
      }
    ],
    "total": 1,
    "limit": 20,
    "offset": 0
  }
}

Update a webhook

PATCH/v1/webhooks/:webhookId
FieldTypeDescription
activeboolean (optional)Pause (false) or resume (true) deliveries to this endpoint (all feeds).
webhookUrlstring (optional)Move the endpoint to a new URL. Applies to all of its feed subscriptions.
webhookMethodstring (optional)"post" (default) or "put".
filtersobject (optional)Endpoint-wide filters, applied to every feed. To change one feed's filters use PATCH /v1/webhooks/:webhookId/feeds/:feedId.

Delete a webhook

DELETE/v1/webhooks/:webhookId

Delivery logs

GET/v1/webhooks/:webhookId/logs
json
# GET /v1/webhooks/:webhookId/logs?limit=50
{
  "data": {
    "logs": [
      {
        "logId": "log_a1b2c3",
        "webhookId": "wh_7a3b9c2d",
        "statusCode": 200,
        "latencyMs": 142,
        "createdAt": "2026-06-01T14:22:31Z",
        "responseBody": "ok"
      }
    ]
  }
}
FieldTypeDescription
limitinteger (query)Number of log entries to return. Default 50, max 500.
cursorstring (query)Pagination cursor from a previous response for the next page.

Send a test delivery

POST/v1/webhooks/:webhookId/test

Sends a sample payload to the registered endpoint. Useful for verifying your endpoint is reachable and signature verification is working.

Rotate webhook secret

POST/v1/webhooks/:webhookId/rotate-secret

Generates a new webhook secret. The new secret takes effect immediately. Retrieve it anytime via GET /v1/webhooks/:webhookId/secret.

Feeds

GET/v1/feeds

Returns all subscribable feeds. Each feed carries its feedId, name, seriesId, and availability — the feed's access class (free, live, or analytics), which maps to the plan required to subscribe: free feeds are open to every plan, live feeds require the Developer plan, and analytics feeds require the Custom plan. See the feed catalog for cadences and payload schemas.

json
# GET /v1/feeds
{
  "data": {
    "feeds": [
      {
        "feedId": "timing.data",
        "name": "Timing Data",
        "availability": "live",
        "seriesId": "f1"
      },
      {
        "feedId": "race-control.messages",
        "name": "Race Control Messages",
        "availability": "live",
        "seriesId": "f1"
      }
    ],
    "total": 53,
    "limit": 50,
    "offset": 0
  }
}

Simulate

Replay a session against your registered webhooks. Self-serve tiers cover current-season F1 sessions; the full historical archive is on Custom. Sessions are unlocked into lifetime slots — check yours with GET /v1/simulate/quota.

Browse replayable sessions

GET/v1/simulate/sessions

Check your simulate slots

GET/v1/simulate/quota

Simulate is gated by lifetime unique-session slots (Free 5, Developer 10, Custom unlimited) — a slot is consumed the first time you deliver a newsession's replay, and re-running an already-unlocked session is free. Watching a replay in the console never consumes a slot.

json
# GET /v1/simulate/quota
{
  "limit": 10,
  "used": 3,
  "remaining": 7,
  "unlocked": ["9560", "2026-great-britain_r"],
  "currentSeasonOnly": true
}
FieldTypeDescription
limitnumberLifetime unique-session slots for your tier (-1 = unlimited on Custom).
usednumberDistinct sessions you have unlocked so far.
remainingnumberSlots left before you hit the cap.
unlockedstring[]Session ids you have already unlocked — re-running any of these is free.
currentSeasonOnlybooleantrue on self-serve tiers (current-season sessions only); false on Custom, which unlocks the full historical archive.
Unlocking a new session past your cap returns 429; requesting an out-of-season session on a self-serve tier returns 403 — the full historical archive is on Custom.

Prepare a session

POST/v1/simulate/prepare
FieldTypeDescription
sessionIdstring (required)Session id from GET /v1/simulate/sessions. The playback artifact is generated once and cached.

Check readiness

GET/v1/simulate/status?session=:sessionId

Play back

GET/v1/simulate/stream?session=:sessionId&speed=10&sinks=webhook
FieldTypeDescription
speednumber (optional)Playback speed multiplier, up to 10.
sinksstring (optional)"visual" (SSE to you, default) | "webhook" (deliver to your registered webhooks, detached) | "both" (SSE + webhooks in lockstep).
bash
# 1. Prepare the session's playback artifact (cached after first build)
curl -X POST https://api.racehooks.io/v1/simulate/prepare \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{ "sessionId": "9560" }'

# 2. Check readiness
curl "https://api.racehooks.io/v1/simulate/status?session=9560" \
  -H "Authorization: Bearer ${TOKEN}"

# 3. Play it back into your registered webhooks (detached, run-to-completion)
curl "https://api.racehooks.io/v1/simulate/stream?session=9560&speed=10&sinks=webhook" \
  -H "Authorization: Bearer ${TOKEN}"

# sinks=visual streams SSE to you instead; sinks=both does both in lockstep

Stop a delivery

DELETE/v1/simulate/stream?runId=:runId

Stops a detached (sinks=webhook) replay. The runId comes from the stream response. Returns 200 when the run stops immediately, or 202 while the stop propagates across instances (a few seconds). Events already delivered stay delivered. A simulate slot is consumed when you first unlock a session; stopping a run does not free the slot, and re-running an already-unlocked session is free.

Delivery volume during replays

A replay re-delivers every recorded event for the feeds each endpoint is subscribed to — a full race is tens of thousands of events, and the speed multiplier compresses them into a shorter window (10× ≈ a full race's events in ~9 minutes). Each event is POSTed to every active endpoint, so total attempts = events × endpoints. Free capture bins (webhook.site and similar) rate-limit far below this throughput and will drop or 429 mid-replay; for clean samples use speed=1 with a single endpoint, or point the replay at a receiver sized for bursts. Every delivery carries X-RaceHooks-Replay-* headers so a receiver can tell replay traffic from live and group or discard a whole run. An endpoint that fails many deliveries in a row (errors, timeouts, or any non-2xx response — including 429s from an overwhelmed receiver) is paused for that run: deliveries stop, a single probe goes out every few seconds, and deliveries resume automatically once the endpoint responds successfully. The standing webhook subscription is never affected.

Demo session: replaying the curated demo race delivers the full Analytics feed set to all of your active endpoints, regardless of your tier or per-endpoint feed subscriptions — it is the platform showcase and is exempt from the replay quota. Expect the highest event volume there, even on the Free tier. Every other session delivers only your tier's feeds and each endpoint's own subscriptions.

Events

GET/v1/events

Returns the race calendar for all supported sports with sessions and their statuses. Currently returns F1 season data. Use it to detect when live sessions are starting; for replayable session ids use GET /v1/simulate/sessions.

json
# GET /v1/events
{
  "data": {
    "events": [
      {
        "eventId": "2026-great-britain",
        "name": "British Grand Prix",
        "circuitName": "Silverstone",
        "round": 12,
        "sessions": [
          {
            "sessionId": "2026-great-britain_q",
            "type": "Qualifying",
            "startDate": "2026-07-05T14:00:00Z",
            "status": "upcoming"
          },
          {
            "sessionId": "2026-great-britain_r",
            "type": "Race",
            "startDate": "2026-07-06T14:00:00Z",
            "status": "upcoming"
          }
        ]
      }
    ]
  }
}

Predictions

GET/v1/preview/upcoming
GET/v1/preview/:sessionId

Session previews with real model output — win probability, qualifying probability, expected strategy, circuit intelligence, and championship form for upcoming scoring sessions. Available to everytier, including Free (a headline "light" view; paid tiers get the full object). Full field reference on the Predictions page.

Fantasy

Fantasy points and pit-stop time leaderboards for a race weekend. All three endpoints require a paid plan — Developer tier or higher; on the Free tier they return 403. Scores use RaceHooks' estimated fantasy model (finishing position, qualifying, positions gained, fastest lap, teammate comparison, and retirements), with a full per-driver breakdown on every entry. See the Fantasy guide for the scoring tables and a build-an-app walkthrough.

Race scores

GET/v1/fantasy/races/:raceId/scoresDEVELOPER+

Estimated fantasy points per driver for a completed session. Defaults to the Grand Prix; pass ?session=sprint to score the Sprint (compressed points table). The response sessionType (grand_prix | sprint) echoes which was scored.

FieldTypeDescription
raceIdstring (path)Race id slug, e.g. 2026-italy-r16.
sessionstring (query)"race" (default) = Grand Prix; "sprint" = Sprint race.
bash
# Estimated fantasy points for a completed race (Grand Prix by default)
curl "https://api.racehooks.io/v1/fantasy/races/2026-italy-r16/scores" \
  -H "Authorization: Bearer ${TOKEN}"

# Score the Sprint instead (compressed points table)
curl "https://api.racehooks.io/v1/fantasy/races/2026-italy-r16/scores?session=sprint" \
  -H "Authorization: Bearer ${TOKEN}"

# Response — sorted highest total first, each with a full breakdown
{
  "data": {
    "raceId": "2026-italy-r16",
    "sessionType": "grand_prix",
    "scores": [
      {
        "driver": "verstappen-max", "tla": "VER", "team": "Red Bull Racing",
        "breakdown": {
          "racePosPoints": 25, "qualiPosPoints": 10, "q3Bonus": 3,
          "positionsGained": 0, "positionsGainedPoints": 0,
          "fastestLapPoints": 5, "beatTeammateRace": 3, "beatTeammateQuali": 2,
          "dnfPenalty": 0, "total": 48
        }
      }
    ]
  }
}

Live in-session scores

GET/v1/fantasy/session/:sessionId/scoresDEVELOPER+

A running, provisional score for each driver while a session is live, from the current running order and retirements. The response carries live and provisional flags plus currentLap / totalLaps. Position, teammate and DNF components update live; grid-derived and fastest-lap components stay null until the session ends. Returns live: false with an empty scores list and a message when no session is live for the id.

Pit-lane leaderboard

GET/v1/fantasy/session/:sessionId/pit-timesDEVELOPER+

Pit-lane times for a session ranked fastest to slowest, with the fastest stop broken out. Returns { sessionId, source, fastestStop, stops }; source is live during the session and historical afterward.

Usage

Delivery statistics for the authenticated client. All usage data resets at the start of each calendar month.

Summary

GET/v1/usage
FieldTypeDescription
deliveryCountnumberTotal webhook deliveries in the current billing window.
successCountnumberDeliveries that received a 2xx response from your endpoint.
failureCountnumberDeliveries that received a non-2xx response or timed out.
windowStartstringISO 8601 timestamp for the start of the current billing window.

Breakdown by feed

GET/v1/usage/by-feed

Returns per-feed delivery counts. Useful for understanding which feeds are generating the most traffic.

FieldTypeDescription
byFeed[].feedIdstringFeed identifier.
byFeed[].deliveryCountnumberTotal deliveries for this feed in the current window.

Latency percentiles

GET/v1/usage/latency

Returns p50, p95, and p99 delivery latency per feed, measured from when the F1 event was detected to when your endpoint acknowledged it.

FieldTypeDescription
byFeed[].feedIdstringFeed identifier.
byFeed[].p50numberMedian delivery latency in milliseconds.
byFeed[].p95number95th percentile latency in milliseconds.
byFeed[].p99number99th percentile latency in milliseconds.
byFeed[].sampleCountnumberNumber of deliveries included in the sample.

Plans & limits

Enforced limits per plan. Creating past the endpoint or feeds-per-endpoint limit returns 402; the API rate limit returns429 with X-RateLimit-* headers. The monthly delivery bucket works differently: on Free, deliveries beyond it areskippedas a hard backstop (your endpoint stops receiving events until the month rolls over); on Developer, delivery continues and overage bills at $3/1,000 up to your spend cap. You're warned by email as you approach the bucket — monitorGET /v1/usage. Remaining simulate slots are at GET /v1/simulate/quota.

LimitFreeDeveloper
Webhook endpoints325
Feeds per endpoint6Unlimited
Deliveries per month5,00050,000 + $3/1k
API requests per hour6001,800
Simulate sessions (lifetime)510
Replay session archiveCurrent seasonCurrent season
gRPC streaming
ML analytics enrichment

Every plan includes HMAC-signed deliveries. Simulate is gated by your lifetime unique-session slots (see GET /v1/simulate/quota), separate from the monthly delivery bucket — see the Simulate section above for replay delivery volume.

Error codes

All error responses return a JSON body with error (human-readable), code (machine-readable), and statusCode fields.

StatusTitleDescription
400Bad RequestInvalid request body or missing required fields. Check the error message for specifics.
401UnauthorizedMissing or invalid Bearer token. Re-authenticate to get a new token.
402Payment RequiredA plan limit was reached (webhook endpoints, feeds per endpoint) or the feed requires a higher tier.
403ForbiddenValid token but insufficient tier for this resource (e.g. an analytics feed on Free, or an out-of-season simulate session on a self-serve tier — the full historical archive is on Custom).
404Not FoundThe requested resource (webhook, simulation, etc.) does not exist or belongs to another account.
409ConflictA conflicting operation is already in progress — e.g. a replay is already running, or a session artifact is still being prepared.
429Too Many RequestsRate limit exceeded (see the X-RateLimit-Remaining header — back off and retry), or your lifetime simulate session cap is reached (GET /v1/simulate/quota).
503Service UnavailableStripe Checkout/Portal not configured. Contact support.
← Analytics referenceQuestions? Contact support →