# Flight API — Historical flight status API

Query any past flight by number and date. Actual departure and arrival times, block time, aircraft and delay minutes — one REST endpoint, one settled record.

- Base URL: `https://flight-api.dev`
- OpenAPI: `https://flight-api.dev/openapi.json`
- This document as JSON: `https://flight-api.dev/api/docs`

## Conventions

- All timestamps are ISO 8601 with a Z offset in UTC.
- Dates are `YYYY-MM-DD`.
- Every response is JSON carrying a boolean `success` discriminator, with `data` on success and `message` on failure.
- Boolean flags accept `1`, `0`, `true`, `false`, `yes`, `no`.

## Authentication

Every key is prefixed `flt_`. Send it in one of:

- `Authorization: Bearer flt_…` — Preferred.
- `X-Api-Key: flt_…` — Equivalent.
- `X-Flight-Api-Key: flt_…` — Legacy alias, still accepted.

Keys are stored as a SHA-256 hash; the plaintext is shown once, at creation. A missing credential returns `401`, a disabled key or account returns `403`, and an account with no active plan returns `402`.

## Metering

- **When it counts** — At authorisation, before the resolve runs. A miss, a cache replay and a full live resolve all cost exactly one credit.
- **What does not count** — Rejected credentials (401), disabled keys and accounts (403), and GET /api/v1/account, which is deliberately unmetered so checking a balance never moves it.
- **Validation failures** — 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.
- **At the ceiling** — 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.
- **Windows** — 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.
- **Billing the excess** — 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.

Every metered response carries the running position:

| Header | Type | Meaning |
| --- | --- | --- |
| `X-Credits-Included` | `integer` | Requests included in the current window. `unlimited` on internal accounts. |
| `X-Credits-Used` | `integer` | Requests consumed so far in the current window, including this one. |
| `X-Credits-Remaining` | `integer` | Requests left before the ceiling. Reaches zero one request before a 429. |
| `X-Credits-Overage` | `integer` | Requests taken beyond the included allowance. Zero unless overage is enabled. |
| `X-Credits-Period-End` | `string (ISO 8601)` | When the current window closes and the allowance resets. |

## Endpoints

### GET /api/v1/flights/status

Resolve a historical flight — Resolves one flight on one date and returns the selected record. Settled disruptions are replayed from storage.

Auth: API key. URL: `https://flight-api.dev/api/v1/flights/status`

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `flightNumber` | `string (2–12)` | yes | IATA flight designator. Trimmed and upper-cased server-side. Aliases: flight, number. |
| `flightDate` | `YYYY-MM-DD` | yes | Scheduled departure date in UTC. A full ISO timestamp is truncated to the date. Aliases: date. |
| `origin` | `IATA (3 letters)` | no | Disambiguates codeshares and repeated flight numbers. Aliases: originIata, from. |
| `destination` | `IATA (3 letters)` | no | Disambiguates codeshares and repeated flight numbers. Aliases: destinationIata, dest, to. |
| `refresh` | `boolean flag` | no | Skip the persisted record and resolve the flight again from scratch. |
| `screenshot` | `boolean flag` | no | Attach a rendered proof image of the record as base64. Never persisted. Aliases: includeScreenshot. |

| Status | Meaning |
| --- | --- |
| `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. |

### GET /api/v1/account

Inspect 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.

Auth: API key. URL: `https://flight-api.dev/api/v1/account`

| Status | Meaning |
| --- | --- |
| `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. |

### GET /api/health

Liveness probe — Unauthenticated. Returns immediately.

Auth: none (public). URL: `https://flight-api.dev/api/health`

| Status | Meaning |
| --- | --- |
| `200` | Service is up. |

## Response shape

### data

| Field | Type | Notes |
| --- | --- | --- |
| `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. Null 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. |

### data.selected

| Field | Type | Notes |
| --- | --- | --- |
| `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. |

### data.meta

| Field | Type | Notes |
| --- | --- | --- |
| `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. |

## Caching

- **Persisted** — 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.
- **Never persisted** — On-time and not-found results, anything inside the three-day settle window, and screenshots.
- **refresh=1** — Bypasses the stored record and resolves from scratch. The fresh answer overwrites the row in place when it is still persistable.

Settled records are frozen because the underlying flight can no longer change, so a cached reply is never stale.

## Examples

```bash
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"
```

```typescript
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)
```

```json
{
  "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": []
  }
}
```
