An itinerary manager does not begin with a flight. It begins with an email somebody forwarded: a confirmation from an airline, an agency or a corporate booking tool, in one of a few thousand layouts. Your parser, or your model, pulls a flight number and a date out of it. Everything after that depends on how much of that pair you can trust.
This guide is the pipeline between "we extracted TK 1848, 12 Sep" and "this flight is watched, once, and every user on it will hear about the gate". It assumes you already parse the email; it is about what to do with the result.
What the email gives you, and what it does not
| From the email | Reliable? | Notes |
|---|---|---|
| Airline code and flight number | Mostly | Often the marketing number, not the one on the airport board. |
| Departure date | Yes | Local to the origin, which is exactly what the API wants. |
| Origin and destination airports | Yes | Keep them. They are your leg selector. |
| Scheduled times | At time of booking | Schedules move in the months between booking and flying. |
| Terminal, gate, aircraft | Rarely, and stale | Do not store these from the email as facts. |
| "Operated by" | Sometimes, in small print | Useful as a cross-check, not as a key. |
Store four things as the identity of the flight: carrier, number, local departure date, origin. Store the destination too. Treat every other field as a display value that will be replaced.
The date is the easy part, for once
FlightByte addresses a flight by the local departure date at the origin. A confirmation email states exactly that date, in the passenger's terms. There is no UTC conversion to get wrong here, and a 23:50 departure that lands tomorrow is still today's flight.
Step 1: confirm it, if the calendar allows
A lookup turns the pair into a full record:
curl "https://api.flightbyte.dev/v1/flights/TK/1848/2026-09-12?origin=ATH" \
-H "x-api-key: $FLIGHTBYTE_KEY"Inside the live window, which runs from a few days back to a couple of days ahead, this returns the snapshot: terminals, gates when assigned, the aircraft type and tail, the operating airline, every codeshare number, and times in both UTC and airport-local form. That is enough to replace everything the email guessed at.
Outside the window the answer is 404. That is not an error in your parser. Most bookings are forwarded weeks or months ahead, and no operational record exists yet. The rule is simple:
- Within the window: look up, store the snapshot's display fields, mark the flight confirmed.
- Beyond it: keep what the email said, mark the flight unconfirmed, and let the subscription confirm it later.
Do not poll a future flight waiting for it to appear. That is what a pending subscription is for, and it is the next-to-last step here.
Step 2: pick the leg
A flight number is not a flight. TK1 is SAW to AMS in the morning and IST to JFK in the afternoon, same date. A lookup without a selector describes one of them and lists all of them:
{
"flightKey": "TK1:2026-09-12@SAW-AMS",
"alternates": [
{ "departureIata": "SAW", "arrivalIata": "AMS", "scheduledDepartureLocal": "08:45", "selected": true },
{ "departureIata": "IST", "arrivalIata": "JFK", "scheduledDepartureLocal": "13:40", "selected": false }
]
}You never need to guess, because the email told you the origin. Pass origin on every lookup and every subscription. If the origin you parsed matches nothing, the lookup answers 404 and names the legs that exist, which is a strong signal that the parse was wrong and worth a second look before the user sees it.
The resolved leg has a canonical flightKey, for example TK1:2026-09-12@IST-JFK. Use that as the identity of the flight in your own tables from the moment you have it. The ambiguity guide covers the odd cases.
Step 3: one aircraft, one watch
Two of your users are on the same aircraft. One booked with Lufthansa and forwarded LH8320. The other booked with United and forwarded UA906. Parsed naively those are two flights, two subscriptions, two line items, and two sets of notifications that say the same thing a second apart.
The snapshot gives you what you need to notice:
{
"carrier": { "iata": "LH", "name": "Lufthansa" },
"operatedBy": "United Airlines",
"codeshares": [
{ "carrier": "UA", "name": "United Airlines", "flightNumber": "906" },
{ "carrier": "AC", "name": "Air Canada", "flightNumber": "5310" }
]
}Keep an alias table next to your flights:
create table watched_flights (
id text primary key, -- your own id
flight_key text unique not null, -- from the snapshot
subscription text, -- sub_… once created
local_date date not null,
origin text not null
);
create table flight_aliases (
carrier text not null,
flight_number text not null,
local_date date not null,
origin text not null,
watched_flight text not null references watched_flights(id),
primary key (carrier, flight_number, local_date, origin)
);When a snapshot arrives, insert one alias for the number you asked with and one for every entry in codeshares[], all pointing at the same watched flight. When the next booking is parsed, check the alias table first. UA906 on that date from that origin is already there; attach the user to the existing flight and do not subscribe again.
Subscribing with either number watches the same aircraft, so it does not matter which user's number created the watch. For display, show each user the number on their own booking and mention the operator when operatedBy is present, because that is the name on the board they will be standing under.
Unconfirmed flights cannot be deduplicated yet
A booking months out has no snapshot and therefore no codeshare list. Two users on one aircraft under two numbers will look like two flights until the first of them is confirmed. Merge them then: the alias insert will collide, and the collision is your signal.
Step 4: subscribe when it is time
Subscriptions can be created up to seven days before departure. A daily job is all it takes:
for (const flight of await flightsDepartingWithin(7 /* days */, { unsubscribed: true })) {
const res = await fetch("https://api.flightbyte.dev/v1/subscriptions", {
method: "POST",
headers: {
"x-api-key": process.env.FLIGHTBYTE_KEY!,
"content-type": "application/json",
"idempotency-key": `watch:${flight.id}`,
},
body: JSON.stringify({
carrier: flight.carrier,
flightNumber: flight.number,
date: flight.localDate,
origin: flight.origin,
webhookUrl: "https://api.yourapp.com/hooks/flights",
secret: process.env.FLIGHTBYTE_WEBHOOK_SECRET,
delayThresholdMinutes: 10,
metadata: { watchedFlight: flight.id },
}),
});
const sub = await res.json();
await saveSubscription(flight.id, sub.id, sub.status, sub.flightKey);
}Three details carry weight:
- The idempotency key makes the job safe to rerun. The same key returns the same subscription.
metadatacomes back in every webhook. It is how a delivery finds its row without a lookup by flight number.statusmay bepending. If the airline has not published the flight yet, the subscription waits, is checked on a slow schedule, and sendssubscription.activatedwith the snapshot the moment it appears. That event is where an unconfirmed flight becomes confirmed: write the display fields, insert the aliases, merge any duplicate the aliases reveal.
If the date passes and the flight never appears, subscription.completed arrives without a flight. Tell the user the booking could not be matched rather than showing a flight that silently never updates.
Step 5: keep the itinerary honest
From here the receiver does the work. Each flight.updated delivery carries the full snapshot plus the list of changes, so the trip screen can be updated from the same event that triggers the push. Two rules keep the itinerary from lying:
- Display times come from
times.*.local. Never convert a UTC value on the way to the screen; the snapshot already carries the airport-local time and the zone abbreviation. - The newest snapshot wins. Retries can arrive after a newer event. Compare
flight.fetchedAtbefore overwriting.
The fan-out from one delivery to every device on the flight is its own guide. Connections, where the interesting fact lives between two flights, are another.
What this costs
A watched flight is one subscription, counted when it is created. It is not a user and not a booking. With the alias table doing its job, the number that reaches your bill is the number of distinct aircraft your users board in a month:
| Distinct flights a month | Plan | Monthly |
|---|---|---|
| 100 | Hobby | $9 |
| 500 | Starter | $19 |
| 2,000 | Growth | $49 |
| 5,000 | Scale | $99 |
| 10,000 | Scale plus 5,000 extra at $0.02 | $199 |
Lookups are separate, cached, and included by the tens of thousands, so confirming a flight at import and refreshing the trip screen do not move that number. The full table is on the pricing page.