{
  "format": "1.0",
  "product": {
    "name": "Flight API",
    "tagline": "Historical flight status API",
    "description": "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.",
    "baseUrl": "https://flight-api.dev",
    "documentation": "https://flight-api.dev/docs"
  },
  "authentication": {
    "type": "api-key",
    "keyPrefix": "flt_",
    "headers": [
      {
        "header": "Authorization",
        "value": "Bearer flt_…",
        "note": "Preferred."
      },
      {
        "header": "X-Api-Key",
        "value": "flt_…",
        "note": "Equivalent."
      },
      {
        "header": "X-Flight-Api-Key",
        "value": "flt_…",
        "note": "Legacy alias, still accepted."
      }
    ],
    "storage": "Keys are stored as a SHA-256 hash and compared in constant time. The plaintext is shown once, at creation."
  },
  "conventions": {
    "timezone": "UTC",
    "dateFormat": "YYYY-MM-DD",
    "timestampFormat": "ISO 8601 with a Z offset",
    "envelope": "Every response is JSON carrying a boolean `success` discriminator, with `data` on success and `message` on failure.",
    "settleWindowDays": 3,
    "booleanFlagValues": [
      "1",
      "0",
      "true",
      "false",
      "yes",
      "no"
    ]
  },
  "metering": {
    "rules": [
      {
        "label": "When it counts",
        "rule": "At authorisation, before the resolve runs. A miss, a cache replay and a full live resolve all cost exactly one credit."
      },
      {
        "label": "What does not count",
        "rule": "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."
      },
      {
        "label": "Validation failures",
        "rule": "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."
      },
      {
        "label": "At the ceiling",
        "rule": "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."
      },
      {
        "label": "Windows",
        "rule": "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."
      },
      {
        "label": "Billing the excess",
        "rule": "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."
      }
    ],
    "responseHeaders": [
      {
        "header": "X-Credits-Included",
        "type": "integer",
        "description": "Requests included in the current window. `unlimited` on internal accounts."
      },
      {
        "header": "X-Credits-Used",
        "type": "integer",
        "description": "Requests consumed so far in the current window, including this one."
      },
      {
        "header": "X-Credits-Remaining",
        "type": "integer",
        "description": "Requests left before the ceiling. Reaches zero one request before a 429."
      },
      {
        "header": "X-Credits-Overage",
        "type": "integer",
        "description": "Requests taken beyond the included allowance. Zero unless overage is enabled."
      },
      {
        "header": "X-Credits-Period-End",
        "type": "string (ISO 8601)",
        "description": "When the current window closes and the allowance resets."
      }
    ]
  },
  "endpoints": [
    {
      "id": "flight-status",
      "method": "GET",
      "path": "/api/v1/flights/status",
      "title": "Resolve a historical flight",
      "summary": "Resolves one flight on one date and returns the selected record. Settled disruptions are replayed from storage.",
      "auth": "api-key",
      "params": [
        {
          "name": "flightNumber",
          "aliases": [
            "flight",
            "number"
          ],
          "type": "string (2–12)",
          "required": true,
          "description": "IATA flight designator. Trimmed and upper-cased server-side."
        },
        {
          "name": "flightDate",
          "aliases": [
            "date"
          ],
          "type": "YYYY-MM-DD",
          "required": true,
          "description": "Scheduled departure date in UTC. A full ISO timestamp is truncated to the date."
        },
        {
          "name": "origin",
          "aliases": [
            "originIata",
            "from"
          ],
          "type": "IATA (3 letters)",
          "description": "Disambiguates codeshares and repeated flight numbers."
        },
        {
          "name": "destination",
          "aliases": [
            "destinationIata",
            "dest",
            "to"
          ],
          "type": "IATA (3 letters)",
          "description": "Disambiguates codeshares and repeated flight numbers."
        },
        {
          "name": "refresh",
          "type": "boolean flag",
          "description": "Skip the persisted record and resolve the flight again from scratch."
        },
        {
          "name": "screenshot",
          "aliases": [
            "includeScreenshot"
          ],
          "type": "boolean flag",
          "description": "Attach a rendered proof image of the record as base64. Never persisted."
        }
      ],
      "statuses": [
        {
          "code": 200,
          "meaning": "Resolved. `data.selected` may still be null when the flight was not found."
        },
        {
          "code": 400,
          "meaning": "Query validation failed. `message` names the offending field."
        },
        {
          "code": 401,
          "meaning": "Missing or invalid API key."
        },
        {
          "code": 402,
          "meaning": "No active plan on the account, or the subscription is canceled or past due. No credit is taken."
        },
        {
          "code": 403,
          "meaning": "Key or account disabled."
        },
        {
          "code": 429,
          "meaning": "Request allowance for the current window is exhausted and overage is not enabled. No credit is taken."
        },
        {
          "code": 500,
          "meaning": "Resolve failed. Safe to retry."
        }
      ],
      "url": "https://flight-api.dev/api/v1/flights/status"
    },
    {
      "id": "airports",
      "method": "GET",
      "path": "/api/v1/airports",
      "title": "List the airports you can subscribe to",
      "summary": "The airports whose arrival and departure boards we poll, and therefore the only values a webhook subscription accepts. Unmetered — this call costs nothing.",
      "auth": "api-key",
      "statuses": [
        {
          "code": 200,
          "meaning": "The current catalogue."
        },
        {
          "code": 401,
          "meaning": "Missing or invalid API key."
        },
        {
          "code": 402,
          "meaning": "No active plan, or the subscription is canceled or past due."
        },
        {
          "code": 403,
          "meaning": "Key or account disabled."
        }
      ],
      "url": "https://flight-api.dev/api/v1/airports"
    },
    {
      "id": "account",
      "method": "GET",
      "path": "/api/v1/account",
      "title": "Inspect the calling key",
      "summary": "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",
      "statuses": [
        {
          "code": 200,
          "meaning": "Key is valid."
        },
        {
          "code": 401,
          "meaning": "Missing or invalid API key."
        },
        {
          "code": 402,
          "meaning": "No active plan, or the subscription is canceled or past due."
        },
        {
          "code": 403,
          "meaning": "Key or account disabled."
        }
      ],
      "url": "https://flight-api.dev/api/v1/account"
    },
    {
      "id": "health",
      "method": "GET",
      "path": "/api/health",
      "title": "Liveness probe",
      "summary": "Unauthenticated. Returns immediately.",
      "auth": "none",
      "statuses": [
        {
          "code": 200,
          "meaning": "Service is up."
        }
      ],
      "url": "https://flight-api.dev/api/health"
    }
  ],
  "schemas": {
    "data": [
      {
        "name": "flightNumber",
        "type": "string",
        "description": "Echoed back, normalised to upper case."
      },
      {
        "name": "flightDate",
        "type": "string",
        "description": "Echoed back as `YYYY-MM-DD`."
      },
      {
        "name": "originIata",
        "type": "string | null",
        "description": "Resolved departure airport."
      },
      {
        "name": "destinationIata",
        "type": "string | null",
        "description": "Resolved arrival airport."
      },
      {
        "name": "selected",
        "type": "object | null",
        "description": "The normalised record, including on-time flights. Null only when nothing was found."
      },
      {
        "name": "flightDelay",
        "type": "object | null",
        "description": "Delay and disruption verdict with its rationale, when timings were available."
      },
      {
        "name": "meta",
        "type": "object",
        "description": "Timing, cache and persistence state."
      }
    ],
    "data.selected": [
      {
        "name": "found",
        "type": "boolean",
        "description": "The flight was matched."
      },
      {
        "name": "complete",
        "type": "boolean",
        "description": "Enough timings were present to settle the record."
      },
      {
        "name": "status",
        "type": "'delayed' | 'cancelled' | 'diverted' | 'on_time' | 'not_found'",
        "description": "Classified outcome."
      },
      {
        "name": "isDelayed",
        "type": "boolean",
        "description": "Arrival delay greater than zero."
      },
      {
        "name": "isCancelled",
        "type": "boolean",
        "description": "Flight never operated."
      },
      {
        "name": "isDiverted",
        "type": "boolean",
        "description": "Landed somewhere other than the destination."
      },
      {
        "name": "delayMinutes",
        "type": "number | null",
        "description": "Arrival delay in minutes."
      },
      {
        "name": "departureDelayMinutes",
        "type": "number | null",
        "description": "Departure delay in minutes."
      },
      {
        "name": "scheduledDeparture",
        "type": "string | null",
        "description": "ISO 8601, UTC."
      },
      {
        "name": "scheduledArrival",
        "type": "string | null",
        "description": "ISO 8601, UTC."
      },
      {
        "name": "estimatedDeparture",
        "type": "string | null",
        "description": "ISO 8601, UTC."
      },
      {
        "name": "estimatedArrival",
        "type": "string | null",
        "description": "ISO 8601, UTC."
      },
      {
        "name": "actualDeparture",
        "type": "string | null",
        "description": "ISO 8601, UTC."
      },
      {
        "name": "actualArrival",
        "type": "string | null",
        "description": "ISO 8601, UTC."
      },
      {
        "name": "airline",
        "type": "object | null",
        "description": "`name`, `iata`, `icao`."
      },
      {
        "name": "aircraft",
        "type": "object | null",
        "description": "`registration`, `iata`, `icao`, `icao24`, `label`."
      },
      {
        "name": "flightIata",
        "type": "string | null",
        "description": "Normalised IATA designator."
      },
      {
        "name": "flightIcao",
        "type": "string | null",
        "description": "Normalised ICAO designator."
      },
      {
        "name": "statusRaw",
        "type": "string | null",
        "description": "Raw status string, unmodified."
      }
    ],
    "data.meta": [
      {
        "name": "durationMs",
        "type": "number",
        "description": "Wall clock for the whole resolve. `0` on a cache hit."
      },
      {
        "name": "startedAt",
        "type": "string",
        "description": "ISO 8601, UTC."
      },
      {
        "name": "finishedAt",
        "type": "string",
        "description": "ISO 8601, UTC."
      },
      {
        "name": "cached",
        "type": "boolean",
        "description": "Served from the persisted record instead of a live resolve."
      },
      {
        "name": "persisted",
        "type": "boolean",
        "description": "This response was written to the record table."
      },
      {
        "name": "recordId",
        "type": "string | null",
        "description": "Always null on the public endpoint."
      }
    ]
  },
  "caching": [
    {
      "label": "Persisted",
      "rule": "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."
    },
    {
      "label": "Never persisted",
      "rule": "On-time and not-found results, anything inside the three-day settle window, and screenshots."
    },
    {
      "label": "refresh=1",
      "rule": "Bypasses the stored record and resolves from scratch. The fresh answer overwrites the row in place when it is still persistable."
    }
  ],
  "resolverOutcomes": [
    {
      "value": "hit",
      "meaning": "A usable, complete record."
    },
    {
      "value": "incomplete",
      "meaning": "Matched the flight but lacked timings."
    },
    {
      "value": "miss",
      "meaning": "No record for this flight and date."
    },
    {
      "value": "skipped",
      "meaning": "Not needed for this lookup."
    },
    {
      "value": "error",
      "meaning": "The resolve failed. Safe to retry."
    }
  ],
  "webhooks": {
    "summary": "Registered endpoints receive a signed POST for every flight observed on the arrival and departure boards of the airports they selected. Boards are polled every 30 minutes; queued events are sent within 2 minutes. Deliveries are never metered.",
    "management": {
      "dashboard": "https://flight-api.dev/dashboard/webhooks",
      "catalogue": "https://flight-api.dev/api/v1/airports"
    },
    "airports": [
      {
        "iata": "OTP",
        "name": "Bucharest Otopeni",
        "city": "Bucharest",
        "country": "Romania",
        "countryCode": "RO",
        "timezone": "Europe/Bucharest",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "CLJ",
        "name": "Cluj-Napoca",
        "city": "Cluj-Napoca",
        "country": "Romania",
        "countryCode": "RO",
        "timezone": "Europe/Bucharest",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "RMO",
        "name": "Chișinău",
        "city": "Chișinău",
        "country": "Moldova",
        "countryCode": "MD",
        "timezone": "Europe/Chisinau",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": [
          "KIV"
        ]
      },
      {
        "iata": "TIA",
        "name": "Tirana",
        "city": "Tirana",
        "country": "Albania",
        "countryCode": "AL",
        "timezone": "Europe/Tirane",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "BUD",
        "name": "Budapest",
        "city": "Budapest",
        "country": "Hungary",
        "countryCode": "HU",
        "timezone": "Europe/Budapest",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "MLA",
        "name": "Malta",
        "city": "Luqa",
        "country": "Malta",
        "countryCode": "MT",
        "timezone": "Europe/Malta",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "BCM",
        "name": "Bacău",
        "city": "Bacău",
        "country": "Romania",
        "countryCode": "RO",
        "timezone": "Europe/Bucharest",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "VIE",
        "name": "Vienna",
        "city": "Vienna",
        "country": "Austria",
        "countryCode": "AT",
        "timezone": "Europe/Vienna",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "CDG",
        "name": "Paris Charles de Gaulle",
        "city": "Paris",
        "country": "France",
        "countryCode": "FR",
        "timezone": "Europe/Paris",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "LTN",
        "name": "London Luton",
        "city": "London",
        "country": "United Kingdom",
        "countryCode": "GB",
        "timezone": "Europe/London",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "VCE",
        "name": "Venice Marco Polo",
        "city": "Venice",
        "country": "Italy",
        "countryCode": "IT",
        "timezone": "Europe/Rome",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "BCN",
        "name": "Barcelona El Prat",
        "city": "Barcelona",
        "country": "Spain",
        "countryCode": "ES",
        "timezone": "Europe/Madrid",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "CPH",
        "name": "Copenhagen",
        "city": "Copenhagen",
        "country": "Denmark",
        "countryCode": "DK",
        "timezone": "Europe/Copenhagen",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "BHX",
        "name": "Birmingham",
        "city": "Birmingham",
        "country": "United Kingdom",
        "countryCode": "GB",
        "timezone": "Europe/London",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "FCO",
        "name": "Rome Fiumicino",
        "city": "Rome",
        "country": "Italy",
        "countryCode": "IT",
        "timezone": "Europe/Rome",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "ATH",
        "name": "Athens",
        "city": "Athens",
        "country": "Greece",
        "countryCode": "GR",
        "timezone": "Europe/Athens",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "MAN",
        "name": "Manchester",
        "city": "Manchester",
        "country": "United Kingdom",
        "countryCode": "GB",
        "timezone": "Europe/London",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "ORY",
        "name": "Paris Orly",
        "city": "Paris",
        "country": "France",
        "countryCode": "FR",
        "timezone": "Europe/Paris",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "WRO",
        "name": "Wrocław",
        "city": "Wrocław",
        "country": "Poland",
        "countryCode": "PL",
        "timezone": "Europe/Warsaw",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "BSL",
        "name": "Basel Mulhouse EuroAirport",
        "city": "Basel/Mulhouse",
        "country": "France",
        "countryCode": "FR",
        "timezone": "Europe/Paris",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": [
          "MLH",
          "EAP"
        ]
      },
      {
        "iata": "BLQ",
        "name": "Bologna",
        "city": "Bologna",
        "country": "Italy",
        "countryCode": "IT",
        "timezone": "Europe/Rome",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "ALC",
        "name": "Alicante",
        "city": "Alicante",
        "country": "Spain",
        "countryCode": "ES",
        "timezone": "Europe/Madrid",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "LGW",
        "name": "London Gatwick",
        "city": "London",
        "country": "United Kingdom",
        "countryCode": "GB",
        "timezone": "Europe/London",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "MXP",
        "name": "Milan Malpensa",
        "city": "Milan",
        "country": "Italy",
        "countryCode": "IT",
        "timezone": "Europe/Rome",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "BRI",
        "name": "Bari",
        "city": "Bari",
        "country": "Italy",
        "countryCode": "IT",
        "timezone": "Europe/Rome",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "NAP",
        "name": "Naples",
        "city": "Naples",
        "country": "Italy",
        "countryCode": "IT",
        "timezone": "Europe/Rome",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "MAD",
        "name": "Madrid Barajas",
        "city": "Madrid",
        "country": "Spain",
        "countryCode": "ES",
        "timezone": "Europe/Madrid",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "VLC",
        "name": "Valencia",
        "city": "Valencia",
        "country": "Spain",
        "countryCode": "ES",
        "timezone": "Europe/Madrid",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "ARN",
        "name": "Stockholm Arlanda",
        "city": "Stockholm",
        "country": "Sweden",
        "countryCode": "SE",
        "timezone": "Europe/Stockholm",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "BGY",
        "name": "Milan Bergamo",
        "city": "Bergamo",
        "country": "Italy",
        "countryCode": "IT",
        "timezone": "Europe/Rome",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "NCE",
        "name": "Nice Côte d’Azur",
        "city": "Nice",
        "country": "France",
        "countryCode": "FR",
        "timezone": "Europe/Paris",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "SKP",
        "name": "Skopje",
        "city": "Skopje",
        "country": "North Macedonia",
        "countryCode": "MK",
        "timezone": "Europe/Skopje",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "LIS",
        "name": "Lisbon",
        "city": "Lisbon",
        "country": "Portugal",
        "countryCode": "PT",
        "timezone": "Europe/Lisbon",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "CRL",
        "name": "Brussels South Charleroi",
        "city": "Charleroi",
        "country": "Belgium",
        "countryCode": "BE",
        "timezone": "Europe/Brussels",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "CTA",
        "name": "Catania Fontanarossa",
        "city": "Catania",
        "country": "Italy",
        "countryCode": "IT",
        "timezone": "Europe/Rome",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "AGP",
        "name": "Málaga",
        "city": "Málaga",
        "country": "Spain",
        "countryCode": "ES",
        "timezone": "Europe/Madrid",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "BTS",
        "name": "Bratislava",
        "city": "Bratislava",
        "country": "Slovakia",
        "countryCode": "SK",
        "timezone": "Europe/Bratislava",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "KRK",
        "name": "Kraków",
        "city": "Kraków",
        "country": "Poland",
        "countryCode": "PL",
        "timezone": "Europe/Warsaw",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "KTW",
        "name": "Katowice",
        "city": "Katowice",
        "country": "Poland",
        "countryCode": "PL",
        "timezone": "Europe/Warsaw",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "BRU",
        "name": "Brussels",
        "city": "Brussels",
        "country": "Belgium",
        "countryCode": "BE",
        "timezone": "Europe/Brussels",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "LCA",
        "name": "Larnaca",
        "city": "Larnaca",
        "country": "Cyprus",
        "countryCode": "CY",
        "timezone": "Asia/Nicosia",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "WAW",
        "name": "Warsaw Chopin",
        "city": "Warsaw",
        "country": "Poland",
        "countryCode": "PL",
        "timezone": "Europe/Warsaw",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "CPO",
        "name": "Copiapó Desierto de Atacama",
        "city": "Copiapó",
        "country": "Chile",
        "countryCode": "CL",
        "timezone": "America/Santiago",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "HRK",
        "name": "Kharkiv",
        "city": "Kharkiv",
        "country": "Ukraine",
        "countryCode": "UA",
        "timezone": "Europe/Kyiv",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "LPL",
        "name": "Liverpool John Lennon",
        "city": "Liverpool",
        "country": "United Kingdom",
        "countryCode": "GB",
        "timezone": "Europe/London",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "OSL",
        "name": "Oslo Gardermoen",
        "city": "Oslo",
        "country": "Norway",
        "countryCode": "NO",
        "timezone": "Europe/Oslo",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "WMI",
        "name": "Warsaw Modlin",
        "city": "Warsaw",
        "country": "Poland",
        "countryCode": "PL",
        "timezone": "Europe/Warsaw",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "LBA",
        "name": "Leeds Bradford",
        "city": "Leeds",
        "country": "United Kingdom",
        "countryCode": "GB",
        "timezone": "Europe/London",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "POZ",
        "name": "Poznań Ławica",
        "city": "Poznań",
        "country": "Poland",
        "countryCode": "PL",
        "timezone": "Europe/Warsaw",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "PSA",
        "name": "Pisa Galileo Galilei",
        "city": "Pisa",
        "country": "Italy",
        "countryCode": "IT",
        "timezone": "Europe/Rome",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "CIA",
        "name": "Rome Ciampino",
        "city": "Rome",
        "country": "Italy",
        "countryCode": "IT",
        "timezone": "Europe/Rome",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      },
      {
        "iata": "STN",
        "name": "London Stansted",
        "city": "London",
        "country": "United Kingdom",
        "countryCode": "GB",
        "timezone": "Europe/London",
        "boards": [
          "departures",
          "arrivals"
        ],
        "aliases": []
      }
    ],
    "eventTypes": [
      {
        "type": "flight.tracked",
        "kind": "first_seen",
        "title": "Flight entered the board",
        "description": "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.",
        "volume": "high"
      },
      {
        "type": "flight.delayed",
        "kind": "became_delayed",
        "title": "Flight became delayed",
        "description": "The flight crossed from on time into delayed. Fires on the transition, not on every re-estimate.",
        "volume": "medium"
      },
      {
        "type": "flight.cancelled",
        "kind": "became_cancelled",
        "title": "Flight was cancelled",
        "description": "The board now reports the flight as cancelled. Fires once.",
        "volume": "low"
      },
      {
        "type": "flight.diverted",
        "kind": "became_diverted",
        "title": "Flight was diverted",
        "description": "The flight is landing somewhere other than its filed destination. `data.flight.divertedTo` carries the airport when the board names one.",
        "volume": "low"
      },
      {
        "type": "flight.recovered",
        "kind": "recovered",
        "title": "Flight left disruption",
        "description": "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.",
        "volume": "low"
      },
      {
        "type": "flight.delay_changed",
        "kind": "delay",
        "title": "Delay estimate moved",
        "description": "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.",
        "volume": "high"
      },
      {
        "type": "flight.status_changed",
        "kind": "status",
        "title": "Status string changed",
        "description": "The board's own status text changed — \"Scheduled\" to \"Estimated 14:35\" to \"Landed 14:41\". Highest volume of the set.",
        "volume": "high"
      },
      {
        "type": "flight.divert_updated",
        "kind": "divert",
        "title": "Divert target changed",
        "description": "The diversion airport was set, changed or cleared.",
        "volume": "low"
      },
      {
        "type": "flight.qualifying",
        "kind": "qualified",
        "title": "Flight became an EU261 case",
        "description": "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.",
        "volume": "low"
      },
      {
        "type": "flight.predicted_qualifying",
        "kind": "predicted_qualifying",
        "title": "Flight predicted to qualify",
        "description": "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.",
        "volume": "low"
      }
    ],
    "delivery": {
      "pollIntervalMinutes": 30,
      "deliveryIntervalMinutes": 2,
      "timeoutSeconds": 10,
      "maxAttempts": 5,
      "retryBackoffMinutes": [
        1,
        5,
        20,
        60
      ],
      "disableAfterConsecutiveFailures": 25,
      "successCriteria": "Any 2xx. Redirects are not followed.",
      "rules": [
        {
          "label": "Freshness",
          "rule": "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."
        },
        {
          "label": "At least once, never twice",
          "rule": "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."
        },
        {
          "label": "Success",
          "rule": "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."
        },
        {
          "label": "Retries",
          "rule": "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."
        },
        {
          "label": "Auto-disable",
          "rule": "25 consecutive failures across all deliveries disables the endpoint and stops the queue. Any 2xx — including a manual test send — resets the counter."
        },
        {
          "label": "Ordering",
          "rule": "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."
        },
        {
          "label": "Scope",
          "rule": "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."
        }
      ]
    },
    "signature": {
      "algorithm": "HMAC-SHA256",
      "header": "X-Flight-Signature",
      "format": "t=<unix seconds>,v1=<hex digest>",
      "signedPayload": "{timestamp}.{rawBody}",
      "secretPrefix": "whsec_",
      "toleranceSeconds": 300,
      "rules": [
        {
          "label": "What is signed",
          "rule": "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."
        },
        {
          "label": "Why the timestamp",
          "rule": "It is inside the MAC, so it cannot be edited. Reject anything more than five minutes old and a captured request stops being replayable."
        },
        {
          "label": "Comparison",
          "rule": "Compare in constant time. A byte-by-byte early exit leaks the signature one character at a time."
        },
        {
          "label": "Rotation",
          "rule": "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."
        }
      ]
    },
    "requestHeaders": [
      {
        "header": "X-Flight-Signature",
        "description": "HMAC-SHA256 over `{timestamp}.{body}`, as `t=<unix>,v1=<hex>`."
      },
      {
        "header": "X-Flight-Event",
        "description": "Board event id. Shared by every subscriber to it."
      },
      {
        "header": "X-Flight-Event-Type",
        "description": "Same value as `type` in the body."
      },
      {
        "header": "X-Flight-Delivery",
        "description": "Delivery id — unique per endpoint. Same value as `id` in the body."
      },
      {
        "header": "X-Flight-Webhook",
        "description": "The endpoint that was configured to receive this."
      },
      {
        "header": "X-Flight-Attempt",
        "description": "1 on the first try, incrementing on each retry."
      }
    ],
    "schemas": {
      "event": [
        {
          "name": "id",
          "type": "string",
          "description": "Delivery id. Unique per endpoint — use it as your idempotency key."
        },
        {
          "name": "event",
          "type": "string",
          "description": "Board event id. Identical across every endpoint that received this change."
        },
        {
          "name": "type",
          "type": "string",
          "description": "One of the event types below."
        },
        {
          "name": "createdAt",
          "type": "string",
          "description": "When the change was observed. ISO 8601, UTC."
        },
        {
          "name": "webhook",
          "type": "object",
          "description": "The subscription that matched: `id`, `name`, and the `airports` it selected."
        },
        {
          "name": "airport",
          "type": "object",
          "description": "The watched airport this board belongs to: `iata`, `name`, `city`, `country`, `timezone`."
        },
        {
          "name": "data",
          "type": "object",
          "description": "The flight and its previous observation. `flight.qualifying` adds `qualification`; `flight.predicted_qualifying` adds `prediction`."
        }
      ],
      "event.data.flight": [
        {
          "name": "id",
          "type": "string",
          "description": "Stable across every event for this flight on this board."
        },
        {
          "name": "flightNumber",
          "type": "string",
          "description": "IATA designator as the board prints it."
        },
        {
          "name": "callsign",
          "type": "string | null",
          "description": "ATC callsign, when the board carries one."
        },
        {
          "name": "direction",
          "type": "'departure' | 'arrival'",
          "description": "Which board of the watched airport this row came from."
        },
        {
          "name": "airline",
          "type": "object",
          "description": "`name` from the board, `iata` from the designator."
        },
        {
          "name": "aircraftType",
          "type": "string | null",
          "description": "ICAO type code, e.g. `B738`."
        },
        {
          "name": "registration",
          "type": "string | null",
          "description": "Tail number, once the aircraft has been looked up. Set on most qualifying and predicted flights."
        },
        {
          "name": "operation",
          "type": "'passenger' | 'cargo' | 'military' | 'private'",
          "description": "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."
        },
        {
          "name": "origin",
          "type": "object",
          "description": "`{ iata }`. Equals `airport.iata` on a departure. `iata` is null when the board does not name the other end."
        },
        {
          "name": "destination",
          "type": "object",
          "description": "`{ iata }`. Equals `airport.iata` on an arrival. `iata` is null when the board does not name the other end."
        },
        {
          "name": "divertedTo",
          "type": "object | null",
          "description": "`{ iata }` when the board names a diversion airport."
        },
        {
          "name": "scheduledAt",
          "type": "string",
          "description": "ISO 8601, UTC."
        },
        {
          "name": "estimatedAt",
          "type": "string | null",
          "description": "ISO 8601, UTC. Null until the board estimates."
        },
        {
          "name": "delayMinutes",
          "type": "number | null",
          "description": "Estimated minus scheduled, in whole minutes. Negative when early."
        },
        {
          "name": "status",
          "type": "string",
          "description": "The board's own status text, unmodified."
        },
        {
          "name": "disruption",
          "type": "'scheduled' | 'on_time' | 'delayed' | 'cancelled' | 'diverted'",
          "description": "Normalised verdict. `scheduled` means the board has no estimate yet, so nothing can be claimed."
        },
        {
          "name": "isCancelled",
          "type": "boolean",
          "description": "The board reports the flight as cancelled."
        },
        {
          "name": "isDiverted",
          "type": "boolean",
          "description": "The board reports a diversion."
        },
        {
          "name": "runway",
          "type": "string | null",
          "description": "When published."
        },
        {
          "name": "firstSeenAt",
          "type": "string",
          "description": "When this row first appeared. ISO 8601, UTC."
        },
        {
          "name": "lastSeenAt",
          "type": "string",
          "description": "When it was last observed. Use it to discard an out-of-order delivery."
        }
      ],
      "event.data.previous": [
        {
          "name": "status",
          "type": "string | null",
          "description": "Board status text before the change."
        },
        {
          "name": "estimatedAt",
          "type": "string | null",
          "description": "ISO 8601, UTC."
        },
        {
          "name": "delayMinutes",
          "type": "number | null",
          "description": "Delay before the change."
        },
        {
          "name": "disruption",
          "type": "string | null",
          "description": "Verdict before the change."
        }
      ],
      "event.data.qualification": [
        {
          "name": "reason",
          "type": "'cancelled' | 'diverted' | 'delayed'",
          "description": "Arrivals only ever qualify as `delayed` — a cancelled arrival’s passengers never reach this airport."
        },
        {
          "name": "delayMinutes",
          "type": "number | null",
          "description": "The delay that crossed the line. Null for a cancellation."
        },
        {
          "name": "detectedAt",
          "type": "string",
          "description": "When the board first showed it. ISO 8601, UTC."
        },
        {
          "name": "minutesBeforeScheduled",
          "type": "number",
          "description": "Scheduled time minus detection. Positive means the team still has that long before the flight was due."
        },
        {
          "name": "compensation",
          "type": "object | null",
          "description": "`{ amountEur, distanceKm, band }` — the EU261 amount for the route and delay. Null when either end of the route is unknown."
        }
      ],
      "event.data.prediction": [
        {
          "name": "registration",
          "type": "string",
          "description": "The tail due to operate this departure."
        },
        {
          "name": "projectedDepartureAt",
          "type": "string",
          "description": "Earliest the aircraft can realistically leave. ISO 8601, UTC."
        },
        {
          "name": "projectedDelayMinutes",
          "type": "number",
          "description": "Projected departure minus scheduled."
        },
        {
          "name": "confidence",
          "type": "'high' | 'medium'",
          "description": "`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."
        },
        {
          "name": "minTurnaroundMinutes",
          "type": "number",
          "description": "Ground time assumed for this airframe class — the low end, so projections err short."
        },
        {
          "name": "inbound",
          "type": "object",
          "description": "`{ flightNumber, origin: { iata }, scheduledArrivalAt, projectedArrivalAt, status }` — the leg the aircraft is flying in on."
        },
        {
          "name": "detectedAt",
          "type": "string",
          "description": "When the prediction was made. ISO 8601, UTC."
        },
        {
          "name": "minutesBeforeScheduled",
          "type": "number",
          "description": "Scheduled departure minus detection."
        },
        {
          "name": "compensation",
          "type": "object | null",
          "description": "`{ amountEur, distanceKm, band }` — the EU261 amount for the route and delay. Null when either end of the route is unknown."
        }
      ]
    },
    "examples": {
      "payload": {
        "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"
          }
        }
      },
      "verify": "import { createHmac, timingSafeEqual } from 'node:crypto'\n\nconst TOLERANCE_SECONDS = 300\n\nexport function verify(rawBody: string, header: string, secret: string): boolean {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)))\n  const timestamp = Number(parts.t)\n  if (!Number.isFinite(timestamp)) return false\n\n  // Bound the age first: the timestamp is inside the MAC, so an attacker\n  // cannot backdate a captured request to keep replaying it.\n  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false\n\n  const expected = createHmac('sha256', secret)\n    .update(`${timestamp}.${rawBody}`)\n    .digest('hex')\n\n  const a = Buffer.from(parts.v1 ?? '')\n  const b = Buffer.from(expected)\n  return a.length === b.length && timingSafeEqual(a, b)\n}",
      "receiver": "// Next.js route handler. Read the raw body — re-serialising the parsed\n// JSON produces different bytes and the signature will not match.\nexport async function POST(request: Request) {\n  const raw = await request.text()\n  const header = request.headers.get('x-flight-signature') ?? ''\n\n  if (!verify(raw, header, process.env.FLIGHT_WEBHOOK_SECRET!)) {\n    return new Response('invalid signature', { status: 401 })\n  }\n\n  const event = JSON.parse(raw)\n\n  // Acknowledge first, work after. We time out at 10s and\n  // retry a non-2xx, so slow handlers turn into duplicate attempts.\n  void enqueue(event)\n\n  return new Response(null, { status: 204 })\n}"
    }
  },
  "examples": {
    "curl": "curl -sG \"https://flight-api.dev/api/v1/flights/status\" \\\n  -H \"Authorization: Bearer $FLIGHT_API_KEY\" \\\n  --data-urlencode \"flightNumber=BA249\" \\\n  --data-urlencode \"flightDate=2026-08-14\"",
    "typescript": "const params = new URLSearchParams({\n  flightNumber: 'BA249',\n  flightDate: '2026-08-14',\n})\n\nconst response = await fetch(`https://flight-api.dev/api/v1/flights/status?${params}`, {\n  headers: { Authorization: `Bearer ${process.env.FLIGHT_API_KEY}` },\n})\n\nconst { success, data, message } = await response.json()\nif (!success) throw new Error(message)\n\n// data.selected is the resolved record.\nconsole.log(data.selected?.status, data.selected?.delayMinutes, data.meta.cached)",
    "response": {
      "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": []
      }
    }
  },
  "artifacts": {
    "openapi": "https://flight-api.dev/openapi.json",
    "json": "https://flight-api.dev/api/docs",
    "llms": "https://flight-api.dev/llms.txt",
    "markdown": "https://flight-api.dev/docs.md"
  }
}