Google Flights Scraper — Fares, Flight Numbers, Layovers, CO2
Pricing
$0.30 / 1,000 per flight itineraries
Google Flights Scraper — Fares, Flight Numbers, Layovers, CO2
Search Google Flights by route and date and get every itinerary on the board: price, airline, per-leg flight numbers, layovers, stops, duration, baggage and CO2. One-way and round-trip. Pay only for itineraries returned — $0.30 per 1,000.
Pricing
$0.30 / 1,000 per flight itineraries
Rating
0.0
(0)
Developer
Scrapers Delight
Maintained by CommunityActor stats
1
Bookmarked
1
Total users
1
Monthly active users
10 days ago
Last modified
Categories
Share
✈️ Google Flights Scraper — fares, per-leg flight numbers, layovers and CO2
Give it a route and a date. Get back every itinerary Google Flights puts on that fare board — the price, the airline, the actual flight number of every leg, each layover with its airport and duration, stops, total duration, baggage allowance and the full CO2 block.
No API key. No login. No browser. One HTTP request per board, parsed out of Google's own server-rendered HTML.
$0.30 per 1,000 itineraries. You pay per itinerary returned — nothing else.
🎯 The two things this does that the lane does not
1. Per-leg flight numbers, without a second request
Most Google Flights scrapers hand you "1 stop, Alaska, 10 hr 12 min" and stop there. This one gives
you the itinerary Google itself files with the Travel Impact Model:
"legs": [{ "origin": "JFK", "destination": "SEA", "carrierCode": "AS", "flightNumber": "21", "date": "2026-10-15" },{ "origin": "SEA", "destination": "LAX", "carrierCode": "AS", "flightNumber": "487", "date": "2026-10-15" }],"flightNumbers": ["AS21", "AS487"],"layovers": [{ "ordinal": "1 of 1", "airportCode": "SEA", "airportName": "Seattle-Tacoma International Airport","city": "Seattle", "durationText": "58 min", "durationMinutes": 58 }]
That is a real row from a real board. Flight numbers make a fare row joinable — to schedules, to OAG/Cirium data, to your own inventory — instead of just readable.
2. You are not billed twice for the same flight
Google renders every fare board twice. A JFK→LAX board contains 196 itinerary <li> elements
for 98 real itineraries — same ssk, same data-id, same aria-label, same layovers, rendered
into two sets of containers. Measured on four boards: 196→98, 178→89, 114→57, 96→48.
The obvious parser (cheerio → li.pIav2d → push) emits all 196 and bills you for all 196. This actor
deduplicates before anything is delivered or charged. On the validation run below: 400 raw rows
read, 75 delivered, 75 billed.
📦 What you get on every row — 65 fields
Search context
itineraryId · section (Google's own "Top flights" vs "Other flights" bucket) · rankInSection ·
searchOrigin · searchDestination · searchDepartureDate · searchReturnDate · tripType ·
cabinClass · passengers · currency · currencyName · searchUrl · tfsToken · scrapedAt
Price
price (numeric) · priceFormatted ("$204", "£1,455") · priceType (one way /
round trip total) · priceUnavailable · bookingToken · priceLevel (low/typical/high
from Google's Price-insights panel) · priceDeltaVsUsual (signed, e.g. -46) · bookingTip ·
resultsOnBoard
Airlines
airlines[] · airlineCodes[] (IATA, derived per leg — works on multi-carrier rows where the logo
is absent) · airlineLogoUrl · operatedBy ("Operated by Republic Airways as American Eagle") ·
isCodeshare · mixedCabin ("Business Class + Premium Economy")
Schedule
departureTime · departureDate · departureDateTimeIso · arrivalTime · arrivalDate ·
arrivalDateTimeIso · arrivalDayOffset (+1 / +2) · durationText · durationMinutes ·
originCode · originName · destinationCode · destinationName
Routing
stops · stopsText · layovers[] (ordinal, airportCode, airportName, city,
durationText, durationMinutes, transferFrom, transferTo) · longLayover ·
changeOfAirport · legs[] · flightNumbers[]
Baggage
carryOnBagsIncluded · checkedBagsIncluded · baggageNote
Emissions (the full block Google exposes)
co2Grams · co2Kg · co2TypicalGrams · co2SavingsGrams · co2PercentVsTypical ·
emissionsLabel · relativeEmissions (1 = low, 3 = high) · environmentalRanking ·
contrailsImpact · easaSafDiscountGrams · travelImpactModelUrl · seatingClassCode
📊 Measured field fill — from a real Apify run, not an estimate
Run t17MbzlTYmjUVhwdm, 2026-09-03. 3 boards, 75 itineraries delivered, SUCCEEDED in 11.4 s,
peak memory 415 MB, chargedEventCounts = 75.
Searches: JFK→LAX 2026-10-15 one-way (97 on the board), JFK→BKK 2026-11-10 one-way (58),
JFK↔LAX 2026-11-10/17 round-trip (45), each capped at 25 rows.
65 fields on every row. 56 of them at 100% fill.
| Field group | Fill |
|---|---|
ids, search context, currency, currencyName, searchUrl, tfsToken, scrapedAt | 100% |
price, priceFormatted, priceType, priceUnavailable, bookingToken | 100% |
airlines, airlineCodes, airlineLogoUrl, isCodeshare | 100% |
all 8 schedule fields + arrivalDayOffset, durationText, durationMinutes | 100% |
originCode/Name, destinationCode/Name, stops, stopsText | 100% |
legs, flightNumbers, longLayover, changeOfAirport | 100% |
all 12 emissions fields incl. travelImpactModelUrl | 100% |
priceLevel, resultsOnBoard | 100% |
layovers | 56.0% — nonstop rows have none, by definition |
carryOnBagsIncluded, checkedBagsIncluded, baggageNote | 66.7% — Google omits the baggage line on some international fares |
searchReturnDate, priceDeltaVsUsual, bookingTip | 33.3% — only on the round-trip search / only when Google shows that panel |
operatedBy | 1.3% — a codeshare marker; it is true data, not a gap |
mixedCabin | 0% on this run — economy boards don't mix cabins (measured 12 of 89 rows on a business board) |
Nothing above is padding: layovers is empty on nonstop rows because a nonstop flight has no
layover, and operatedBy is rare because codeshares are rare (12 of 98, 15 of 89, 2 of 48 and 1 of
57 rows on the four validation boards).
Also validated offline against 7 captured boards — exact row counts
| Board | Raw <li> | Unique itineraries | Parsed |
|---|---|---|---|
| JFK→LAX one-way (residential proxy) | 196 | 98 | 98 ✅ |
| JFK→LAX one-way (datacenter proxy) | 196 | 98 | 98 ✅ |
| JFK→LAX one-way (direct) | 196 | 98 | 98 ✅ |
| JFK↔LAX round-trip | 96 | 48 | 48 ✅ |
| JFK→BKK one-way | 114 | 57 | 57 ✅ |
| JFK→LHR business, GBP, 2 pax | 178 | 89 | 89 ✅ |
| LWS→BFF (no such route) | 0 | 0 | 0 ✅ clean empty |
488 rows, every itineraryId unique, £1,455 parsed as price: 1455 + currencyName: "British pounds".
🚀 How to use it
Simplest — one route
{ "origin": "JFK", "destination": "LAX", "departureDate": "2026-10-15" }
Evergreen (for a schedule — never goes stale)
{ "origin": "JFK", "destination": "LAX", "daysFromNow": 30, "maxResultsPerSearch": 25 }
Leave departureDate blank and the actor computes the date at run time, so a saved task or a nightly
schedule never starts searching a date in the past.
Round trip
{ "origin": "JFK", "destination": "LHR", "departureDate": "2026-11-10", "returnDate": "2026-11-17" }
Many routes and dates in one run
{"searches": [{ "origin": "JFK", "destination": "LAX", "departureDate": "2026-10-15" },{ "origin": "JFK", "destination": "LHR", "departureDate": "2026-10-16", "returnDate": "2026-10-23" },{ "origin": "SFO", "destination": "NRT", "departureDate": "2026-10-20" }],"cabinClass": "business","currency": "USD"}
Every input
| Input | Default | What it does |
|---|---|---|
origin / destination | JFK / LAX | IATA airport or city codes |
departureDate | — | YYYY-MM-DD; blank uses daysFromNow |
returnDate | — | set it and the search becomes a round trip |
daysFromNow | 30 | used when departureDate is blank |
tripType | one-way | one-way or round-trip |
searches | [] | bulk list; each entry overrides the single-route fields |
adults / children / infantsInSeat / infantsOnLap | 1 / 0 / 0 / 0 | passenger mix |
cabinClass | economy | economy, premium-economy, business, first |
maxStops | any | 0 nonstop only, 1, 2 — applied by Google, so the response is smaller and faster |
currency / country / language | USD / US / en | point of sale; fares genuinely differ by it |
maxResultsPerSearch | 0 (whole board) | cap applied before delivery, so you are never billed past it |
skipPriceUnavailable | false | drop rows Google has no sellable price for |
proxyConfiguration | Apify Proxy, automatic pool | switch to RESIDENTIAL or bring your own |
💵 Pricing
Pay per event: flight-scraped — $0.0003 per itinerary ($0.30 per 1,000). No start fee, no
per-run fee, no per-page fee, no usage surcharge. A duplicate render is never billed; a row dropped
by maxResultsPerSearch or skipPriceUnavailable is never billed.
| Board (measured) | Itineraries | You pay |
|---|---|---|
| JFK→LAX one-way | 98 | $0.029 |
| JFK→LHR business, 2 pax | 89 | $0.027 |
| JFK→BKK one-way | 57 | $0.017 |
| JFK↔LAX round-trip | 48 | $0.014 |
| 30 routes × 60 departure dates ≈ 126,000 rows | ≈ $38 |
Rows with priceUnavailable: true (2–8% of a board in testing) are real itineraries with full
schedule, per-leg and CO2 data, and they are billed. Set skipPriceUnavailable: true if you do
not want them.
⚠️ Honest limits — what this does NOT do
- No multi-city. Not a shortcut — a measured wall. Google accepts a multi-city request and
renders the search form with every leg filled in correctly, then ships zero server-rendered
result rows: HTTP 200, ~1.89 MB, 0 itinerary elements, across trip-type values 3, 4 and 5 and
five
tfuparameter variants. Those boards are built client-side, so no HTTP-only scraper can read them. One-way and round-trip are fully supported. - A round-trip board is the DEPARTING slice. This is how Google works, not a shortcut here.
Every row on a round-trip board is an outbound itinerary priced as the round-trip total —
measured 48 of 48 rows carrying
"From 357 US dollars round trip total". The return leg is a second selection Google does not put on this board. Rows say so explicitly:priceType: "round trip total", andlegs[]covers the outbound slice only. - No booking-option / OTA list. The per-itinerary list of who sells the fare and at what price is behind a click; this actor reads the board.
- No price calendar / date grid and no Explore. Different pages, deliberately out of scope.
departureDateTimeIsoandarrivalDateTimeIsocarry no timezone. They are local airport clock times, exactly as Google shows them.arrivalDayOffsetgives you the+1/+2day roll.- Prices are what Google displays, not a booked fare. They move, and they differ by
countryandcurrency. - English only. The parser reads Google's English accessibility sentences. Leave
language: en;currencyandcountrystill control pricing and point of sale.
Anti-bot and proxy — measured, not guessed
Verified on 2026-09-02 by running the same JFK→LAX board three ways:
| Route | Status | Bytes | Itineraries | Time |
|---|---|---|---|---|
| Direct (no proxy) | 200 | 7,638,349 | 98 | — |
Apify Proxy RESIDENTIAL + country-US | 200 | 7,644,199 | 98 | 6,482 ms |
| Apify Proxy automatic (datacenter) | 200 | 7,644,082 | 98 | 3,431 ms |
No CAPTCHA, no challenge, no JS execution needed. The datacenter pool is the default because it
returned identical data about twice as fast and at effectively no proxy cost; residential runs about
$0.0026 per search at the measured 329 KB of compressed traffic. One residential request during
testing returned an Apify-proxy 590 UPSTREAM504 with 0 bytes while others succeeded — transient, so
the actor retries up to 4 times with a fresh session each time.
A blocked run never reports "no flights found." If no board can be read, the run exits cleanly with a status message that names it as a transport failure. A genuinely empty board is a different, distinguishable case (LWS→BFF returned 200 / 1.79 MB / 0 rows) and is reported as such.
robots.txt
https://www.google.com/robots.txt line 176, under User-agent: *, reads verbatim:
Disallow: /travel/flights/search
That is the path this actor reads. It is disclosed here so you can make your own call. Use the output in line with Google's terms and the law in your jurisdiction; you are responsible for how you use it.
🛠 How it works
One GET https://www.google.com/travel/flights/search?tfs=…&hl=…&gl=…&curr=…&tfu=EgYIABABGAA per
board. tfs is a base64url protobuf encoding the route, dates, passenger mix, cabin and stop cap —
hand-built by this actor, with a self-check that asserts the encoder still reproduces a known-good
token before any request goes out (a wrong token would return a perfectly valid page for the wrong
route).
tfu=EgYIABABGAA is not optional: with it a JFK→LAX board carries 97–98 itineraries for 328,862
compressed bytes; without it, 32 itineraries for 270,067 bytes.
Parsing is anchored on the stable semantic layer — Google's accessibility sentences and the
data-* attributes of the Travel Impact Model block, which is a public contract — rather than on
obfuscated class names, which Google rotates without notice. Class selectors are used only as the
fast path, always with an aria/attribute fallback, and the run fails loudly rather than silently if
itinerary elements are present but none parse.
❓ FAQ
Do I need a Google API key? No. No key, no login, no cookies.
How many itineraries come back per search? The whole board. Measured: 98 (JFK→LAX one-way),
89 (JFK→LHR business), 58 (JFK→BKK), 45–48 (JFK↔LAX round-trip). resultsOnBoard on every row tells
you the true board size even when you cap the rows you keep.
Why is the price on the row different from the visible text on Google's page? It isn't — the
visible price text is font-obfuscated, so the number is read from Google's own accessibility label
("214 US dollars"), which is why currencyName comes back too.
Can I get the return flights of a round trip? Not from this board — see honest limits. You get the departing itineraries priced as the round-trip total.
Can I search many dates? Yes — put one entry per date in searches. That is the normal way to
build a fare-history or fare-alert dataset.
Is priceUnavailable a bug? No. Google lists some itineraries with no sellable price. They keep
full schedule, per-leg and CO2 data. Set skipPriceUnavailable: true to drop them.
How fast is it? 3–7 seconds per board including the fetch. The 3-board validation run finished in 11.4 seconds.
What happens on an impossible route? A clean run with zero rows and a status message saying so — never a failure, and never confused with a block.