Fantasy
The Fantasy API turns a RaceHooks race weekend into fantasy points. It gives you an estimated points breakdown per driver for any completed Grand Prix or Sprint, a running score while a session is on track, and a pit-lane leaderboard. All of it is keyed to the same canonical driver identity (TLA and team) as every other RaceHooks surface. Pair it with the drop-in fantasy scorer and you have a live leaderboard from race-event webhooks.
https://api.racehooks.io/v1401 (code: "unauthorized"); a valid key on the Free tier returns 403 (code: "forbidden", "requires the Developer plan or higher"). A season with no scoring ruleset returns 400 SEASON_NOT_SUPPORTED; today only 2026 is scored. See plans & limits for the full tier matrix, and the pricing page to upgrade.What you get
Four endpoints, all authenticated with a Bearer token and versioned under /v1:
| Field | Type | Description |
|---|---|---|
races/:raceId/scores | GET · Developer+ | Estimated fantasy points per driver for a completed race. Grand Prix by default; ?session=sprint scores the Sprint. Returns a full per-driver breakdown. |
session/:sessionId/scores | GET · Developer+ | Running (provisional) fantasy score for each driver while a session is live, computed from the current running order and retirements. |
prices | GET · Developer+ | RaceHooks-generated driver and constructor prices after a round (?season=2026&round=N). A season with no scoring ruleset returns 400 SEASON_NOT_SUPPORTED. Documented in the API reference. |
session/:sessionId/pit-times | GET · Developer+ | Pit-lane time leaderboard for a session, ranked fastest → slowest, with the fastest stop called out. |
The scoring model
The race and session endpoints return RaceHooks' own estimated fantasy model, a points system computed with the official F1 Fantasy 2026 rules: finishing position, qualifying, positions gained and lost, fastest lap, and retirements. Every entry ships the full breakdown so you can show your users exactly where each point came from, or re-map the components onto your own league's rules. (To score against official-style, DFS-style, or fully custom rule sets instead, use the fantasy scorer further down.)
Grand Prix finishing points
| Finish | Points |
|---|---|
| P1 | 25 |
| P2 | 18 |
| P3 | 15 |
| P4 | 12 |
| P5 | 10 |
| P6 | 8 |
| P7 | 6 |
| P8 | 4 |
| P9 | 2 |
| P10 | 1 |
| P11+ | 0 |
Sprint finishing points
A Sprint scores a compressed P1 to P8 table. Qualifying does not apply to a Sprint. Under the rules a Sprint fastest lap is worth +5, but it is not currently served: fastestLapPoints comes back null on a Sprint.
| Finish | Points |
|---|---|
| P1 | 8 |
| P2 | 7 |
| P3 | 6 |
| P4 | 5 |
| P5 | 4 |
| P6 | 3 |
| P7 | 2 |
| P8 | 1 |
| P9+ | 0 |
Qualifying points
Grand Prix scoring adds qualifying position points (P1 = 10 down to P10 = 1, −1 per place). The game has no driver Q2/Q3 bonus; the constructor qualifying bonus is returned in the race response under constructors (both cars in Q3 +10, one +5; both in Q2 +3, one +1; neither −1):
| Field | Type | Description |
|---|---|---|
qualiPosPoints | P1 to P10 | 10 points for pole, decreasing by one per place to 1 point at P10. P11+ scores 0. |
Bonuses & penalties
| Field | Type | Description |
|---|---|---|
positionsGainedPoints | +1 / place | One point for every position gained between the starting grid and the finish. |
positionsLostPoints | −1 / place | One point deducted for every position lost (a Sprint loss is capped at −10). |
fastestLapPoints | +10 | Awarded to the driver with the fastest race lap (a Sprint fastest lap is worth +5 under the rules, not currently served). |
dnfPenalty | −20 / −10 | Applied to a retirement or disqualification: −20 in the Grand Prix, −10 in a Sprint. |
Reading the breakdown object
Every driver entry carries a breakdown with the same shape on both the race and live endpoints. A component is null when it does not apply (Sprint, or a live session before the flag) and 0 when it applies but scored nothing.
| Field | Type | Description |
|---|---|---|
racePosPoints | integer | Finishing-position points from the table (Grand Prix or Sprint scale). |
qualiPosPoints | integer | null | Qualifying-position points. null when no qualifying result is available, or on a Sprint / live session. |
q3Bonus | integer | null | Always 0: the game has no driver Q2/Q3 bonus (the constructor bonus is under constructors). null when qualifying data is unavailable. |
positionsGained | integer | null | Raw places gained (grid - finish); negative when places were lost. null when grid or finish is unknown. |
positionsGainedStatus | string | Why positionsGained is what it is: scored = classified with a grid slot; not-classified = DNF/DNS/DSQ (the game scores -20/-25 instead of places, so positionsGained is null by rule); no-grid = grid unknown. |
qualiStatus | string | null | timed = set a qualifying time; no-time and disqualified both score the game's -5 in qualiPosPoints; null when no qualifying data. |
positionsGainedPoints | integer | Points for places gained: +1 per place (gains only). |
positionsLostPoints | integer | Points deducted for places lost: -1 per place, 0 when none were lost (a Sprint loss is capped at -10). |
fastestLapPoints | integer | null | +10 if this driver set the fastest race lap, else 0. null on a Sprint without captured fastest-lap timing, and on a live session. |
overtakes | integer | null | On-track overtakes credited to the driver in the session, counted from the stored timing-data overtake events. null when no overtake record exists for the session. |
overtakePoints | integer | null | Points for overtakes: +1 per overtake (overtakes x 1). null when overtakes is null. |
driverOfTheDayPoints | integer | Driver of the Day bonus (+10 for the winner of the fan vote), else 0. Always 0 on a Sprint. |
beatTeammateRace | integer | Deprecated: always 0. The game awards no beat-teammate points. |
beatTeammateQuali | integer | null | Deprecated: always 0 (null on a Sprint / live session). The game awards no beat-teammate points. |
dnfPenalty | integer | -20 on a Grand Prix DNF / DSQ, -10 in a Sprint, else 0. |
total | integer | Sum of every component above: the driver's fantasy score for the session. |
The race response also carries a constructors array (one entry per team) and, on a Grand Prix, pitSource (dhl or live-timing-approximate). A Sprint response carries constructors too: constructors score in a Sprint under the same rules, so each entry holds the team's Sprint points with qualifyingBonus and pitPoints null. A Sprint has no pitSource.
| Field | Type | Description |
|---|---|---|
constructorId | string | Canonical team slug: the constructor `id` in GET /v1/fantasy/prices. |
assetId | string | Same as constructorId. |
team | string | Constructor display name. |
qualifyingBonus | integer | null | Constructor qualifying bonus (both cars in Q3 +10, one +5; else both in Q2 +3, one +1, neither -1). null on a Sprint and when qualifying data is unavailable. |
pitPoints | integer | null | Constructor pit-stop points from the DHL award times; approximate until the round's DHL table is published (see pitSource). null on a Sprint. |
points | number | The constructor's whole-weekend points for the session scored: both drivers' points under the constructor rules, plus qualifyingBonus and pitPoints on a Grand Prix. A Sprint response carries the Sprint's constructor points. |
excludedFromDrivers | object | Terms in the drivers' totals that the game does not give a constructor: driverOfTheDayPoints (Driver of the Day is never a constructor score) and qualifyingDisqualificationExtra (a qualifying disqualification costs the constructor -10 against the driver's -5, so -5 here). |
Race scores
/v1/fantasy/races/:raceId/scoresDEVELOPER+Estimated fantasy points for a completed session, sorted highest total first. Pass ?session=sprint to score the Sprint instead of the Grand Prix; the response sessionType (grand_prix or sprint) echoes which was scored. If a race has no qualifying data yet, qualiPosPoints and q3Bonus come back null and a dataQuality note is included.
| Field | Type | Description |
|---|---|---|
raceId | string (path) | Race id, e.g. 2026-monza-r13. Take it from events[].raceId in GET /v1/events (slugs are not guessable; the Italian GP is 2026-monza-r13, not 2026-italy-r16). A race of an unsupported season returns 400 SEASON_NOT_SUPPORTED. |
session | string (query) | "race" (default) scores the Grand Prix; "sprint" scores the Sprint using the compressed points table. |
Unknown race, no sprint, not scored yet
Three different situations get three different answers, so you can tell a wrong id from a race that has not happened yet:
| Field | Type | Description |
|---|---|---|
404 RACE_NOT_FOUND | error | The id matches no race (a slug you guessed, such as 2026-japan_r, rather than events[].raceId). Take the id from GET /v1/events. |
404 SPRINT_NOT_AT_ROUND | error | ?session=sprint on a round that has no sprint (only sprint weekends do). |
200, scores: [] | result | A real race with no results yet: scores is empty and dataQuality says so. Retry after the session. |
The same envelope applies to session ids: /session/:sessionId/scores and /session/:sessionId/pit-times return 404 SESSION_NOT_FOUND for an id that matches no session (take it from events[].sessions[].sessionId). A real session that is not live, or has no stops yet, returns 200 with an empty list.
Live in-session scores
/v1/fantasy/session/:sessionId/scoresDEVELOPER+A running, provisional score for each driver while a session is on track, derived from the live running order and retirements. Position, teammate and DNF components update lap by lap; the grid-derived (positions gained, qualifying) and fastest-lap components are only finalised once the session ends; until then they are null. For the final breakdown, call the race scores endpoint after the flag.
| Field | Type | Description |
|---|---|---|
sessionId | string (path) | Session id, e.g. 2026-italy_r or 2026-italy_s. |
live | boolean | true while a session is genuinely live for this id; false (with an empty scores list and a message) otherwise. |
provisional | boolean | Always true for this endpoint. Scores are in-flight and will change. |
currentLap | integer | Current lap of the session. |
totalLaps | integer | Scheduled total laps. |
Push: the fantasy.scores feed
fantasy.scoresliveThe same provisional scores as the endpoint above, pushed to your webhook or SSE stream instead of polled. Both come from one scoring function, so a pushed frame and a polled response never disagree about a driver's points.
It emits on scoring change, not on a timer. A lap counter ticking over is not by itself a delivery. A position swap, a driver leading their teammate, or a retirement is. That keeps a race in the low hundreds of deliveries rather than the ~2,700 an every-tick feed would bill you for. Fires during Race and Sprint sessions only: practice and qualifying have no finishing-position table.
| Field | Type | Description |
|---|---|---|
feed | string | Always "fantasy.scores". |
sessionId | string | The session these scores belong to. This is the same id you would pass to the REST routes above. |
utc | string | When the running order behind these scores was observed (ISO-8601), not when the frame was serialised. |
live | boolean | Always true. The feed only fires during a running session. |
provisional | boolean | Always true. Grid-derived and fastest-lap components stay null until the flag; call the race scores endpoint after the session for the final breakdown. |
sessionType | string | "grand_prix" or "sprint". A sprint scores on the compressed points table. |
scores | array | Per-driver entries, sorted by total descending. Each carries a full driver reference and the same breakdown object the REST routes return. |
scores[].driver | object | Full driver reference: driverId, constructorId, number, tla, name, team. |
utc (and currentLap), keep the newest frame per session, and ignore an older one that arrives late.Pit-stop time leaderboard
/v1/fantasy/session/:sessionId/pit-timesDEVELOPER+Stationary pit-stop times for a session (the seconds the car is stopped in its box), ranked fastest to slowest, with the single fastest stop broken out as fastestStop. Stationary pit-stop time is the constructor scoring category in most fantasy games, so feed it straight into a team leaderboard. This is the same pit picture surfaced by the strategy & tyre feeds.
| Field | Type | Description |
|---|---|---|
sessionId | string (path) | Session id, e.g. 2026-italy_r. |
source | "live" | "historical" | Where the ranking came from: an in-progress session vs. the completed record. |
fastestStop | object | null | The fastest single stop of the session (same shape as a stops[] entry), or null when none is recorded. |
stops[].pitStopTimeSec | number | Stationary pit-stop time in seconds (car stopped in its box). stops[] also carries driver (id slug), tla, team, and lap. |
Build a fantasy app
For a live leaderboard driven by your own scoring rules, subscribe to the events.race feed and pipe its payloads into the open-source @racehooks/f1-fantasy-scorer package (installed from its GitHub repository). The scorer is zero-dependency, fully typed, and deterministic. Every point is logged with a reason code. It ships official-style, DFS-style, and custom rule sets, plus a budget-aware roster optimizer.
1. Install
The scorer installs straight from GitHub as @racehooks/f1-fantasy-scorer and builds itself on install. It is not on the npm registry, so npm install @racehooks/f1-fantasy-scorer will not resolve.
2. Subscribe a roster and wire the scorer
Create one events.race webhook filtered to your league's drivers, and point a FantasyScorer at the same roster:
3. Score deliveries as they arrive
Verify each delivery's signature, then hand the payload to scorer.ingest(). It routes each event to the right handler and finalises the classification automatically when the session completes:
events.race payloads it would in a live race, so you can validate a full weekend of scoring against a known result on a quiet afternoon.How race events map to scoring
The scorer draws its points from these events.race events. Two of them are inputs the scorer accepts but RaceHooks does not emit. They are marked below, and the scorer works without them (see events.ts in the scorer repository for the same note).
| Field | Type | Description |
|---|---|---|
driver_finished | event (not emitted) | Finishing-position points, plus positions gained or lost against the starting grid. RaceHooks emits no canonical driver_finished event; the scorer derives the final classification from the last timing positions when session.complete fires, so this is only needed if you synthesise one upstream. |
session.complete | event | Triggers final classification from the last observed order (the scorer's finalize step). |
overtake | event | Overtake points (handles cumulative overtake counts). |
fastest.lap | event | Fastest-lap bonus, for rule sets that award one. |
pit.stop.complete | event | Constructor pit-stop time points, plus fastest-stop and record-stop bonuses. |
retirement | event | DNF penalty, race vs. sprint aware. |
driver_of_the_day | event (not emitted) | Driver of the Day bonus. Not a live RaceHooks event: Driver of the Day is a post-race fan vote, added after the session rather than emitted on events.race. Supply it yourself if your rule set awards it. |