Skip to content

Extracting Structured Fields From a Flight Itinerary Beyond Calendar Events

11 min read · updated August 11, 2026

Turning an itinerary into calendar entries is a solved and boring problem. The interesting fields are the ones a traveller argues about at a gate: what class the ticket actually is, how many bags it includes, and which of the several long numbers on the page identifies the ticket rather than the booking.

The fields that are not the schedule

An itinerary email carries two layers. The scheduling layer — flight numbers, airports, dates, times — is what calendar integrations consume and it is comparatively well behaved. The commercial layer is what an expense system, a corporate travel policy check or a refund calculation needs, and it is where the ambiguity lives: fare conditions, class of service, baggage entitlement, ticket numbers, coupon status, fare construction and taxes.

That second layer is worth extracting separately because it answers different questions and changes at different times. A schedule change rewrites the first layer and leaves the second alone. A voluntary change rewrites both and issues a new ticket number. Keeping them in one flat record means a schedule-change email overwrites commercial fields it never mentioned.

Cabin, booking class and fare basis are three things

“Class” on an itinerary can mean any of three fields, and collapsing them is the most common modelling error on this document.

  • Cabin is the physical product: Economy, Premium Economy, Business, First. It is what the traveller experiences and what most travel policies are written against. There are four or five of them.
  • Booking class, the reservation booking designator, is a single letter identifying the inventory bucket the seat was sold from. There are more than twenty in use per airline, several map to the same cabin, and they differ between airlines — a letter that is full-fare economy on one carrier is a discounted bucket on another.
  • Fare basis code is an alphanumeric string such as QLXNC7 that identifies the specific fare and its conditions: changeability, refundability, minimum stay, mileage accrual. It is the field that actually determines what the ticket permits, and it is airline-specific with no public universal decoder.

The practical consequence: a policy rule written as “economy only” must test the cabin, and a refund calculation must read the fare basis, and neither can be answered from the other. Extract all three into named fields, allow each to be null, and never derive the cabin from the booking class with a hardcoded letter map. If you need that mapping, it comes from the airline’s own distribution data and it changes.

The ticket number is the stable key

Itineraries show at least two long identifiers and travellers use the words interchangeably. The record locator, or PNR, is the six-character alphanumeric booking reference. The ticket number is a thirteen-digit number, and they behave completely differently.

A record locator identifies a booking within one airline’s reservation system, and airlines recycle them. The same six characters will belong to a different passenger next year. It is a fine lookup handle for the next few weeks and a poor primary key for anything you keep, exactly as a licence plate is a poor key for a vehicle while the VIN is a good one.

The thirteen-digit ticket number is structured: a three-digit prefix identifying the validating carrier, followed by a ten-digit document number whose final digit is a modulus-7 check digit. IATA assigns the carrier prefixes and publishes the list; resolve codes against that list rather than a hardcoded map, since prefixes are reassigned when airlines merge or fail.

The check digit is genuinely useful, and makes this a checksum-validated identifier field in the same sense a VIN is — with one important caveat about its definition. The arithmetic is a plain remainder, not a weighted sum:

ticket        016 765432109 ?
              ^^^ carrier prefix

interpretation: check digit = (preceding 12 digits) mod 7

  016765432109 / 7  =  2,395,061,729  remainder 6
  check digit = 6
  full ticket number = 0167654321096
The thirteen-digit structure and the fact that the last digit is a modulus-7 check digit are well established. The exact span of digits the modulus is taken over is defined by IATA resolution, and published descriptions of it differ. Validate the interpretation above against a corpus of ticket numbers you know to be good before you let a validator reject anything on the strength of it, and read the resolution rather than a summary of it.

Alongside the number, each flight coupon carries a status — OPEN FOR USE, CHECKED IN, USED/FLOWN, REFUNDED, EXCHANGED, VOID — and the status is per coupon, not per ticket. A ticket where the outbound coupon is flown and the return is open is an entirely normal state that a single ticket-level status field cannot express.

Baggage allowance is per segment, not per trip

Baggage entitlement is printed as a short string such as 1PC, 2PC or 23KG, and it appears against each segment and each passenger rather than against the trip. Two conventions coexist: the piece concept, which counts bags with a weight limit per bag, and the weight concept, which allows a total weight regardless of how it is divided. An itinerary can carry both on different segments.

It genuinely varies within one booking. On an interline itinerary the allowance is determined by rules about which carrier’s baggage provisions apply to the journey, and a codeshare segment operated by a partner can carry a different allowance from the marketing carrier’s own. Flattening this to a single trip-level baggage_allowance string discards the variation that is the entire reason someone is reading the field.

Extract it as a list keyed by passenger and segment, keep the raw string, and parse the concept and quantity into separate fields with the unit preserved. 2PC and 32KG are not comparable quantities and there is no conversion between them.

"baggage": [
  { "passenger_ref": "P1", "segment_ref": "S1",
    "raw": "1PC", "concept": "piece", "quantity": 1, "unit": null },
  { "passenger_ref": "P1", "segment_ref": "S2",
    "raw": "23KG", "concept": "weight", "quantity": 23, "unit": "kg" }
]

Every time on the page is a different zone

A segment line reads “LHR 11:15 → ORD 14:05” and those two times are in different zones, both local, neither stated. The arrival time is not later than the departure time by three hours; it is later by nine. Normalising either to UTC using one assumed zone produces a flight that appears to arrive before it left, or one that takes half a day.

Airline itineraries are the purest case of the rule set out on the reservation confirmation page: store the wall time and the zone separately, and derive the instant. Here the zone comes from the airport code, which is an excellent zone key — IATA three-letter codes map cleanly to IANA zones, and that mapping is maintained in public datasets. Resolve departure against the origin airport and arrival against the destination airport, independently.

Two further details on this document. A date-change indicator, usually printed as a superscript +1 or +2 next to the arrival time, means the arrival falls on a later calendar day; dropping it produces an arrival that precedes departure and is easy for an extractor to miss because it is typographically tiny. And an elapsed flight time printed on the itinerary is a useful cross-check: derive the duration from your two resolved instants and compare it to the printed one. If they disagree by an hour, you have a zone or daylight-saving error, and this check will find it without any reference data at all.