---
name: flightbyte
description: Integrate the FlightByte flight status API. Look flights up by carrier, flight number and local departure date, subscribe to a flight, and receive and verify its signed webhooks (gate changes, delays, departures, arrivals, baggage carousel). Use when code calls api.flightbyte.dev, handles FlightByte webhooks, or the user wants to track flights or get flight status updates with FlightByte.
---

# FlightByte

FlightByte is a flight status API with signed webhooks. You can ask it about a flight, or ask it to watch one: every change then arrives at your endpoint as a signed `POST`, while FlightByte does the polling.

## Read the docs, do not guess

The API has specifics that general knowledge gets wrong. Before writing code, read the page for the part you are touching:

- Everything in one file: https://flightbyte.dev/llms-full.txt
- Index of pages: https://flightbyte.dev/llms.txt
- Any docs page as Markdown, its URL plus `.md`: https://flightbyte.dev/docs/flights.md, https://flightbyte.dev/docs/subscriptions.md, https://flightbyte.dev/docs/webhooks.md, https://flightbyte.dev/docs/errors.md, https://flightbyte.dev/docs/authentication.md
- Exact request and response schemas: https://flightbyte.dev/openapi.json

## Essentials

- Base URL: `https://api.flightbyte.dev/v1`.
- Authentication: the `x-api-key` header (`Authorization: Bearer <key>` also works). Read the key from an environment variable such as `FLIGHTBYTE_KEY`. It is a server-side secret: never put it in a browser bundle or a mobile app.
- A flight is addressed by carrier code, flight number and the **local departure date at the origin** (`YYYY-MM-DD`). A flight that leaves at 23:50 and lands the next morning is on the day it left. Leading zeros in the number are ignored.
- Live data covers flights from a few days before today to a couple of days ahead; a lookup outside that window answers `404`. A subscription can be created up to seven days ahead and waits as `pending` until the flight appears.
- Branch on `status.phase`: `scheduled`, `boarding`, `departed`, `en_route`, `landed`, `arrived`, `cancelled`, `diverted`, `redirected`, `unknown`. `status.code` and `status.label` are the upstream's wording. When you need one time for a departure or arrival, use `bestTimeUtc` (actual, else estimated, else scheduled).

## Look a flight up

```http
GET /v1/flights/{carrier}/{flightNumber}/{date}
GET /v1/flights/search?carrier=TK&flightNumber=1848&date=2026-09-12
```

```ts
const res = await fetch("https://api.flightbyte.dev/v1/flights/TK/1848/2026-09-12", {
  headers: { "x-api-key": process.env.FLIGHTBYTE_KEY! },
});
if (!res.ok) throw new Error(`FlightByte ${res.status}: ${(await res.json()).message}`);
const flight = await res.json();
console.log(flight.status.phase, flight.departure.gate, flight.arrival.bestTimeUtc);
```

Flight numbers are not unique within a day (`TK1` flies SAW → AMS and IST → JFK). A lookup without a selector returns the upstream's default leg and lists every leg under `alternates`, with `selected: true` on the one described. Pick a leg with `origin` (IATA code such as `IST`), narrow with `destination`, or pass `flightId` from `alternates[].providerFlightId`. A selector that matches nothing answers `404`. `refresh=true` skips the cache.

## Watch a flight

```bash
curl -X POST "https://api.flightbyte.dev/v1/subscriptions" \
  -H "x-api-key: $FLIGHTBYTE_KEY" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: booking-BK-1042" \
  -d '{
    "carrier": "TK",
    "flightNumber": "1848",
    "date": "2026-09-12",
    "webhookUrl": "https://yourapp.com/hooks/flights",
    "events": ["flight.departed", "flight.arrived", "delay.increased", "arrival.gate.changed"],
    "metadata": { "bookingId": "BK-1042" }
  }'
```

- Required: `carrier`, `flightNumber`, `date`, `webhookUrl`. When the number flies more than one leg that day, `origin` (or `destination`, `flightId`) is required too; without it the request is refused with `400` and a `legs` list to choose from.
- Optional: `events` (change types to deliver, default all), `delayThresholdMinutes` (`1` to `240`, default `1`), `headers` (sent with every delivery), `metadata` (string map echoed in every webhook; put your own record id here), `secret` (bring your own signing secret).
- Send an `Idempotency-Key` header so a retried call returns the same subscription instead of creating a second one.
- **Store the `secret` from the response.** It is shown in full only there; `GET /v1/subscriptions/{id}/secret` reveals it again and `POST /v1/subscriptions/{id}/secret/rotate` replaces it.
- A `subscription.created` event is delivered right away. The watch ends by itself after the flight arrives and the baggage carousel is known (`subscription.completed`).
- Other calls: `GET /v1/subscriptions`, `GET|PATCH|DELETE /v1/subscriptions/{id}`, `POST /v1/subscriptions/{id}/pause`, `POST /v1/subscriptions/{id}/resume`. `PATCH` can change `webhookUrl`, `events`, `delayThresholdMinutes`, `headers` and `metadata`, never the flight.
- A subscription belongs to the key that created it; another key gets `404` for it.

## Receive a webhook

Each delivery is a `POST` with a JSON body: `id`, `type`, `createdAt`, `subscriptionId`, `flightKey`, `flight` (the full snapshot), `changes` (each with `type`, `path`, `from`, `to`, `message`, and `deltaMinutes` for times), `previousFetchedAt` and `metadata`. Event `type` is one of `subscription.created`, `subscription.activated`, `flight.updated`, `subscription.test`, `subscription.suspended`, `subscription.completed`; only `flight.updated` carries `changes`. The 23 change types are listed at https://flightbyte.dev/docs/webhooks.md.

The signature header is `x-flightbyte-signature: t=<unix seconds>,v1=<hex>`, where `v1` is `HMAC-SHA256(secret, "<t>.<raw body>")`.

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret: string, header: string, rawBody: Buffer, toleranceSeconds = 300): boolean {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=", 2)));
  if (!parts.t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.`).update(rawBody).digest("hex");
  return parts.v1.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
```

```python
import hmac, hashlib, time

def verify(secret: bytes, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1 or abs(time.time() - float(t)) > tolerance:
        return False
    expected = hmac.new(secret, f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(v1, expected)
```

Rules for the receiver:

- Verify against the **raw bytes** before any JSON parsing. Frameworks that parse for you must be told to keep the raw body (`express.raw({ type: "application/json" })` in Express, `rawBody: true` in Fastify); re-serialising the parsed object does not reproduce the signed bytes.
- Reject timestamps more than five minutes away; compare in constant time.
- Answer any `2xx` within ten seconds, then do the work. Network errors, timeouts, `5xx`, `408` and `429` are retried up to 8 times with exponential backoff; any other `4xx` is not retried. After 20 consecutive failed deliveries the subscription is `suspended`; fix the endpoint and `POST /v1/subscriptions/{id}/resume`.
- Deduplicate on the event `id`: a retry carries the same `id` and body.
- Do not assume order. `createdAt` and `flight.fetchedAt` say which snapshot is newer.
- Use `metadata`, not the URL, to find your own record.
- Read `x-flightbyte-*` headers; the `x-flightradar-*` copies are legacy.

## Test without waiting for a real change

```bash
curl -X POST "https://api.flightbyte.dev/v1/subscriptions/$SUBSCRIPTION_ID/test" -H "x-api-key: $FLIGHTBYTE_KEY"
curl "https://api.flightbyte.dev/v1/subscriptions/$SUBSCRIPTION_ID/deliveries" -H "x-api-key: $FLIGHTBYTE_KEY"
```

The test event is signed and delivered like a real one; the deliveries log shows the HTTP status your endpoint returned, latency, attempt number and error. `GET /v1/subscriptions/{id}/events` returns the exact payloads sent. For a local endpoint, use a tunnel such as `ngrok` or `cloudflared`. Webhook URLs that resolve to private, loopback or link-local addresses are refused. Working receivers in Node.js, Python and Go: https://github.com/batuhanthedeveloper/flightbyte-webhook-examples

## Errors and quotas

Every error is JSON: `statusCode`, `error`, `message` (a string, or an array of strings for validation errors), `path`, `timestamp`, `requestId`.

| Code | Meaning | Retry? |
| --- | --- | --- |
| `400` | Malformed or invalid request; for an ambiguous flight number the body carries `legs`. | No, fix the request. |
| `401` | Missing, invalid, revoked or expired key. | No. |
| `402` | The plan's monthly watched-flight cap is reached (`"code": "watched_flight_limit"`, Free: 3). | Not until the month or the plan changes. |
| `404` | No such flight on that date, no leg matching the selector, or no such subscription for this key. | No. |
| `429` | A quota window (minute, day or month) is full; the body names the `window`. | Yes, after `Retry-After` seconds. |
| `502`, `503` | Upstream or service temporarily unavailable. | Yes, with backoff. |

Responses carry `X-RateLimit-Limit-*`, `X-RateLimit-Remaining-*` and `X-RateLimit-Reset-*` headers per window. Webhook deliveries never count against the request quotas.

## Before you finish

- The key comes from the environment and only server-side code sends it.
- Dates are local departure dates at the origin, and ambiguous numbers pass `origin`.
- The subscription `secret` is stored where the webhook handler can read it.
- The webhook handler verifies the signature on the raw body, checks the timestamp, answers fast and deduplicates on `id`.
- A test event has gone through: `POST /v1/subscriptions/{id}/test`, then check the deliveries log.
