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 we expand beyond F1.
https://api.racehooks.io/v1Authentication
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.
/v1/oauth/v1/oauth/credentialsReturns { 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
/v1/webhooksProvide exactly one feed selection: feedId, feedIds, or subscriptions; omitting all three returns 400.
| Field | Type | Description |
|---|---|---|
webhookUrl | string (required) | The HTTPS endpoint to receive deliveries. Must be publicly reachable. |
feedId | string (one required) | Subscribe to a single feed (the simplest form). See the feed catalog for valid IDs. |
feedIds | string[] (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. |
subscriptions | object[] (one required) | Per-feed subscriptions with their own filters: [{ feedId, filters }]. Use instead of feedIds when different feeds need different filters. |
filters | object (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. |
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
/v1/webhooksReturns 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.
Update a webhook
/v1/webhooks/:webhookId| Field | Type | Description |
|---|---|---|
active | boolean (optional) | Pause (false) or resume (true) deliveries to this endpoint (all feeds). |
webhookUrl | string (optional) | Move the endpoint to a new URL. Applies to all of its feed subscriptions. |
webhookMethod | string (optional) | "post" (default) or "put". |
filters | object (optional) | Endpoint-wide filters, applied to every feed. To change one feed's filters use PATCH /v1/webhooks/:webhookId/feeds/:feedId. |
Delete a webhook
/v1/webhooks/:webhookIdDelivery logs
/v1/webhooks/:webhookId/logs| Field | Type | Description |
|---|---|---|
limit | integer (query) | Number of log entries to return, newest first. Default 50, max 200. |
offset | integer (query) | Entries to skip, for the next page. Default 0. |
Recent deliveries (all endpoints)
/v1/webhooks/deliveriesYour most recent deliveries across every endpoint, newest first, with a summary and a per-endpoint breakdown over the same window. retryCount counts retries: 0 means the first attempt landed. Read-only. To exercise an endpoint, use the test delivery below.
| Field | Type | Description |
|---|---|---|
mode | string (query) | "live", "replay" or "test". Omit for all. |
since | date-time (query) | Window start. Default the last 24 hours; never earlier than 7 days ago. |
limit | integer (query) | Rows to return. Default 50, max 200. The summary always covers the whole window. |
Send a test delivery
/v1/webhooks/:webhookId/testSends a sample payload to the registered endpoint. Useful for verifying your endpoint is reachable and signature verification is working.
Rotate webhook secret
/v1/webhooks/:webhookId/rotate-secretGenerates a new webhook secret. The new secret takes effect immediately. Retrieve it anytime via GET /v1/webhooks/:webhookId/secret.
Feeds
/v1/feedsReturns 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.
Simulate
Replay a session against your registered webhooks. Self-serve tiers cover current-season F1 sessions; the full historical archive is on Custom. Sessions use lifetime slots. Check yours with GET /v1/simulate/quota.
The control plane (sessions, quota, prepare, status) is on api.racehooks.io with the usual Authorization: Bearer header. Playback itself (GET/DELETE /v1/simulate/stream) is served from simulate.racehooks.io and takes the same token as a ?token= query parameter.
Browse replayable sessions
/v1/simulate/sessions| Field | Type | Description |
|---|---|---|
year | string (optional) | Four-digit year, e.g. 2026. |
type | string (optional) | "race" | "qualifying" | "sprint" | "practice". |
limit | integer (optional) | 1–500, default 200. |
offset | integer (optional) | Default 0. |
Returns { sessions, total, limit, offset, restrictedToSeason }. Each session carries sessionId, sessionName, eventId, eventName, circuitId, year, type, startDate, and analyticsPreview (true for the curated demo race). On self-serve tiers restrictedToSeason names the season the list is clamped to; the curated demo race is always included.
Check your simulate slots
/v1/simulate/quotaSimulate is gated by lifetime unique-session slots (Free 5, Developer 10, Custom unlimited). A slot is consumed the first time you deliver a new session's replay, and re-running an already-used session is free. Watching a replay in the console never consumes a slot.
| Field | Type | Description |
|---|---|---|
limit | number | Lifetime unique-session slots for your tier (-1 = unlimited on Custom). |
used | number | Distinct sessions you have used so far. |
remaining | number | Slots left before you hit the cap. |
unlocked | string[] | Session ids you have already used. Re-running any of these is free. |
currentSeasonOnly | boolean | true on self-serve tiers (current-season sessions only); false on Custom, which includes the full historical archive. |
429; requesting an out-of-season session on a self-serve tier returns 403. The full historical archive is on Custom.Prepare a session
/v1/simulate/prepare| Field | Type | Description |
|---|---|---|
sessionId | string (required) | Session id from GET /v1/simulate/sessions. The playback artifact is generated once and cached. |
Returns { sessionId, modelVersion, profile, status: "preparing" } while the artifact builds, or the ready artifact. Unknown session ids return 404.
Check readiness
/v1/simulate/status?session=:sessionIdstatus is "ready" (open the stream), "missing" (call prepare), or "unknown" (no replayable session with that id).
Play back
https://simulate.racehooks.io/v1/simulate/stream?session=:sessionId&speed=10&sinks=webhook&token=:token| Field | Type | Description |
|---|---|---|
session | string (required) | Session id from GET /v1/simulate/sessions. |
token | string (required) | Your Bearer token from POST /v1/oauth, passed as a query parameter (the playback host does not read the Authorization header). |
speed | number (optional) | Playback speed multiplier, clamped to 0.1–120. Default 10. |
sinks | string (optional) | "visual" (default, SSE to you) | "webhook" (deliver to your registered webhooks, detached; returns 202 with a runId) | "both" (SSE + webhooks in lockstep). |
ackLive | string (optional) | Set to "1" to deliver a replay while a live session is in progress. Without it, a delivering replay returns 409 live-session-active during a live session. |
A sinks=webhook request returns 202 with { status: "delivering", runId, session, speed, modelVersion, hooks } and runs to completion on its own. Other sinks=webhook responses: 409 { status: "preparing" } with Retry-After if the artifact is still building (any sink); 409 live-session-active when a live session is running for a series your webhooks cover, unless ackLive=1 (also applies to sinks=both); 409 delivery-active if another replay is already delivering to your webhooks (one detached replay at a time; the body carries its runId); 422 if you have no active webhooks; 429 when a new session would exceed your slot cap; and 403 for an out-of-season session on a self-serve tier. With sinks=both, a slot-cap or season refusal does not fail the request; the SSE stream still plays and webhook delivery is withheld.
Stop a delivery
https://simulate.racehooks.io/v1/simulate/stream?runId=:runId&token=:tokenStops a detached (sinks=webhook) replay. The runId comes from the 202 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 replay a session; stopping a run does not free the slot, and re-running an already-used session is free.
Lost the runId? GET https://simulate.racehooks.io/v1/simulate/delivery?token=:token returns { active }: your in-flight detached delivery (with its runId and sessionId), or null.
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 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
/v1/eventsReturns 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. events[].raceId is the id the /v1/fantasy race routes take (it is not a guessable slug, e.g. the Italian GP is 2026-monza-r13); it is null for events with no race id (testing).
| Field | Type | Description |
|---|---|---|
season | integer (query) | Only events of this season (e.g. 2026). Omit for all seasons. year is accepted as an alias; if both are sent and differ the request is 400. |
upcoming | 1 (query) | Only events not yet completed. |
order | asc | desc (query) | Calendar order by weekend start (the first session's start), event id as the tiebreak. asc (default) is oldest first; events with no session start sort last in both directions. |
limit | integer (query) | Page size. Default 24, max 500. |
offset | integer (query) | Items to skip. For the next page, send the nextOffset from the previous response; it is null on the last page. |
Predictions
/v1/preview/upcoming/v1/preview/:sessionIdSession previews with real model output, win probability, qualifying probability, expected strategy, circuit intelligence, and championship form for upcoming scoring sessions. Available to every tier, including Free (a headline "light" view; Developer gets the win / podium / top-10 grid; Free and Developer probabilities are rounded to 5% with no position distributions; higher tiers get exact numbers and distributions). Full field reference on the Predictions page.
Fantasy
Fantasy points and pit-stop time leaderboards for a race weekend. All four endpoints require a paid plan: Developer tier or higher. A missing, invalid or expired key returns 401 (unauthorized); a valid key on the Free tier returns 403 (forbidden). Scores for a season with no scoring ruleset (today anything but 2026) return 400 SEASON_NOT_SUPPORTED. An id that matches no race or session returns 404 RACE_NOT_FOUND / SESSION_NOT_FOUND, and ?session=sprint on a round with no sprint returns 404 SPRINT_NOT_AT_ROUND; a real race with no results yet is 200 with empty scores. Sprint responses carry constructors too. The breakdown fields are listed in the Fantasy guide. 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
/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.
| Field | Type | Description |
|---|---|---|
raceId | string (path) | Race id from events[].raceId in GET /v1/events, e.g. 2026-monza-r13 (not a guessable slug). |
session | string (query) | "race" (default) = Grand Prix; "sprint" = Sprint race. |
Generated prices
/v1/fantasy/pricesDEVELOPER+RaceHooks-generated fantasy prices for budget-cap optimisers: each driver's and constructor's season-start price plus every price change since, from a fixed rule applied to RaceHooks' own fantasy points. Each asset returns startPrice, price after the chosen round, a per-round history, and nextRound thresholds: the weekend points that produce each of the four possible moves. Every response carries a label: "RaceHooks-generated estimate; may differ from the F1 Fantasy game". An unsupported season returns 400 SEASON_NOT_SUPPORTED; a round without completed results returns 404.
| Field | Type | Description |
|---|---|---|
season | integer (query, required) | Season year, e.g. 2026. |
round | integer (query) | Price after this round. Default: the latest completed round. |
history[].points | integer | null (response) | The asset's whole weekend points that round, which is what the price rule reads: for a driver, Grand Prix + Sprint (when the round has one) + qualifying-position points; for a constructor, both drivers' points + qualifying bonus + pit points. It is therefore not the race-only figure from /v1/fantasy/races/:raceId/scores. |
enteredRound | integer (response, mid-season entrants only) | The round an asset first appears, for a mid-season seat change. Its history starts here and startPrice is the price at that round, not a press start price. Absent for assets priced from round 1. |
inheritsPriceFrom | string (response, mid-season entrants only) | The asset id whose price this entrant inherited at enteredRound (the seat's price after the previous round). Absent for assets priced from round 1. |
Live in-session scores
/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
/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
/v1/usage| Field | Type | Description |
|---|---|---|
deliveryCount | number | Total webhook deliveries in the current billing window. |
successCount | number | Deliveries that received a 2xx response from your endpoint. |
failureCount | number | Deliveries that received a non-2xx response or timed out. |
windowStart | string | ISO 8601 timestamp for the start of the current billing window. |
Breakdown by feed
/v1/usage/by-feedReturns per-feed delivery counts. Useful for understanding which feeds are generating the most traffic.
| Field | Type | Description |
|---|---|---|
byFeed[].feedId | string | Feed identifier. |
byFeed[].deliveryCount | number | Total deliveries for this feed in the current window. |
Latency percentiles
/v1/usage/latencyReturns p50, p95, and p99 delivery latency per feed, measured from when the F1 event was detected to when your endpoint acknowledged it.
| Field | Type | Description |
|---|---|---|
byFeed[].feedId | string | Feed identifier. |
byFeed[].p50 | number | Median delivery latency in milliseconds. |
byFeed[].p95 | number | 95th percentile latency in milliseconds. |
byFeed[].p99 | number | 99th percentile latency in milliseconds. |
byFeed[].sampleCount | number | Number 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 returns 429 with X-RateLimit-* headers. The monthly delivery bucket works differently: on Free, deliveries beyond it are skipped as 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; monitor GET /v1/usage. Remaining simulate slots are at GET /v1/simulate/quota.
| Limit | Free | Developer |
|---|---|---|
| Webhook endpoints | 3 | 25 |
| Feeds per endpoint | 6 | Unlimited |
| Deliveries per month | 5,000 | 50,000 + $3/1k |
| API requests per hour | 600 | 1,800 |
| Simulate sessions (lifetime) | 5 | 10 |
| Replay session archive | Current season | Current 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.