Quick start
This is a cache of the official Events API. Paths, parameters and responses are the same, so you change the base URL and the key and nothing else.
Already using FIRST's API? Replace https:// with and put your key from the dashboard where your FIRST authorization key used to go.
Authentication
Bearer (recommended)
Send your key as a bearer token. Use this for anything new.
Authorization: Bearer fapi_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Basic (FIRST compatible)
FIRST uses Basic base64(username:authorizationKey). We accept the same shape with your key as the password, so existing clients only need new credentials. The username is ignored; your FIRST username or anything else is fine.
Authorization: Basic base64("yourname:fapi_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
Missing or wrong credentials get a 401, same as FIRST.
Endpoints
Every endpoint in FIRST's API works at the same path. FIRST's own reference is the source of truth for what each returns:
- FTC (
v2.0): ftc-events.firstinspires.org/api-docs · basehttps://ftcapi.org - FRC (
v3.0): frc-api-docs.firstinspires.org · basehttps://frcapi.org
Both programs also work on either domain with a prefix, e.g. /ftc/v2.0/… and /frc/v3.0/….
Anything we haven't seen before is fetched from FIRST on the first request (that one is as slow as FIRST), stored, and kept fresh from then on.
Freshness
The poller tracks every event's schedule and results, works out when the next result should post, and polls hardest around that moment. FMS doesn't sync while a match is running, so there's nothing to fetch until the score is revealed.
| Interval | Applies to |
|---|---|
| 1s | The next match result, from a minute before it's due until it posts. Alliances during alliance selection. |
| 3s | Matches, score details, rankings and hybrid schedules at a running event. Awards once playoffs start. |
| 10s | Everything else at a running event. |
| 1m – 5m | Event days outside match hours. |
| 10m – 12h | Season lists, upcoming events, events that ended in the last 14 days. |
| never | Events finished more than 14 days ago, and past seasons. |
When a result posts we immediately refresh that event's scores, rankings and hybrid schedule as well, so they don't lag behind.
Headers & caching
| Header | |
|---|---|
| Last-Modified | FIRST's own value when it sends one, otherwise when we saw the data change. |
| ETag | Changes whenever the content does. |
| If-Modified-Since | Returns 304 if nothing changed, like FIRST. |
| If-None-Match | Returns 304 if the ETag still matches. |
| FMS-OnlyModifiedSince | Supported for the 304. When something did change you get the full response, not just the changed rows. |
| x-firstapi-source | memory or edge (served from the Cloudflare data center you hit), hit (our database), derived (filtered from a cached copy), miss (fetched from FIRST just now) or stale. |
| server-timing | How long we spent on the key check, the data lookup and the whole request, in ms. |
| x-firstapi-changed | When this data last changed, ISO 8601. |
CORS is open, so you can call the API straight from a browser. Don't ship your key in public client code though.
Webhooks
Get a POST when something changes instead of polling. Create webhooks in the dashboard or with the API below. Scope a webhook to a whole program, a season, a single event and/or a team.
| Type | Sent when |
|---|---|
| match.result | A match result posts for the first time. data.match is the object from the matches endpoint. |
| match.updated | A posted result changes (score correction, replay). |
| match.scores | Score details for a match post or change. data.scores. |
| schedule.updated | Matches are added to or changed in the schedule. data.added, data.changed. |
| rankings.updated | Event rankings change. data is the full rankings response. |
| alliances.updated | Alliances change. |
| alliances.selection | FTC alliance selection steps change. |
| awards.updated | Awards are given out. data.awards holds the new ones. |
| teams.updated | The team list for an event changes. |
| event.updated | An event's details change or a new event appears in the season. |
| resource.updated | Any polled endpoint changes. Noisy, so it isn't included in *; ask for it by name. |
Every delivery looks like this:
{
"id": "evt_1a2b3c…",
"type": "match.result",
"created": "2026-03-07T18:22:04.120Z",
"program": "ftc",
"season": 2026,
"eventCode": "USCAFFFAQ",
"resource": "/v2.0/2026/matches/USCAFFFAQ?matchNumber=12&tournamentLevel=qual",
"teams": [11115, 16236, 8565, 19456],
"data": { "tournamentLevel": "qual", "match": { … } }
}
Respond with any 2xx within 10 seconds. Failed deliveries are retried after 10s, 1m, 5m, 30m, 2h, 6h and 12h. A webhook that fails 25 times in a row is switched off; turn it back on from the dashboard. id is the same across retries, so use it to ignore duplicates.
Verifying signatures
Each request carries x-firstapi-signature: t=<ms>,v1=<hex>, where v1 is HMAC-SHA256 of t + "." + rawBody using your webhook secret.
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto.createHmac('sha256', secret).update(parts.t + '.' + rawBody).digest('hex');
const fresh = Math.abs(Date.now() - Number(parts.t)) < 5 * 60_000;
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
fresh = abs(time.time() * 1000 - int(parts["t"])) < 5 * 60_000
return fresh and hmac.compare_digest(expected, parts["v1"])
Management API
Everything the dashboard does is available under /v1 with your bearer key.
| GET /v1/me | Your account and keys. |
| GET /v1/webhooks | List webhooks. |
| POST /v1/webhooks | Create one. Body: url, program, optional season, eventCode, team, types (array, default ["*"]), description. The response includes the secret, shown only this once. |
| PATCH /v1/webhooks/:id | Change any of the above, or active. |
| DELETE /v1/webhooks/:id | Delete it. |
| POST /v1/webhooks/:id/test | Send a ping right now and get the result back. |
| POST /v1/webhooks/:id/rotate | Issue a new secret. |
| GET /v1/webhooks/:id/deliveries | The last 100 delivery attempts. |
| GET /v1/events/:program/:season/:code | How we're polling an event: state, next match, expected post time, and every endpoint's interval. |
| GET /v1/status | Poller status (public). |
Notes
- XML (
Accept: application/xml) is passed straight through to FIRST without caching. - FIRST's error responses (
400,404,501) are cached and returned as-is. If FIRST is down we serve the last good copy. - This service is unofficial and isn't affiliated with FIRST®.