Webhooks
The delivery format, every event and change type, how to verify signatures, and what retries look like.
A webhook is a POST with a JSON body to your subscription's URL. It carries the event, the full flight snapshot at that moment, and the list of what changed since the previous delivery.
Headers
POST /hooks/flights HTTP/1.1
Content-Type: application/json
x-flightbyte-event-id: evt_2f0c8a4e-6f1d-4d0d-9a3b-0f1e2d3c4b5a
x-flightbyte-event-type: flight.updated
x-flightbyte-subscription-id: sub_7fd9fa92faf3475db99b
x-flightbyte-delivery-attempt: 1
x-flightbyte-timestamp: 1789305971
x-flightbyte-signature: t=1789305971,v1=5f1c9a...e3b2Any headers you set on the subscription are sent as well.
Legacy header names
The same six values are also sent as x-flightradar-* for receivers written before the rename. Read the x-flightbyte-* names in new code; the old ones will be dropped in a future release.
Payload
{
"id": "evt_2f0c8a4e-6f1d-4d0d-9a3b-0f1e2d3c4b5a",
"type": "flight.updated",
"createdAt": "2026-09-12T09:26:11.000Z",
"subscriptionId": "sub_7fd9fa92faf3475db99b",
"flightKey": "TK1848:2026-09-12@ATH-IST",
"flight": { "...": "the full flight snapshot" },
"changes": [
{
"type": "departure.gate.changed",
"path": "departure.gate",
"from": "A11",
"to": "A22",
"message": "Departure gate changed from A11 to A22"
},
{
"type": "arrival.estimated.changed",
"path": "arrival.times.estimatedGate",
"from": "2026-09-12T10:45:00Z",
"to": "2026-09-12T11:15:00Z",
"deltaMinutes": 30,
"message": "Arrival estimated gate time changed from 2026-09-12T10:45:00Z to 2026-09-12T11:15:00Z"
}
],
"previousFetchedAt": "2026-09-12T09:16:04.000Z",
"metadata": { "bookingId": "BK-1042" }
}| Field | Meaning |
|---|---|
id | Stable across retries. Deduplicate on it. |
type | One of the event types. |
flight | The snapshot as of this event. Present on every event that has a resolved flight. |
changes | Empty on lifecycle events; otherwise one entry per change, each with a path into the snapshot, from, to, a human message, and deltaMinutes for time changes. |
previousFetchedAt | When the snapshot these changes were computed against was taken. |
metadata | Exactly what you stored on the subscription. |
Event types
| Type | When |
|---|---|
subscription.created | Right after creation. Carries the flight if it could be resolved. |
subscription.activated | A pending subscription's flight appeared upstream. |
flight.updated | Something about the flight changed. The only type with a non-empty changes. |
subscription.test | You called the test endpoint. |
subscription.suspended | Deliveries kept failing and polling stopped. Delivery of this one is attempted too. |
subscription.completed | The watch ended: the flight arrived, or the date passed without it. |
Change types
These are the values for a subscription's events filter and for changes[].type. A flight.updated event can carry several at once.
| Type | Fires when |
|---|---|
status.changed | The status code or phase changed for any reason. |
flight.departed | The flight left the gate or the runway. |
flight.arrived | The flight reached the gate. |
flight.cancelled | Cancelled. |
flight.diverted | Landing somewhere other than planned. |
flight.reinstated | A cancelled flight is back. |
departure.gate.changed, departure.terminal.changed | Departure gate or terminal moved. |
arrival.gate.changed, arrival.terminal.changed | Arrival gate or terminal moved. |
arrival.baggage.changed | Baggage carousel assigned or changed. |
departure.scheduled.changed, departure.estimated.changed, departure.actual.changed | A departure time changed. |
arrival.scheduled.changed, arrival.estimated.changed, arrival.actual.changed | An arrival time changed. |
delay.increased, delay.decreased | The gate delay grew or shrank by at least delayThresholdMinutes. |
aircraft.changed, aircraft.tail.changed | Aircraft type or the specific airframe changed. |
route.changed | Origin or destination changed. |
codeshares.changed | The codeshare list changed. |
Verifying a delivery
The signature header is t=<unix seconds>,v1=<hex> where v1 is HMAC-SHA256(secret, "<t>.<raw body>"). Verify against the raw bytes you received, before any JSON parsing, and reject timestamps older than a few minutes; FlightByte itself allows five.
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));
}Frameworks that parse JSON for you
Express, Fastify, Django and friends usually hand you an object, not bytes. Configure the route to keep the raw body (express.raw, rawBody: true in Fastify, request.body in Django) and verify that; re-serialising the parsed object will not reproduce the exact bytes that were signed.
Retries and suspension
A delivery succeeds when your endpoint answers any 2xx within ten seconds. Otherwise:
- Network errors, timeouts and
5xxresponses are retried up to 8 times with exponential backoff, starting at four seconds. 408and429are retried the same way.- Any other
4xxis treated as your endpoint rejecting the event and is not retried. - Every attempt is written to the delivery log with its status and latency.
After 20 consecutive failed deliveries the subscription is suspended and polling stops. Fix the endpoint, then POST /v1/subscriptions/{id}/resume; use the events log to see what you missed.
Writing a receiver that behaves
- Answer fast, work later. Acknowledge with
204, then process. Ten seconds is generous until your database is slow. - Deduplicate on
id. A retry carries the sameidand the same body. Keep a small table of processed ids. - Do not assume order. Retries can arrive after a newer event.
createdAtandflight.fetchedAttell you which snapshot is newer; when in doubt, theflightin the latest event is the truth. - Use
metadata, not the URL. One endpoint can serve every subscription;metadatatells you which booking it is about. - Fail loudly in development. Log signature failures with the reason; a wrong secret and a re-serialised body look identical from the outside.
Three receivers that do all of the above, in Node.js, Python and Go, with a script that sends signed test events, are in flightbyte-webhook-examples.