FormulaAPI reference
FormulaAPI is a single REST + WebSocket surface over Formula 1, Formula 2, Formula 3, and F1 Academy. The same response shapes are used across all four series — the only thing that changes is the series slug in the URL.
Overview
All REST endpoints are mounted under /v1. Series are addressed by lowercase slug: f1, f2, f3, f1a. Every resource has a stable ULID-style publicId.
Quickstart
Request a key, set the Authorization header, you're done — no SDK install, no setup. The same key works for REST and WebSocket.
# 1. Request an API key # email hello@codai.app with your project name + intended use # 2. Verify it's live curl https://api.formulaapi.codai.app/v1/health # 3. First call — F2 2026 driver standings curl -H "Authorization: Bearer $FORMULA_API_KEY" \ "https://api.formulaapi.codai.app/v1/series/f2/seasons/2026/standings/drivers" | jq '.standings[:3]' # 4. Cross-series career for a single driver curl -H "Authorization: Bearer $FORMULA_API_KEY" \ "https://api.formulaapi.codai.app/v1/drivers/<driverPublicId>/results" | jq # 5. Subscribe to live timing (Indie plan and up) wscat -c "wss://api.formulaapi.codai.app/v1/live?key=$FORMULA_API_KEY" > {"subscribe":"session:f2:<sessionPublicId>"}
Authentication
REST requests require Authorization: Bearer <your_api_key>. The WebSocket endpoint accepts the same key via a ?key= query parameter on the upgrade request. Live timing is available on all paid plans. Email hello@codai.app to request a key.
curl https://api.formulaapi.codai.app/v1/series \ -H "Authorization: Bearer $FORMULA_API_KEY"
Plans & rate limits
| Plan | REST | Live timing | Support | Price |
|---|---|---|---|---|
| Indie | 500k req / month · 600 req/min | 100 session-hours / month | email · 48h response | €29 / mo |
| Pro | 5M req / month · 3000 req/min | 2000 session-hours / month | email · 24h response | €149 / mo |
| Enterprise | custom, dedicated pool | unlimited | shared Slack · 99.9% SLA | contact |
Rate limits are sliding-window per minute and rolling per month. Hit either and you'll get HTTP 429 with error: rate_limit_exceeded and aRetry-After header. Email hello@codai.app to request a key.
Public IDs
Every entity in the API is addressed by a stable publicId — a lowercase ULID (26 base32 characters). They are URL-safe, k-sortable by creation time, and never change. Use them to identify drivers, teams, rounds, sessions and circuits across calls.
Versioning
The current version is v1; all endpoints are mounted under that prefix. Breaking changes ship a new version prefix (/v2, …) — /v1 is supported for at least 12 months after a successor lands. Additive changes (new optional fields, new endpoints, new frame kinds) ship in-place under /v1.
Series
The 4 series currently served: f1, f2, f3, f1a.
Seasons & calendar
The calendar response groups sessions under their parent round and includes circuit metadata where available. Excerpt from a real F2 2026 round:
{
"rounds": [
{
"publicId": "01krvntynhrz6wcp1a27d23p9h",
"roundNumber": 1,
"name": "Melbourne",
"startDate": "2026-03-06",
"endDate": "2026-03-08",
"circuit": {
"name": "Melbourne",
"country": "AU"
},
"sessions": [
{
"publicId": "01krvntzq4e47ttxa7ctmsf5p5",
"type": "practice",
"scheduledAt": "2026-03-05T23:00:00.000Z",
"status": "finished"
},
{
"publicId": "01krvntzy1me92vr4mg6kjpe00",
"type": "qualifying",
"scheduledAt": "2026-03-06T03:55:00.000Z",
"status": "finished"
},
{
"publicId": "01krvntynrbydcb4dksq4sqkgr",
"type": "sprint",
"scheduledAt": "2026-03-07T03:30:00.000Z",
"status": "finished"
},
{
"publicId": "01krvntyny78e3tc9fzn8xd52c",
"type": "feature_race",
"scheduledAt": "2026-03-08T00:25:00.000Z",
"status": "finished"
}
]
}
]
}Session types
The type field on a session is one of: practice, fp1,fp2, fp3, qualifying, sprint_qualifying,sprint, race, feature_race,pre_qualifying, warm_up.
Session status
scheduled, live, finished, cancelled, postponed.
Standings
{
"standings": [
{
"position": 1,
"points": "35.00",
"wins": 0,
"podiums": 0,
"driver": {
"publicId": "01krvntz0xa2xqbr4h68m0kyza",
"firstName": "Nikola",
"lastName": "Tsolov",
"abbreviation": "TSO",
"nationality": null
},
"team": {
"publicId": "01krvntz16gspmn70dwgzbvze6",
"name": "Campos Racing",
"color": null
}
}
]
}Sessions
A session is the atomic unit — a single on-track activity (practice, qualifying, sprint, race, etc.). All sessions are addressed by their publicId, independent of series.
Result status
Each result row has a status: finished, dnf,dns, dsq, nc, or lapped. Results may be marked isProvisional: true while a session is in progress — once stewards finalise the classification, the row is rewritten withisProvisional: false.
{
"results": [
{
"position": 1,
"gridPosition": null,
"points": "0.00",
"lapsCompleted": 25,
"fastestLap": false,
"fastestLapTimeMs": 116041,
"gapToLeaderMs": null,
"status": "finished",
"isProvisional": false,
"driver": {
"publicId": "01krvntz2c41tc6jv9rnp3tdm7",
"firstName": "Gabriele",
"lastName": "Minì",
"abbreviation": "MIN"
},
"team": {
"publicId": "01krvntz2ma833ga4exp8bwj8j",
"name": "MP Motorsport",
"color": null
}
}
]
}Drivers & teams
Driver entities span every series: a single publicId follows a driver from F3 to F2 to F1 and beyond. /v1/drivers/:publicId/resultsreturns up to 500 of the most recent session results across all series the driver has competed in.
WebSocket /v1/live
Connect to /v1/live, authenticate via ?key=<api_key>, then send subscribe messages for the sessions you want to follow. The server pushes one JSON frame per event with a millisecond timestamp; ordering within a session is guaranteed.
Connection
| Endpoint | Auth | Plans |
|---|---|---|
| wss://api.formulaapi.codai.app/v1/live | ?key=<api_key> | Indie, Pro, Enterprise |
Client messages
| Message | Effect |
|---|---|
| {"subscribe":"session:<series>:<publicId>"} | Subscribe to a session's live frames. The session:<series>: prefix is optional — bare publicId also works. |
| {"unsubscribe":"<publicId>"} | Stop receiving frames for a session. |
Server messages
Acknowledgements: {"subscribed":"<publicId>"} and {"unsubscribed":"<publicId>"}. Errors: {"error":"unknown_session","sessionId":"..."} or {"error":"invalid_json"}. Live frames follow the shape below.
const ws = new WebSocket( `wss://api.formulaapi.codai.app/v1/live?key=${process.env.FORMULA_API_KEY}` ); ws.onopen = () => { // topic format: "session:<series>:<sessionPublicId>" — bare publicId also works ws.send(JSON.stringify({ subscribe: 'session:f2:01krvntypycbqew4vs9rs3qhm2' })); }; ws.onmessage = (event) => { const frame = JSON.parse(event.data); // { series, sessionPublicId, ts, kind, payload } if (frame.kind === 'timing') { // per-driver position, lap, sectors, gap console.log(frame.payload); } else if (frame.kind === 'race_control') { notifyBroadcast(frame.payload); } };
Frame kinds
Every live frame on the stream has the same envelope —{ series, sessionPublicId, ts, kind, payload } — and thekind field determines what's inside payload.
| Kind | Frequency | Payload |
|---|---|---|
| session_status | on change | Status transitions (scheduled → live → finished). |
| session_clock | periodic | Server-authoritative remaining-time / lap-of clock. |
| timing | continuous | Per-driver timing snapshot (position, current lap, last lap, sectors). |
| position | on change | Driver position + gap to leader. |
| lap_time | per lap | Completed lap with sector splits. |
| sector | per sector | Sector split with purple/green/yellow flag. |
| pit | on event | Pit-in / pit-out, tire change, stop duration. |
| weather | periodic | Air & track temp, humidity, wind, rain. |
| track_status | on change | Green / yellow / SC / VSC / red. |
| race_control | on event | Stewards notes, penalties, investigations. |
| team_radio | on event | Driver↔pit radio transcripts (where licensed). |
Errors
Error responses are JSON with a stable error code. Common codes:not_found, series_not_found, invalid_json,unknown_session, missing_api_key, invalid_api_key,rate_limit_exceeded.
{
"error": "not_found"
}