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:

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.

IntervalApplies to
1sThe next match result, from a minute before it's due until it posts. Alliances during alliance selection.
3sMatches, score details, rankings and hybrid schedules at a running event. Awards once playoffs start.
10sEverything else at a running event.
1m – 5mEvent days outside match hours.
10m – 12hSeason lists, upcoming events, events that ended in the last 14 days.
neverEvents 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-ModifiedFIRST's own value when it sends one, otherwise when we saw the data change.
ETagChanges whenever the content does.
If-Modified-SinceReturns 304 if nothing changed, like FIRST.
If-None-MatchReturns 304 if the ETag still matches.
FMS-OnlyModifiedSinceSupported for the 304. When something did change you get the full response, not just the changed rows.
x-firstapi-sourcememory 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-timingHow long we spent on the key check, the data lookup and the whole request, in ms.
x-firstapi-changedWhen 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.

TypeSent when
match.resultA match result posts for the first time. data.match is the object from the matches endpoint.
match.updatedA posted result changes (score correction, replay).
match.scoresScore details for a match post or change. data.scores.
schedule.updatedMatches are added to or changed in the schedule. data.added, data.changed.
rankings.updatedEvent rankings change. data is the full rankings response.
alliances.updatedAlliances change.
alliances.selectionFTC alliance selection steps change.
awards.updatedAwards are given out. data.awards holds the new ones.
teams.updatedThe team list for an event changes.
event.updatedAn event's details change or a new event appears in the season.
resource.updatedAny 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/meYour account and keys.
GET /v1/webhooksList webhooks.
POST /v1/webhooksCreate one. Body: url, program, optional season, eventCode, team, types (array, default ["*"]), description. The response includes the secret, shown only this once.
PATCH /v1/webhooks/:idChange any of the above, or active.
DELETE /v1/webhooks/:idDelete it.
POST /v1/webhooks/:id/testSend a ping right now and get the result back.
POST /v1/webhooks/:id/rotateIssue a new secret.
GET /v1/webhooks/:id/deliveriesThe last 100 delivery attempts.
GET /v1/events/:program/:season/:codeHow we're polling an event: state, next match, expected post time, and every endpoint's interval.
GET /v1/statusPoller status (public).


    

Notes