Flight Price Scraping: Hold Four Inputs
Hold currency, locale, point of sale and device constant so a fare difference is a price difference, then record the four inputs beside every quoted fare.
BotBrowser Team
Want the structured docs for Network?
This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.
Flight price scraping only produces a usable series when the environment behind each request is held constant. A fare is quoted for a currency, a locale, a point of sale and a device, and changing any one of them can change the number without anything about the market changing. BotBrowser documents profile-backed locale, timezone and language inputs with geo-aligned egress, so a capture can declare the environment it represents. It does not supply fares, does not book, does not control a carrier's pricing logic and does not guarantee that a quoted price will be returned again. Where a licensed fare feed or agency API exists, that is the route to use.
TL;DR
- A fare is quoted for a specific currency, locale, point of sale and device. Hold all four, or the difference you measure is partly an environment difference.
- Currency is a request or account setting, not a browser profile value. Locale and language are profile-backed. Point of sale is a site parameter that has to agree with the egress region and the account.
- Record the four inputs beside every quoted fare, not once per run.
- Classify a delta by which inputs changed. A currency change is not a fare movement, however large the number.
- Prefer a licensed fare feed or agency API where one exists. A browser run does not create permission to monitor, and it cannot book.
Contents
- Four inputs that move a fare
- Which of the four a profile actually holds
- Record the four inputs per capture
- Read a price delta by which input changed
- When a licensed fare feed is the answer
- Practical conclusion
Four inputs that move a fare
A flight search request carries more than an origin, a destination and a date. It carries a currency, a language preference, a market the itinerary is priced for, and a device class that may select a different offer set. Those four are not interchangeable, and a monitoring series that varies them measures its own configuration as much as it measures fares.
| Input | Where it is set | What it can change |
|---|---|---|
| Currency | Request parameter or account setting | The quoted amount and how it is rounded |
| Locale | Profile-backed locale and language list | Page language, formatting, sometimes the offer set |
| Point of sale | Site parameter, egress region and account, together | The market whose inventory and rules apply |
| Device | Viewport and profile device values | Layout, and sometimes a mobile-only offer |
schema.org's Offer type gives the vocabulary a useful shape here. It defines price and recommends pairing it with priceCurrency using an ISO 4217 code rather than an ambiguous symbol, and it warns against using separators inside the numeric value. That guidance is about publishing an offer, and it maps directly onto the capture problem: a fare recorded as $412 is not a fact, while 412 USD is a comparable observation.
The second and third inputs are the ones that get conflated. A locale is what the browser declares. A point of sale is what market the site prices for, and it is often derived from a combination of a site parameter, the request's apparent region and the account's own setting. Making the browser declare German does not by itself make the fare a German point-of-sale fare, and exiting from a German address does not by itself set the currency.
Currency deserves a paragraph of its own because it is the input most often left implicit. A site may accept an explicit currency parameter, derive one from the point of sale, or read it from an account preference, and the three paths can disagree. When they do, the page usually shows one currency and the checkout shows another, or the same currency with a different conversion rate applied. Record the currency the page displayed, not the one the request asked for, and record the point of sale beside it, because the pair is what makes the amount reproducible.
Rounding is a smaller but persistent source of noise. A fare converted from one currency to another is rounded at a currency-specific precision, and some currencies round to whole units while others keep two decimals. Two captures of the same underlying fare in the same currency should therefore match exactly; if they differ by a fraction of a unit, the difference usually comes from a conversion rather than from a fare change, and the record should say so rather than treating the fraction as movement.
Taxes and fees belong to the same family of problems. A displayed amount may include them, exclude them, or include a subset that depends on the point of sale, and a change in the included set looks exactly like a price change. Record what the displayed amount includes when the page states it, and keep that field with the amount. A series that switches from a tax-inclusive display to a tax-exclusive one will show a step change that has nothing to do with the fare.
Which of the four a profile actually holds
BotBrowser's timezone, locale and language documentation describes automatic derivation from the proxy exit IP when a proxy is configured, plus manual overrides of timezone, locale and languages, with the manual overrides gated behind an ENT Tier1 license. The GeoIP documentation describes translating the exit address into geographic defaults, and states the accuracy boundary plainly: country-level mapping is generally strong while city-level mapping can resolve to a nearby city or region.
That covers the locale half of the table and part of the point of sale. It does not cover currency, and it does not fully cover point of sale either, because a site may accept a currency parameter independently of the language it serves, a site behaviour rather than a documented browser input. A monitoring design that assumes the profile handles everything will therefore hold two of four inputs and believe it holds four.
| Input | Held by the profile | Needs an explicit check |
|---|---|---|
| Currency | No | The request parameter or account setting |
| Locale | Yes, when configured | The declared list the page actually reports |
| Point of sale | Partly | The site's own indication of the market |
| Device | Yes, when configured | The viewport and the device values the page reports |
The practical consequence is that the check has to be per capture, not per configuration. A run configured correctly at the start will drift as soon as a request parameter is edited, an override is added, or a proxy is changed for one region. Recording the four inputs at the moment of the quote is the only way to know which of them applied.
Device class is the input that hides best. A desktop viewport and a mobile viewport can return different offer sets, and some carriers expose a mobile-only fare that simply does not exist on the desktop path. That is not a price change between two observations of the same offer; it is a different offer. Record the viewport alongside the device class so a later reader can tell whether the two captures used the same one, and treat a change in the offer set as a device effect rather than a fare movement.
There is also a time dimension inside the environment itself. A capture taken during a sale window and one taken an hour later can differ because the sale ended, which is a source-side change that the four inputs correctly report as unchanged. That is the right outcome: the record is doing its job by ruling out the capture as the explanation. The mistake to avoid is the reverse, blaming the source for a difference that a currency switch explains.
A last habit is worth stating because it is cheap. Capture the four inputs before the fare page loads rather than after, and keep the pre-load values in the record. A page can change a locale or a currency during its own client-side initialisation, so a value read after the quote may describe the final state rather than the state the request carried. Recording both, when they differ, turns a confusing difference into a documented transition.
Record the four inputs per capture
A capture record for fare monitoring is small. It needs the itinerary, the quoted amount with its currency, the four inputs, and the observation time. Everything else is supporting detail.
| Field | Content | Why it is in the record |
|---|---|---|
itinerary_key | Route, dates and cabin class as requested | Defines what is being compared |
amount | Numeric value with a decimal point | Comparable only with a currency |
currency | ISO 4217 code | Prevents a symbol being read as an amount |
accept_language | Declared language list at capture time | Shows which locale applied |
point_of_sale | Market the site indicated it priced for | Separates the market from the language |
device_class | Desktop or mobile, with viewport | Separates layout from fare |
observed_at | Capture time in UTC | Orders observations and exposes schedule changes |
alignment | aligned or the named input that differed | Keeps an incomparable row out of a series |
Two entries in that table are easy to record wrongly. amount should be a number with a decimal point rather than a formatted string, because a string carries the locale's separator convention and becomes ambiguous as soon as the series spans two locales. point_of_sale should be what the site indicated, not what the operator intended: an intent the site did not honour is exactly the case worth catching.
The record builder below compares two captures of the same itinerary and reports which inputs held. The fixture is constructed.
// Constructed fixture: two captures of one itinerary, one input changed.
const captureA = {
itineraryKey: 'YYZ-LHR 2026-12-10 Y',
amount: 812.4,
currency: 'CAD',
acceptLanguage: 'en-CA,en',
pointOfSale: 'CA',
deviceClass: 'desktop',
observedAt: '2026-11-10T09:00:00Z',
};
const captureB = {
itineraryKey: 'YYZ-LHR 2026-12-10 Y',
amount: 512.1,
currency: 'EUR',
acceptLanguage: 'en-CA,en',
pointOfSale: 'CA',
deviceClass: 'desktop',
observedAt: '2026-11-10T09:00:30Z',
};
const FIELDS = ['currency', 'acceptLanguage', 'pointOfSale', 'deviceClass'];
const changed = FIELDS.filter(f => captureA[f] !== captureB[f]);
const alignment = changed.length === 0 ? 'aligned' : `input_changed:${changed.join(',')}`;
const lines = [
`itinerary=${captureA.itineraryKey}`,
`a=${captureA.amount} ${captureA.currency} b=${captureB.amount} ${captureB.currency}`,
`currency=${captureA.currency}->${captureB.currency} locale=${captureA.acceptLanguage}`,
`pos=${captureA.pointOfSale} device=${captureA.deviceClass}`,
`changed=${changed.join(',') || 'none'} alignment=${alignment}`,
`observed_at=${captureA.observedAt}->${captureB.observedAt}`,
];
console.log(lines.join('\n'));
The constructed output below is the point of the exercise. The amount fell by roughly 37 percent, and the classification says that the drop is not a fare movement at all.
itinerary=YYZ-LHR 2026-12-10 Y
a=812.4 CAD b=512.1 EUR
currency=CAD->EUR locale=en-CA,en
pos=CA device=desktop
changed=currency alignment=input_changed:currency
observed_at=2026-11-10T09:00:00Z->2026-11-10T09:00:30Z
Read a price delta by which input changed
The size of a difference says nothing about its cause. The useful question is which of the four inputs were held, because that determines what the pair of captures can support.
| Inputs held | Observation | What the delta supports |
|---|---|---|
| All four identical | Amount differs | A comparable price change in that environment |
| Currency differs, others held | Amount differs | A currency conversion or repricing, not a fare move |
| Point of sale differs, others held | Amount differs | Two markets observed; neither is the other's baseline |
| Device class differs, others held | Amount differs or offer set changes | A device-dependent offer, not a fare change |
The first row is the only one that supports a fare series. The second row is the most common error in practice, because a currency switch changes the number by a large factor and the resulting chart looks like a dramatic movement. The third row is a market comparison that has to be built as two series, not one. The fourth row is where a mobile-only offer can masquerade as a price change.
Two further cautions belong to the reading rather than to the record. First, a quote is not a bookable price: taxes, fees and availability can change between the quote and the checkout page. Second, a repeated capture of an unchanged environment can still differ because the carrier's own inventory or fare rules changed, which is a real observation and should be labelled as a source-side change rather than as a capture error.
The price-monitoring architecture covers scheduling and change detection for a general price series. What travel adds is that the four inputs are more independent of each other than they are in retail, where a single product page usually implies one currency and one locale. A profile-to-route consistency record is the right companion for the egress half of point of sale. A calendar of availability needs a date key rather than a product key, and the availability model sets out how to keep that calendar comparable across captures.
When a licensed fare feed is the answer
Most carriers and large agencies offer a licensed route: a fare distribution feed, an agency API, or an agreed data export. Those routes give the same itineraries with a defined schema, a stated update cadence and a licence that says what the data may be used for. When one exists, a browser run is usually the wrong instrument, because it produces a less stable series with more legal exposure for a source that already offered a supported path.
Where no such route exists, the honest question is what authorizes the observation. A working browser session is not that answer. The applicable carrier or agency terms decide, and they frequently restrict automated access, resale of fare data and the display of cached prices. Record the basis for the run alongside the captures so a later reader inherits the answer rather than the assumption.
schema.org's FlightReservation type is a reminder of the same boundary from the other side. It defines a vocabulary for a reservation, with bookingTime, passenger fields, and reservationFor pointing to the flight. It describes how a reservation may be represented; it says nothing about whether a fare may be monitored, and nothing about what a quoted price will be at checkout.
The vocabulary is still useful for naming the things a capture keeps separate. A reservation is not an offer, and an offer is not a fare rule. When a record mixes the three, a later comparison cannot tell whether the carrier changed a price, the itinerary, or the conditions attached to it. Keep the itinerary key describing what was requested, the amount and currency describing what was quoted, and the four environment inputs describing how the quote was obtained.
FAQ
Is a currency change a price change?
No. It changes the unit of the amount, and usually the rounding. A series that switches currency mid-flight measures the switch, so keep one currency per series or convert with a stated rate and record the conversion.
Can a browser profile hold the point of sale?
Only partly. The profile controls locale, language and, through the proxy, the apparent region. A site's point of sale is often a separate parameter or account setting, so verify what the site indicated rather than what the operator intended.
Does the same itinerary always return the same price?
No. Inventory and fare rules change, and the quote is not the checkout total. A repeated capture in an unchanged environment can still differ, and that difference belongs to the source.
Does BotBrowser supply fares or book flights?
No. It provides documented browser inputs so a capture can declare its environment. It does not supply fare data, does not book, and does not guarantee a quoted price.
Sources
- schema.org FlightReservation
- schema.org Offer
- MDN Accept-Language
- BotBrowser timezone, locale and language
- BotBrowser GeoIP database
Practical conclusion
Decide which currency, locale, point of sale and device the series represents before the first capture. Verify all four per capture, because a profile holds two of them fully and one only partly, and record them beside the quoted amount and the observation time. Classify every delta by which input changed, and treat a currency switch as an environment change rather than a fare movement. Where a licensed fare feed or agency API exists, use it and keep the browser run out of the design.
This article is part of the guide: Proxy Configuration Overview: A Browser Network Decision Map
Related Articles
Ecommerce Price Monitoring: The Browser Layer
NetworkBooking.com Scraping: Availability as a Calendar
DeploymentCompetitor Price Monitoring: Browser Pipeline Design
DeploymentFingerprint Protection for E-Commerce Price Monitoring
NetworkProxy Rotation for Web Scraping: Session Boundaries
NetworkScraping Rate Limits: 429, Retry-After and Concurrency
Run the same check in BotBrowser
Load a profile, keep the signals you just read about consistent inside the browser, and confirm the result on your own machine before a rollout.