Guides · September 12, 2026 · 5 min read

TK1 flies twice a day: the flight number ambiguity nobody documents

A flight number is not a flight. Some numbers operate two legs on one date, most APIs hand you both and let you guess, and the guess is silent. What the ambiguity looks like in the data, how FlightByte refuses to guess, and the three lines of code that handle it.

On most days, Turkish Airlines flight 1 leaves Istanbul Sabiha Gökçen for Amsterdam in the morning and Istanbul Airport for New York in the afternoon. Two aircraft, two crews, two sets of passengers, one number. Ask a flight API "where is TK1 today" and the honest answer is a question back: which one?

Very few APIs ask it. They return an array, or the first match, and the integration that called them watches whichever leg came first. Nothing fails. A driver goes to the airport for a New York arrival that is actually a morning flight from Amsterdam, and the bug is filed as "the flight data was wrong".

Why numbers repeat

A flight number is a marketing label for a route in a timetable. Airlines have a few thousand of them and many more legs to cover, so the same number is reused:

  • Every day. TK1848 tomorrow is a different aircraft from TK1848 today. Every lookup needs a date, and the date is the local departure date at the origin, which matters for anything that leaves near midnight.
  • Twice on one day. The TK1 case: two unrelated legs sharing a number, usually a short morning rotation and a long-haul afternoon one.
  • Through-flights. One number, one aircraft, two legs with a stop: A to B, then B to C. The feeds publish both legs under the same number and date. A passenger on the second leg wants the second leg's arrival; a passenger travelling A to C wants both.
  • Codeshares. Not a repeat of the same number, but the same aircraft under several numbers, which is the mirror-image problem. Codeshare flight covers it.

How often it bites depends on which airlines your customers fly. It is not rare: any hub carrier with a mix of domestic rotations and long-haul flights has numbers like this, and a booking system that sees a few hundred flights a week meets one.

What the ambiguity looks like

Look up TK1 for a date without saying which leg, and FlightByte describes the upstream's default leg while listing every leg under alternates, with selected: true on the one described:

{
  "flightKey": "TK1:2026-09-12@SAW-AMS",
  "departure": { "airport": { "iata": "SAW" } },
  "arrival": { "airport": { "iata": "AMS" } },
  "alternates": [
    { "providerFlightId": "1407278900", "departureIata": "SAW", "arrivalIata": "AMS", "scheduledDepartureLocal": "08:45", "scheduledArrivalLocal": "11:30", "selected": true },
    { "providerFlightId": "1407278912", "departureIata": "ISL", "arrivalIata": "JFK", "scheduledDepartureLocal": "14:10", "scheduledArrivalLocal": "18:05", "selected": false }
  ]
}

That is enough to build a picker, and it is all a lookup will do on its own: a lookup is a question, and a question can come back with a default. A subscription is a commitment to watch one aircraft for a day, and there FlightByte will not guess.

The refusal

Subscribe to an ambiguous number without a selector and the answer is a 400 with the legs:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "TK1 operates 2 legs on 2026-09-12. Pass \"origin\" to choose one.",
  "legs": [
    { "origin": "SAW", "destination": "AMS", "carrier": "TK", "scheduledDepartureLocal": "08:45", "flightId": "1407278900" },
    { "origin": "ISL", "destination": "JFK", "carrier": "TK", "scheduledDepartureLocal": "14:10", "flightId": "1407278912" }
  ]
}

This is the design decision the guide is about. An error at subscription time costs the caller one retry. A silent wrong guess costs a passenger a driver. Between an integration that fails loudly on a Tuesday and one that fails quietly on a Saturday night, the loud one is the kind you can fix.

Pass a selector and the refusal goes away:

SelectorUse whenExample
originAlmost always. A passenger knows where they are leaving from."origin": "ISL"
destinationYou know where they are going and not where they start; hotels and pickups."destination": "JFK"
flightIdYou already have a leg from a lookup's alternates and want that exact one."flightId": "1407278912"

Airport codes are accepted in the IATA form and in the upstream's own form when they differ (Istanbul Airport is IST and ISL), so whichever your booking system stored will match. Once the leg is resolved the flight key gains its suffix, TK1:2026-09-12@ISL-JFK, and that is the string to store with the booking. Flight number vs flight ID has the vocabulary.

Selectors that match nothing

A selector that matches no leg is a 404, and the message names what exists: TK1 on 2026-09-12 has no leg matching origin=LHR. Available: SAW-AMS, ISL-JFK. The same message shape covers a typo in the airport code and a booking for a route the number no longer flies.

Handling it in three lines

The right time to resolve a leg is when the booking is made, because that is when a human who knows the answer is present. Most booking flows already have the origin; pass it and the 400 never happens. For flows that do not, catch it and ask:

const res = await fetch("https://api.flightbyte.dev/v1/subscriptions", {
  method: "POST",
  headers: { "x-api-key": KEY, "content-type": "application/json", "idempotency-key": `booking-${booking.id}` },
  body: JSON.stringify({ carrier, flightNumber, date, webhookUrl, secret, metadata: { bookingId: booking.id } }),
});

if (res.status === 400) {
  const { legs } = await res.json();
  if (legs) {
    // Two legs, one number. The booking's airport decides; otherwise ask the customer.
    const pick = legs.find((leg) => leg.destination === booking.arrivalAirport) ?? (await askCustomer(legs));
    return subscribe({ ...booking, origin: pick.origin });
  }
}

The legs array is designed to be shown to a person as-is: origin, destination and a local departure time are what a customer needs to say "the afternoon one".

Through-flights

A number with a stop produces two legs that are not alternatives but a sequence: LH123 from Frankfurt to Nairobi, continuing to Dar es Salaam under the same number. The alternates list shows both; the selectors work the same way. Which leg to watch depends on what you need:

  • A pickup at the final destination: subscribe with destination set to it. That leg's flight.arrived is the one that matters.
  • A passenger boarding at the origin: subscribe with origin. Gate changes and delays at the first airport are on that leg.
  • Both: two subscriptions, one per leg, with the same metadata. Two watched flights, because they are two aircraft movements.

The date boundary

Ambiguity's quieter cousin. A flight that departs Istanbul at 23:50 and lands in Bangkok the next afternoon is, to every feed, the flight of the day it left. A booking system that stores the arrival date and subscribes with it asks about a flight that does not exist, gets a pending subscription that waits for a flight that never appears, and receives subscription.completed a day late.

Store the local departure date at the origin. The snapshot's date and flightKey carry it, and every time in the snapshot has a local form so the arrival date is never in doubt on the other side.

What other APIs do

For the record, and for migrations:

  • aviationstack returns both legs in the data array for the number and date. Nothing marks one as default; your code picks.
  • AeroDataBox returns an array too, ordered by time; the same applies.
  • AeroAPI returns about fourteen days of the ident, each with a fa_flight_id; you filter by date and origin, and its own documentation recommends the ICAO ident to reduce ambiguity.

None of them is wrong to return a list. The difference is what happens when you commit to watching one: a list is fine for a question and dangerous for a subscription, and a subscription that refuses to guess is the only kind that never watches the wrong aircraft.

More guides

Get started

Watch your first flight tonight.

Nine dollars, a hundred flights, every event, signed. The receiver above is the whole integration.

A free key in a minute, no card. On a paid plan? Request a key