API reference
One endpoint, one response shape.
Authenticate with a key, pass a flight number and a date, read the resolved record. Everything below is generated from the route handlers themselves.
https://flight-api.dev
All timestamps are ISO 8601 in UTC. Responses are always JSON and always carry a boolean success discriminator.
Machine-readable
Every artifact below is generated from the same source as this page, so it cannot describe a different API.
Every path, parameter, schema and status code. Feed it to a client generator or an agent toolchain.
The whole reference as one structured document, including the worked examples on this page.
A short, link-first index for agents — start here and follow the links.
No credential is needed to read any of these. Point an MCP client with fetch capability at the index and it can walk the rest itself.
{
"mcpServers": {
"fetch": {
"command": "uvx",
"args": [
"mcp-server-fetch"
]
}
}
}Then: “read https://flight-api.dev/llms.txt and use it to call the flight status endpoint”.
Authentication
Keys are hashed with SHA-256 and compared in constant time. The plaintext is shown once, at creation.
Every key is prefixed flt_. Requests without a credential get a 401; a disabled key or account gets 403; an account with no active plan gets 402. Manage keys from the dashboard.
Metering
One credit per authorised request. The rules below are the whole contract — there is nothing else to model.
At authorisation, before the resolve runs. A miss, a cache replay and a full live resolve all cost exactly one credit.
Rejected credentials (401), disabled keys and accounts (403), GET /api/v1/account — deliberately unmetered so checking a balance never moves it — and GET /api/v1/airports, which is a static catalogue. Webhook deliveries are never metered either: you are billed for lookups, not for being told about a flight.
A 400 still costs a credit — the key was already authorised. The usage log records it against the same key so the ledger and the meter agree.
Without overage the next request is refused with 429 and no credit is taken. With overage enabled the request is served and billed at the plan rate.
Allowances run in 30-day windows that roll from the previous window, not from the first request of the month. Unused credits do not carry over.
Overage is metered as it happens and invoiced in arrears: one invoice at the start of each month covering the month before, charged automatically. A month without overage is not charged. The plan fee bills separately on its own cycle.
/api/v1/flights/statusAPI keyResolve a historical flight
Resolves one flight on one date and returns the selected record. Settled disruptions are replayed from storage.
IATA flight designator. Trimmed and upper-cased server-side.
aliases: flight, number
Scheduled departure date in UTC. A full ISO timestamp is truncated to the date.
aliases: date
Disambiguates codeshares and repeated flight numbers.
aliases: originIata, from
Disambiguates codeshares and repeated flight numbers.
aliases: destinationIata, dest, to
Skip the persisted record and resolve the flight again from scratch.
Attach a rendered proof image of the record as base64. Never persisted.
aliases: includeScreenshot
Boolean flags accept "1" · "0" · "true" · "false" · "yes" · "no".
curl -sG "https://flight-api.dev/api/v1/flights/status" \
-H "Authorization: Bearer $FLIGHT_API_KEY" \
--data-urlencode "flightNumber=BA249" \
--data-urlencode "flightDate=2026-08-14"const params = new URLSearchParams({
flightNumber: 'BA249',
flightDate: '2026-08-14',
})
const response = await fetch(`https://flight-api.dev/api/v1/flights/status?${params}`, {
headers: { Authorization: `Bearer ${process.env.FLIGHT_API_KEY}` },
})
const { success, data, message } = await response.json()
if (!success) throw new Error(message)
// data.selected is the resolved record.
console.log(data.selected?.status, data.selected?.delayMinutes, data.meta.cached)Response
A 200 always means the resolve completed — it does not guarantee the flight was found.
- flightNumber string
- Echoed back, normalised to upper case.
- flightDate string
- Echoed back as `YYYY-MM-DD`.
- originIata string | null
- Resolved departure airport.
- destinationIata string | null
- Resolved arrival airport.
- selected object | null
- The normalised record, including on-time flights. Null only when nothing was found.
- flightDelay object | null
- Delay and disruption verdict with its rationale, when timings were available.
- meta object
- Timing, cache and persistence state.
- found boolean
- The flight was matched.
- complete boolean
- Enough timings were present to settle the record.
- status 'delayed' | 'cancelled' | 'diverted' | 'on_time' | 'not_found'
- Classified outcome.
- isDelayed boolean
- Arrival delay greater than zero.
- isCancelled boolean
- Flight never operated.
- isDiverted boolean
- Landed somewhere other than the destination.
- delayMinutes number | null
- Arrival delay in minutes.
- departureDelayMinutes number | null
- Departure delay in minutes.
- scheduledDeparture string | null
- ISO 8601, UTC.
- scheduledArrival string | null
- ISO 8601, UTC.
- estimatedDeparture string | null
- ISO 8601, UTC.
- estimatedArrival string | null
- ISO 8601, UTC.
- actualDeparture string | null
- ISO 8601, UTC.
- actualArrival string | null
- ISO 8601, UTC.
- airline object | null
- `name`, `iata`, `icao`.
- aircraft object | null
- `registration`, `iata`, `icao`, `icao24`, `label`.
- flightIata string | null
- Normalised IATA designator.
- flightIcao string | null
- Normalised ICAO designator.
- statusRaw string | null
- Raw status string, unmodified.
- durationMs number
- Wall clock for the whole resolve. `0` on a cache hit.
- startedAt string
- ISO 8601, UTC.
- finishedAt string
- ISO 8601, UTC.
- cached boolean
- Served from the persisted record instead of a live resolve.
- persisted boolean
- This response was written to the record table.
- recordId string | null
- Always null on the public endpoint.
{
"success": true,
"data": {
"flightNumber": "BA249",
"flightDate": "2026-08-14",
"originIata": "LHR",
"destinationIata": "GRU",
"selected": {
"found": true,
"complete": true,
"status": "delayed",
"isDelayed": true,
"isCancelled": false,
"isDiverted": false,
"delayMinutes": 47,
"departureDelayMinutes": 39,
"originIata": "LHR",
"destinationIata": "GRU",
"scheduledDeparture": "2026-08-14T21:40:00.000Z",
"scheduledArrival": "2026-08-15T05:05:00.000Z",
"estimatedDeparture": null,
"estimatedArrival": null,
"actualDeparture": "2026-08-14T22:19:00.000Z",
"actualArrival": "2026-08-15T05:52:00.000Z",
"airline": {
"name": "British Airways",
"iata": "BA",
"icao": "BAW"
},
"aircraft": {
"registration": "G-ZBKF",
"iata": "789",
"icao": "B789",
"icao24": null,
"label": "Boeing 787-9"
},
"flightIata": "BA249",
"flightIcao": "BAW249",
"source": null,
"statusRaw": "Landed 05:52"
},
"flightDelay": {
"delayMinutes": 47,
"delayFormatted": "47 min late",
"statusKind": "landed",
"disruption": {
"type": "delay",
"isCancelled": false,
"isDiverted": false,
"neverArrived": false,
"arrivalDelayMinutes": 47,
"departureDelayMinutes": 39,
"confidence": "high",
"sources": [
"ata_vs_sta"
],
"rationale": [
"Actual arrival 47 min after scheduled arrival"
]
}
},
"meta": {
"durationMs": 0,
"startedAt": "2026-08-24T09:12:04.881Z",
"finishedAt": "2026-08-24T09:12:04.881Z",
"source": null,
"cached": true,
"persisted": true,
"recordId": null
},
"sources": []
}
}Caching
Only outcomes that can no longer change are frozen, so a cached reply is never stale.
Delayed, cancelled or diverted, complete, and departing more than three UTC days ago. Stored unique on flight number and date, then replayed with meta.cached true and durationMs 0.
On-time and not-found results, anything inside the three-day settle window, and screenshots.
Bypasses the stored record and resolves from scratch. The fresh answer overwrites the row in place when it is still persistable.
Errors
Failures carry a human-readable message; validation errors name the field.
- 200
- Resolved. `data.selected` may still be null when the flight was not found.
- 400
- Query validation failed. `message` names the offending field.
- 401
- Missing or invalid API key.
- 402
- No active plan on the account, or the subscription is canceled or past due. No credit is taken.
- 403
- Key or account disabled.
- 429
- Request allowance for the current window is exhausted and overage is not enabled. No credit is taken.
- 500
- Resolve failed. Safe to retry.
{
"success": false,
"message": "flightDate: flightDate must be YYYY-MM-DD"
}/api/v1/airportsAPI keyList the airports you can subscribe to
The airports whose arrival and departure boards we poll, and therefore the only values a webhook subscription accepts. Unmetered — this call costs nothing.
These are the boards we actually poll, so they are the only values a webhook subscription accepts. Read it at startup rather than hard-coding the list — it grows.
| IATA | Airport | City | Country | Timezone |
|---|---|---|---|---|
| OTP | Bucharest Otopeni | Bucharest | Romania | Europe/Bucharest |
| CLJ | Cluj-Napoca | Cluj-Napoca | Romania | Europe/Bucharest |
| RMO | Chișinău | Chișinău | Moldova | Europe/Chisinau |
| TIA | Tirana | Tirana | Albania | Europe/Tirane |
| BUD | Budapest | Budapest | Hungary | Europe/Budapest |
| MLA | Malta | Luqa | Malta | Europe/Malta |
| BCM | Bacău | Bacău | Romania | Europe/Bucharest |
| VIE | Vienna | Vienna | Austria | Europe/Vienna |
| CDG | Paris Charles de Gaulle | Paris | France | Europe/Paris |
| LTN | London Luton | London | United Kingdom | Europe/London |
| VCE | Venice Marco Polo | Venice | Italy | Europe/Rome |
| BCN | Barcelona El Prat | Barcelona | Spain | Europe/Madrid |
| CPH | Copenhagen | Copenhagen | Denmark | Europe/Copenhagen |
| BHX | Birmingham | Birmingham | United Kingdom | Europe/London |
| FCO | Rome Fiumicino | Rome | Italy | Europe/Rome |
| ATH | Athens | Athens | Greece | Europe/Athens |
| MAN | Manchester | Manchester | United Kingdom | Europe/London |
| ORY | Paris Orly | Paris | France | Europe/Paris |
| WRO | Wrocław | Wrocław | Poland | Europe/Warsaw |
| BSL | Basel Mulhouse EuroAirport | Basel/Mulhouse | France | Europe/Paris |
| BLQ | Bologna | Bologna | Italy | Europe/Rome |
| ALC | Alicante | Alicante | Spain | Europe/Madrid |
| LGW | London Gatwick | London | United Kingdom | Europe/London |
| MXP | Milan Malpensa | Milan | Italy | Europe/Rome |
| BRI | Bari | Bari | Italy | Europe/Rome |
| NAP | Naples | Naples | Italy | Europe/Rome |
| MAD | Madrid Barajas | Madrid | Spain | Europe/Madrid |
| VLC | Valencia | Valencia | Spain | Europe/Madrid |
| ARN | Stockholm Arlanda | Stockholm | Sweden | Europe/Stockholm |
| BGY | Milan Bergamo | Bergamo | Italy | Europe/Rome |
| NCE | Nice Côte d’Azur | Nice | France | Europe/Paris |
| SKP | Skopje | Skopje | North Macedonia | Europe/Skopje |
| LIS | Lisbon | Lisbon | Portugal | Europe/Lisbon |
| CRL | Brussels South Charleroi | Charleroi | Belgium | Europe/Brussels |
| CTA | Catania Fontanarossa | Catania | Italy | Europe/Rome |
| AGP | Málaga | Málaga | Spain | Europe/Madrid |
| BTS | Bratislava | Bratislava | Slovakia | Europe/Bratislava |
| KRK | Kraków | Kraków | Poland | Europe/Warsaw |
| KTW | Katowice | Katowice | Poland | Europe/Warsaw |
| BRU | Brussels | Brussels | Belgium | Europe/Brussels |
| LCA | Larnaca | Larnaca | Cyprus | Asia/Nicosia |
| WAW | Warsaw Chopin | Warsaw | Poland | Europe/Warsaw |
| CPO | Copiapó Desierto de Atacama | Copiapó | Chile | America/Santiago |
| HRK | Kharkiv | Kharkiv | Ukraine | Europe/Kyiv |
| LPL | Liverpool John Lennon | Liverpool | United Kingdom | Europe/London |
| OSL | Oslo Gardermoen | Oslo | Norway | Europe/Oslo |
| WMI | Warsaw Modlin | Warsaw | Poland | Europe/Warsaw |
| LBA | Leeds Bradford | Leeds | United Kingdom | Europe/London |
| POZ | Poznań Ławica | Poznań | Poland | Europe/Warsaw |
| PSA | Pisa Galileo Galilei | Pisa | Italy | Europe/Rome |
| CIA | Rome Ciampino | Rome | Italy | Europe/Rome |
| STN | London Stansted | London | United Kingdom | Europe/London |
curl -sS "https://flight-api.dev/api/v1/airports" \
-H "Authorization: Bearer $FLIGHT_API_KEY"your endpointSignedAirport board events
The push side of the API. Register an https endpoint against one or more of the airports above and every flight observed on their boards is POSTed to you — new rows, delays, cancellations, diversions. Boards are read every 30 minutes and queued events go out within 2. Deliveries never touch the credit meter. Register from the dashboard, or read the full walkthrough.
A schedule row appeared on a watched arrival or departure board for the first time. One per flight per direction — this is the event that carries every newly fetched flight, on time included.
The flight crossed from on time into delayed. Fires on the transition, not on every re-estimate.
The board now reports the flight as cancelled. Fires once.
The flight is landing somewhere other than its filed destination. `data.flight.divertedTo` carries the airport when the board names one.
A flight that was delayed, cancelled or diverted is back to on time. Rare, and worth handling — it is how a false-positive disruption unwinds.
The estimated time shifted by at least a whole minute. Subscribe to this only if you track a delay as it grows; `flight.delayed` alone is enough to know a flight is late.
The board's own status text changed — "Scheduled" to "Estimated 14:35" to "Landed 14:41". Highest volume of the set.
The diversion airport was set, changed or cleared.
A passenger flight crossed into compensation territory: a departure cancelled, diverted or delayed 3h+, or an arrival delayed 3h+. Cargo, military and business-jet movements never fire, nor do cancelled arrivals — their passengers are not at this airport. Once per flight per board. `data.qualification` carries the reason and the estimated compensation.
The aircraft due to operate this departure is running late on an earlier leg, far enough that it cannot turn around in time to leave less than 3h late — usually before the airline or airport publishes a delay. `data.prediction` carries the tail, the inbound leg and the projected delay. Once per flight; follow up with `flight.qualifying` when the board confirms it.
- id string
- Delivery id. Unique per endpoint — use it as your idempotency key.
- event string
- Board event id. Identical across every endpoint that received this change.
- type string
- One of the event types below.
- createdAt string
- When the change was observed. ISO 8601, UTC.
- webhook object
- The subscription that matched: `id`, `name`, and the `airports` it selected.
- airport object
- The watched airport this board belongs to: `iata`, `name`, `city`, `country`, `timezone`.
- data object
- The flight and its previous observation. `flight.qualifying` adds `qualification`; `flight.predicted_qualifying` adds `prediction`.
- id string
- Stable across every event for this flight on this board.
- flightNumber string
- IATA designator as the board prints it.
- callsign string | null
- ATC callsign, when the board carries one.
- direction 'departure' | 'arrival'
- Which board of the watched airport this row came from.
- airline object
- `name` from the board, `iata` from the designator.
- aircraftType string | null
- ICAO type code, e.g. `B738`.
- registration string | null
- Tail number, once the aircraft has been looked up. Set on most qualifying and predicted flights.
- operation 'passenger' | 'cargo' | 'military' | 'private'
- Freighters, air-force movements and business jets share the board with airliners. Only `passenger` flights can owe EU261 compensation — filter on it if you consume the full stream.
- origin object
- `{ iata }`. Equals `airport.iata` on a departure. `iata` is null when the board does not name the other end.
- destination object
- `{ iata }`. Equals `airport.iata` on an arrival. `iata` is null when the board does not name the other end.
- divertedTo object | null
- `{ iata }` when the board names a diversion airport.
- scheduledAt string
- ISO 8601, UTC.
- estimatedAt string | null
- ISO 8601, UTC. Null until the board estimates.
- delayMinutes number | null
- Estimated minus scheduled, in whole minutes. Negative when early.
- status string
- The board's own status text, unmodified.
- disruption 'scheduled' | 'on_time' | 'delayed' | 'cancelled' | 'diverted'
- Normalised verdict. `scheduled` means the board has no estimate yet, so nothing can be claimed.
- isCancelled boolean
- The board reports the flight as cancelled.
- isDiverted boolean
- The board reports a diversion.
- runway string | null
- When published.
- firstSeenAt string
- When this row first appeared. ISO 8601, UTC.
- lastSeenAt string
- When it was last observed. Use it to discard an out-of-order delivery.
- status string | null
- Board status text before the change.
- estimatedAt string | null
- ISO 8601, UTC.
- delayMinutes number | null
- Delay before the change.
- disruption string | null
- Verdict before the change.
- reason 'cancelled' | 'diverted' | 'delayed'
- Arrivals only ever qualify as `delayed` — a cancelled arrival’s passengers never reach this airport.
- delayMinutes number | null
- The delay that crossed the line. Null for a cancellation.
- detectedAt string
- When the board first showed it. ISO 8601, UTC.
- minutesBeforeScheduled number
- Scheduled time minus detection. Positive means the team still has that long before the flight was due.
- compensation object | null
- `{ amountEur, distanceKm, band }` — the EU261 amount for the route and delay. Null when either end of the route is unknown.
- registration string
- The tail due to operate this departure.
- projectedDepartureAt string
- Earliest the aircraft can realistically leave. ISO 8601, UTC.
- projectedDelayMinutes number
- Projected departure minus scheduled.
- confidence 'high' | 'medium'
- `high` when the inbound is airborne or has a published estimate. `medium` when it simply has not left yet — held to a 15-minute higher bar.
- minTurnaroundMinutes number
- Ground time assumed for this airframe class — the low end, so projections err short.
- inbound object
- `{ flightNumber, origin: { iata }, scheduledArrivalAt, projectedArrivalAt, status }` — the leg the aircraft is flying in on.
- detectedAt string
- When the prediction was made. ISO 8601, UTC.
- minutesBeforeScheduled number
- Scheduled departure minus detection.
- compensation object | null
- `{ amountEur, distanceKm, band }` — the EU261 amount for the route and delay. Null when either end of the route is unknown.
{
"id": "whd_7Qw1pKcRr8mF3xTz",
"event": "clz9k2f9a0004qw2h8vjd1m0p",
"type": "flight.delayed",
"createdAt": "2026-09-03T11:42:07.318Z",
"webhook": {
"id": "clz8k2f9a0002qw2h",
"name": "ops",
"airports": [
"OTP",
"CLJ"
]
},
"airport": {
"iata": "OTP",
"name": "Bucharest Otopeni",
"city": "Bucharest",
"country": "Romania",
"timezone": "Europe/Bucharest"
},
"data": {
"flight": {
"id": "clz9k1v2b0001qw2h",
"flightNumber": "RO301",
"callsign": "ROT301",
"direction": "departure",
"airline": {
"name": "TAROM",
"iata": "RO"
},
"aircraftType": "B738",
"registration": null,
"operation": "passenger",
"origin": {
"iata": "OTP"
},
"destination": {
"iata": "LHR"
},
"divertedTo": null,
"scheduledAt": "2026-09-03T12:50:00.000Z",
"estimatedAt": "2026-09-03T13:37:00.000Z",
"delayMinutes": 47,
"status": "Estimated departure 16:37",
"disruption": "delayed",
"isCancelled": false,
"isDiverted": false,
"runway": null,
"firstSeenAt": "2026-09-03T05:12:44.010Z",
"lastSeenAt": "2026-09-03T11:42:07.301Z"
},
"previous": {
"status": "Scheduled 15:50",
"estimatedAt": null,
"delayMinutes": null,
"disruption": "scheduled"
}
}
}import { createHmac, timingSafeEqual } from 'node:crypto'
const TOLERANCE_SECONDS = 300
export function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)))
const timestamp = Number(parts.t)
if (!Number.isFinite(timestamp)) return false
// Bound the age first: the timestamp is inside the MAC, so an attacker
// cannot backdate a captured request to keep replaying it.
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
const a = Buffer.from(parts.v1 ?? '')
const b = Buffer.from(expected)
return a.length === b.length && timingSafeEqual(a, b)
}The literal request body, prefixed with the timestamp and a dot. Sign the raw bytes you received — re-serialising the parsed JSON will not match.
It is inside the MAC, so it cannot be edited. Reject anything more than five minutes old and a captured request stops being replayable.
Compare in constant time. A byte-by-byte early exit leaks the signature one character at a time.
Rotating issues a new secret immediately; the old one stops verifying at once. Rotate from the dashboard when you are ready to deploy the new value.
Boards are polled every 30 minutes and every observed change is queued in the same pass. An event reaches you within a delivery cycle of the poll that saw it.
One delivery row exists per event and endpoint, unique in the database. A retry re-sends the same `id`; a redeploy or an overlapping pass cannot produce a second one. Key your handler on `id` anyway — that is what it is for.
Any 2xx inside 10 seconds. Redirects are not followed — a 3xx is a failure, so point the webhook at the final URL. The response body is discarded.
Up to 5 attempts, backing off 1, 5, 20, 60 minutes. After the last one the delivery is marked failed and is not retried again.
25 consecutive failures across all deliveries disables the endpoint and stops the queue. Any 2xx — including a manual test send — resets the counter.
Deliveries are queued in event order but sent concurrently, so they can arrive out of order. Every payload carries `data.flight.lastSeenAt`; use it to discard an older observation.
A webhook only receives the airports it selected. Adding an airport to the catalogue does not widen an existing subscription — you have to select it.
/api/v1/accountAPI keyInspect the calling key
Returns the account and key behind the supplied credential, its lifetime request count and the credit position for the current window. Unmetered — this call costs nothing.
{
"success": true,
"data": {
"accountId": "acct_9f3c1d",
"accountSlug": "acme-9f3c1d",
"keyId": "clz8k2f9a0002qw2h",
"keyName": "production",
"keyPrefix": "flt_0Rk2Xa9c",
"unlimited": false,
"requestCount": 1284
}
}- 200
- Key is valid.
- 401
- Missing or invalid API key.
- 402
- No active plan, or the subscription is canceled or past due.
- 403
- Key or account disabled.
/api/healthPublicLiveness probe
Unauthenticated. Returns immediately.
{
"ok": true,
"service": "flight-api"
}