I would design the record before choosing a travel API or public-page collector. A flight price becomes comparable only when its itinerary and fare assumptions are preserved.
My default output would be a time-stamped observation. A transaction-ready offer would have a separate state and confirmation path.
The record carries the query
This illustrative example uses fictional itinerary details:
{
"observation_id": "example-observation",
"source": "example-public-search",
"observed_at": "2026-10-01T12:00:00Z",
"query": {
"origin": "AAA",
"destination": "BBB",
"departure_date": "2026-11-01",
"return_date": "2026-11-08",
"adults": 1,
"market": "example-market",
"currency": "USD"
},
"itinerary_key": "example-reviewed-itinerary",
"cabin": "economy",
"baggage_description": "Not specified in this sample",
"price": {
"amount": "500.00",
"currency": "USD",
"basis": "displayed_trip_total",
"taxes_included": null
},
"status": "observed",
"revalidated_at": null
}
I represent monetary values using a decimal-safe storage type, not a binary floating-point value. An unknown tax or baggage condition should remain unknown.
The example airports and amount are placeholders. They are not a collected fare or a booking offer.
Identity requires more than flight number
My itinerary key would reflect legs, dates and times with timezone context, airports, and relevant carrier information. The comparison key would additionally account for passenger assumptions, cabin, and the fare bundle required by the use case.
I preserve operating and marketing carriers separately when the source exposes them. Similarly, I retain the provider's native offer ID without assuming it is stable across repeated searches.
Different offers for the same flight can have different conditions. I don't merge them merely because the schedule matches.
I use an explicit lifecycle
For page observations, observed means the collector captured a displayed result. It does not mean the amount is available for purchase.
For an authorized booking integration, I add states such as revalidated, expired, and unavailable, based on the provider's actual responses. The Duffel offer model is one reference for understanding that different contract.
I would never set revalidated simply because I fetched the same search page again. Confirmation needs the appropriate authoritative flow.
My fixture matrix exercises price mapping
I would include a direct trip, a connection, a return journey, different passenger counts, baggage variations, multiple currencies where relevant, and no results.
For rendered pages, I also test late-loaded cards, market redirects, a consent screen, and a layout containing fares beside ancillary fees. The parser must attach the correct amount to the correct itinerary.
A no-results response and a failed extraction get different states. Otherwise a parser regression can look like disappearing inventory.
I compare providers under controlled conditions
I would run equivalent route queries close in time, preserve all relevant parameters, and compare only records meeting the same eligibility rules. My report would include missing comparable results, not merely the rows each source returned.
I would distinguish observed fare, revalidated offer, and final checkout amount in downstream interfaces. A monitoring dataset may only have the first, which is still useful when labeled accurately.
For research collection, I would exclude traveler names, account-only data, payment information, and booking records. Those belong to a separate authorized transaction system.
I accept the first collector when it can explain each amount and its missingness on this fixture matrix. Only after that would I increase routes, schedules, or source coverage.
Top comments (1)
Keeping taxes_included as null is a useful distinction. I'd also store the source URL and locale actually reached after a market redirect, not just the requested market. For BrowserAct Cloud extraction, that would make a later currency/tax mismatch traceable without treating another page fetch as offer revalidation.