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 we expand 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&offset=0
{
  "data": {
    "logs": [
      {
        "logId": "log_a1b2c3",
        "webhookId": "wh_7a3b9c2d",
        "feedId": "timing.data",
        "mode": "live",
        "statusCode": 200,
        "retryCount": 0,
        "durationMs": 142,
        "errorMessage": null,
        "dead": false,
        "createdAt": "2026-06-01T14:22:31Z"
      }
    ],
    "total": 1,
    "limit": 50,
    "offset": 0
  }
}
FieldTypeDescription
limitinteger (query)Number of log entries to return, newest first. Default 50, max 200.
offsetinteger (query)Entries to skip, for the next page. Default 0.

Recent deliveries (all endpoints)

GET/v1/webhooks/deliveries

Your 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.

json
# GET /v1/webhooks/deliveries?mode=live&limit=50
{
  "data": {
    "rows": [
      {
        "logId": "log_a1b2c3",
        "createdAt": "2026-06-01T14:22:31Z",
        "feedId": "analytics.battle",
        "webhookId": "wh_7a3b9c2d",
        "url": "https://example.com/hooks/f1",
        "mode": "live",
        "statusCode": 200,
        "durationMs": 142,
        "retryCount": 0,
        "dead": false
      }
    ],
    "summary": { "sent": 1284, "ok": 1281, "retried": 4, "failed": 3, "p50Ms": 138, "p99Ms": 612, "truncated": false },
    "endpoints": [
      { "webhookId": "wh_7a3b9c2d", "url": "https://example.com/hooks/f1", "sent": 1284, "ok": 1281, "p50Ms": 138 }
    ],
    "since": "2026-05-31T14:22:31Z"
  }
}
FieldTypeDescription
modestring (query)"live", "replay" or "test". Omit for all.
sincedate-time (query)Window start. Default the last 24 hours; never earlier than 7 days ago.
limitinteger (query)Rows to return. Default 50, max 200. The summary always covers the whole window.

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": 56,
    "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 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

GET/v1/simulate/sessions
FieldTypeDescription
yearstring (optional)Four-digit year, e.g. 2026.
typestring (optional)"race" | "qualifying" | "sprint" | "practice".
limitinteger (optional)1–500, default 200.
offsetinteger (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

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 new session's replay, and re-running an already-used session is free. Watching a replay in the console never consumes a slot.

json
# GET /v1/simulate/quota
{
  "data": {
    "limit": 10,
    "used": 3,
    "remaining": 7,
    "unlocked": ["2026-australia_r", "2026-great-britain_r", "2026-italy_q"],
    "currentSeasonOnly": true
  }
}
FieldTypeDescription
limitnumberLifetime unique-session slots for your tier (-1 = unlimited on Custom).
usednumberDistinct sessions you have used so far.
remainingnumberSlots left before you hit the cap.
unlockedstring[]Session ids you have already used. Re-running any of these is free.
currentSeasonOnlybooleantrue on self-serve tiers (current-season sessions only); false on Custom, which includes the full historical archive.
ℹ
Replaying 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.

Returns { sessionId, modelVersion, profile, status: "preparing" } while the artifact builds, or the ready artifact. Unknown session ids return 404.

Check readiness

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

status is "ready" (open the stream), "missing" (call prepare), or "unknown" (no replayable session with that id).

Play back

GEThttps://simulate.racehooks.io/v1/simulate/stream?session=:sessionId&speed=10&sinks=webhook&token=:token
FieldTypeDescription
sessionstring (required)Session id from GET /v1/simulate/sessions.
tokenstring (required)Your Bearer token from POST /v1/oauth, passed as a query parameter (the playback host does not read the Authorization header).
speednumber (optional)Playback speed multiplier, clamped to 0.1–120. Default 10.
sinksstring (optional)"visual" (default, SSE to you) | "webhook" (deliver to your registered webhooks, detached; returns 202 with a runId) | "both" (SSE + webhooks in lockstep).
ackLivestring (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.

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": "2026-great-britain_r" }'

# 2. Check readiness (poll until status is "ready")
curl "https://api.racehooks.io/v1/simulate/status?session=2026-great-britain_r" \
  -H "Authorization: Bearer ${TOKEN}"

# 3. Play it back into your registered webhooks (detached, run-to-completion).
#    Playback is served from the simulate host and takes the token as a query param.
curl "https://simulate.racehooks.io/v1/simulate/stream?session=2026-great-britain_r&speed=10&sinks=webhook&token=${TOKEN}"
# → 202 { "status": "delivering", "runId": "...", "session": "...", "speed": 10, "modelVersion": "...", "hooks": 2 }

# 4. Stop it early (optional)
curl -X DELETE "https://simulate.racehooks.io/v1/simulate/stream?runId=${RUN_ID}&token=${TOKEN}"

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

Stop a delivery

DELETEhttps://simulate.racehooks.io/v1/simulate/stream?runId=:runId&token=:token

Stops 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

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. 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).

FieldTypeDescription
seasoninteger (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.
upcoming1 (query)Only events not yet completed.
orderasc | 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.
limitinteger (query)Page size. Default 24, max 500.
offsetinteger (query)Items to skip. For the next page, send the nextOffset from the previous response; it is null on the last page.
json
# GET /v1/events?season=2026&limit=2&offset=12   (abridged to one event)
{
  "data": {
    "events": [
      {
        "eventId": "2026-italy",
        "name": "Italian Grand Prix",
        "officialName": "Italian Grand Prix",
        "countryCode": "IT",
        "countryName": "Italy",
        "location": "Monza",
        "circuitName": "Autodromo Nazionale di Monza",
        "circuitId": "monza",
        "circuitOutline": null,
        "raceId": "2026-monza-r13",
        "state": "completed",
        "type": "race",
        "roundText": "13",
        "sessions": [
          {
            "sessionId": "2026-italy_q",
            "type": "q",
            "name": "Qualifying",
            "status": "completed",
            "startDate": "2026-09-05T14:00:00.000Z",
            "endDate": "2026-09-05T15:00:00.000Z",
            "gmtOffset": "+00:00",
            "path": "2026/2026-09-06_Italian_Grand_Prix/2026-09-05_Qualifying/"
          },
          {
            "sessionId": "2026-italy_r",
            "type": "r",
            "name": "Race",
            "status": "completed",
            "startDate": "2026-09-06T13:00:00.000Z",
            "endDate": "2026-09-06T15:00:00.000Z",
            "gmtOffset": "+00:00",
            "path": "2026/2026-09-06_Italian_Grand_Prix/2026-09-06_Race/"
          }
        ]
      }
    ],
    "total": 24,
    "limit": 2,
    "offset": 12,
    "nextOffset": 14
  }
}

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 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

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 from events[].raceId in GET /v1/events, e.g. 2026-monza-r13 (not a guessable slug).
sessionstring (query)"race" (default) = Grand Prix; "sprint" = Sprint race.
bash
# Race ids come from GET /v1/events (events[].raceId); they are not guessable slugs
# Estimated fantasy points for a completed race (Grand Prix by default)
curl "https://api.racehooks.io/v1/fantasy/races/2026-monza-r13/scores" \
  -H "Authorization: Bearer ${TOKEN}"

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

# Response: sorted highest total first, each with a full breakdown
# (abridged to one driver; values illustrative, field set is the live schema)
{
  "data": {
    "raceId": "2026-monza-r13",
    "sessionType": "grand_prix",
    "scores": [
      {
        "driver": "leclerc-charles", "tla": "LEC", "team": "Ferrari",
        "breakdown": {
          "racePosPoints": 25, "qualiPosPoints": 10, "q3Bonus": null,
          "positionsGained": 0, "positionsGainedPoints": 0, "positionsLostPoints": 0,
          "fastestLapPoints": 10, "overtakes": 3, "overtakePoints": 3,
          "driverOfTheDayPoints": 0, "beatTeammateRace": 0, "beatTeammateQuali": 0,
          "dnfPenalty": 0, "total": 51
        }
      }
    ],
    "constructors": [{ "team": "Ferrari", "qualifyingBonus": 0, "pitPoints": 4, "points": 61 }],
    "pitSource": "dhl"
  }
}

# A season with no scoring ruleset: 400, never another season's rules
{ "error": "No fantasy scores for season 2019. Supported: 2026.", "code": "SEASON_NOT_SUPPORTED", "statusCode": 400 }

Generated prices

GET/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.

FieldTypeDescription
seasoninteger (query, required)Season year, e.g. 2026.
roundinteger (query)Price after this round. Default: the latest completed round.
history[].pointsinteger | 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.
enteredRoundinteger (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.
inheritsPriceFromstring (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.
bash
# RaceHooks-generated prices after the latest completed round (or add &round=N)
curl "https://api.racehooks.io/v1/fantasy/prices?season=2026" \
  -H "Authorization: Bearer ${TOKEN}"

# Response (one entry per driver and constructor; abridged)
{
  "label": "RaceHooks-generated estimate; may differ from the F1 Fantasy game",
  "data": {
    "season": 2026, "round": 16, "latestRound": 16, "priceUnit": "USD millions",
    "drivers": [
      {
        "id": "norris-lando", "name": "Lando Norris", "team": "McLaren",
        "startPrice": 27.2, "price": 25.6,
        "history": [{ "round": 1, "points": 31, "price": 27.2, "change": 0 }],
        "nextRound": {
          "round": 17, "priceBefore": 25.6,
          "thresholds": [
            { "move": -0.3, "price": 25.3, "minPoints": null, "maxPoints": 14, "reachable": true },
            { "move": -0.1, "price": 25.5, "minPoints": 15, "maxPoints": 23, "reachable": true },
            { "move": 0.1, "price": 25.7, "minPoints": 24, "maxPoints": 30, "reachable": true },
            { "move": 0.3, "price": 25.9, "minPoints": 31, "maxPoints": null, "reachable": true }
          ]
        }
      }
    ],
    "constructors": [ /* same shape */ ]
  }
}

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 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.

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.
401UnauthorizedNo Authorization header, or the Bearer token is unknown or expired (body code: unauthorized). Re-authenticate to get a new token. This is also what a missing key gets on tier-gated routes such as Fantasy.
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 (body code: forbidden; e.g. the Fantasy endpoints on Free, which need Developer or higher, 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: a replay is already delivering to your webhooks (delivery-active), a live session is running (live-session-active; pass ackLive=1 to deliver anyway), 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 →