How to Scrape Flight & Fare Data (Python Guide)
- Google Flights renders fares with JavaScript. I fetched the search page on June 12, 2026: HTTP 200, 1.9 MB of HTML, and 0 prices in the static markup, so requests + BeautifulSoup alone parses no fares.
- Airline name strings (United, British Airways, Delta) sit in a JS payload, but the dollar figures only appear after the page's JavaScript fires its XHR calls.
- The realistic DIY route is Playwright: drive a headless browser, wait for the result list, read fares from the rendered DOM. It works but costs CPU and breaks when Google reshuffles class names.
- For bulk fares without running browsers, use a scraper API like ChocoData that renders JS, rotates IPs, and returns JSON. Airlines and Google block repeat IPs fast.
Flight fares are the textbook volatile dataset: the same seat changes price by the hour, so price-tracking tools, travel startups, and analysts all want a programmatic feed. Google Flights is the obvious target because it aggregates most carriers on one page. This guide builds a real Python fare scraper, reports exactly what I got back from a live fetch, and is honest about where the simple approach dies. I ran the fetch on June 12, 2026 and pasted the real numbers below. The short version up front: a plain request returns the page but none of the prices, so you either drive a headless browser or hand the JavaScript problem to an API. For the fundamentals, see my web scraping guide and the Python walkthrough.
Can you scrape Google Flights, and is it legal?
You can fetch the public Flights page with a script, and the legality depends on the method and what you keep. The technical reality first: google.com/travel/flights runs on JavaScript, so a plain request gets the page chrome and zero fares. The prices arrive over a later XHR call, which means a headless browser or an API that renders JS. The legal part has two layers worth separating.
On US computer-access law, reading pages anyone can load without logging in sits on the safer side. In hiQ Labs v. LinkedIn, the Ninth Circuit held that scraping public data does not violate the Computer Fraud and Abuse Act, and the Supreme Court’s Van Buren decision narrowed the CFAA toward traditional hacking. Querying a public fare page with a bot is not, by itself, a federal computer crime.
Contract law is the live risk. Google’s Terms of Service restrict automated access to its services, and most airline sites carry similar clauses. hiQ is the cautionary tale: it won the CFAA question and still paid a $500,000 judgment for breaching LinkedIn’s user agreement. Two rules keep you on the lower-risk path: stay on public fare results, never script a logged-in account or a checkout flow. Fares are facts about prices, not personal data, so the GDPR risk is low as long as you avoid passenger details. I am a tester, not a lawyer, so get advice before any commercial scrape. My is web scraping legal pillar covers the full picture.
What flight data can you extract?
A Google Flights results page carries the fields a price tracker actually needs, once the JavaScript has rendered them. Knowing the structure tells you what to target and what an API should hand back as JSON.
| Field | Where it lives | In the static HTML? |
|---|---|---|
| Price (per passenger) | Rendered result row | No (loads via XHR) |
| Airline / operating carrier | JS payload + rendered row | Partial (names only) |
| Departure / arrival times | Rendered result row | No |
| Total duration | Rendered result row | No |
| Number of stops | Rendered result row | No |
| Layover airports | Rendered result row | No |
| Origin / destination | URL query + rendered row | URL only |
| CO2 estimate | Rendered result row | No |
| Booking provider link | Booking panel (separate step) | No |
The pattern to internalize: almost everything commercially useful (price, times, stops, duration) is painted into the DOM by JavaScript after load. The origin, destination, and date you control through the URL, and the airline names happen to be embedded in a script blob, but the numbers people want are not in the initial response.
How does Google Flights block scrapers?
Google Flights stacks four defenses, and the first one stops a naive requests scraper before it sees a single fare.
- JS-only fare rendering. The initial HTML is a shell plus a large script payload. Fares populate over an XHR call once the page’s JavaScript runs. A static parse finds airline labels but no prices. I confirmed this below.
- Consent redirect. A no-cookie request to
google.com/travel/flightsreturns an HTTP 302 first and only reaches the app after appending a consent parameter (?ucbcb=1). Skip that and you get the interstitial, not the app. - IP rate limits and CAPTCHA. Repeated automated hits from one IP get throttled and then served a
/sorry/CAPTCHA page. Headless browsers from a datacenter IP trip this quickly. Residential rotation is the durable fix at volume. - Headless fingerprinting. Google checks for automation signals (the
navigator.webdriverflag, missing browser features). A default Playwright or Selenium launch is detectable and may get a degraded page or a block.
I tried requests + BeautifulSoup first (here is what came back)
I ran a plain fetch against the live page before reaching for anything heavier, because if the fares were in the HTML this would be a five-line script. They are not. Here is exactly what I ran and the real result.
import requests
# The page 302-redirects no-cookie clients; ucbcb=1 clears the consent hop.
url = (
"https://www.google.com/travel/flights"
"?q=Flights+to+London+from+New+York+on+2026-08-15"
)
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
)
}
resp = requests.get(url, headers=headers, allow_redirects=True)
html = resp.text
import re
prices = re.findall(r"\$[0-9]{2,4}", html) # any "$NNN" string
print("status:", resp.status_code)
print("html size:", len(html))
print("dollar-price matches in static HTML:", len(prices))
for airline in ["United", "British Airways", "Delta"]:
print(f" '{airline}' string count:", html.count(airline))
Real output from my run on June 12, 2026:
status: 200
html size: 1913583
dollar-price matches in static HTML: 0
'United' string count: 175
'British Airways' string count: 42
'Delta' string count: 15
The page returns HTTP 200 and nearly 2 MB of HTML. Airline names appear in the hundreds because they are baked into the JavaScript bundle. Dollar prices: zero. That is the whole problem in one result. BeautifulSoup would parse this cleanly and still hand you no fares, because the numbers are not there yet. I omitted the “tested” stamp on this article for that reason: I fetched the page successfully, but I could not parse real fares from a static response, and I will not paste numbers I did not extract.
Method 1: scrape Google Flights with Playwright (DIY)
Since the fares need JavaScript, the working DIY route is a headless browser. Playwright launches Chromium, runs the page’s JS, waits for the result list, and reads fares from the rendered DOM. I am giving you the real pattern, not a tested fare dump: result-row class names on Google Flights are obfuscated and change, so the selectors below are the part you will re-confirm in your browser’s devtools before this runs clean.
# pip install playwright && playwright install chromium
from playwright.sync_api import sync_playwright
URL = (
"https://www.google.com/travel/flights"
"?q=Flights+to+London+from+New+York+on+2026-08-15&ucbcb=1"
)
def scrape_fares(url: str):
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0 Safari/537.36"
)
)
page.goto(url, wait_until="networkidle", timeout=60_000)
# Each result is a list item; the price is the $-prefixed text inside.
# Confirm these selectors in devtools - Google obfuscates class names.
page.wait_for_selector("li", timeout=30_000)
rows = page.query_selector_all("li")
results = []
for row in rows:
text = row.inner_text()
if "$" in text:
results.append(text.split("\n"))
browser.close()
return results
for r in scrape_fares(URL)[:5]:
print(r)
What to expect and where it breaks:
- Waiting is the hard part.
networkidleplus await_for_selectoron the result list is the minimum. If you read the DOM too early you get the same empty shellrequestsreturned. - Selectors rot. Google ships obfuscated, rotating class names. Target stable signals (the
$text node, ARIA roles, list structure) instead of a brittle.aB3cDclass, and expect to re-check after Google updates the page. - One datacenter IP gets caught. A headless browser from a cloud IP firing repeated searches hits the
/sorry/CAPTCHA fast. For more than a handful of routes you need residential proxy rotation and a stealth setup, which is real ongoing work. - It is heavy. Each query spins up Chromium. Scraping hundreds of routes on a schedule means a browser farm, not a cron job on one box.
Playwright is the honest DIY answer for Google Flights. For a few routes checked occasionally, it works. The cost shows up at scale, in proxies, CAPTCHA solving, and selector maintenance. For the broader headless-browser playbook, see my notes on scraping without getting blocked, and the Scrapy guide if you want to wrap this in a crawler.
Method 2: scrape fares with a scraper API
A scraper API turns JS rendering, IP rotation, and CAPTCHA solving into one HTTP call that returns JSON. You skip the browser farm and the selector maintenance. ChocoData (chocodata.com) is one example I checked against its own docs. It exposes a universal endpoint (“one endpoint, any site”) plus 453 dedicated endpoints. The documented request shape:
import requests
API_KEY = "YOUR_KEY"
resp = requests.get(
"https://chocodata.com/api/v1/google/flights",
params={
"from": "JFK",
"to": "LON",
"date": "2026-08-15",
"api_key": API_KEY,
},
timeout=120,
)
data = resp.json()
for flight in data.get("flights", []):
print(flight["airline"], flight["price"], flight["stops"], flight["duration"])
The endpoint pattern is GET /api/v1/{site}/{resource}?api_key=..., so swapping the path targets other sites. Per ChocoData’s site, the service handles residential proxy rotation, CAPTCHAs, retries, and JavaScript rendering, and returns structured JSON. I have not independently benchmarked their success rate or response schema, so treat the field names above as illustrative and verify against their docs on the free tier before you wire anything to them.
The reason this category exists for Google Flights specifically: the two hard problems (JS rendering and IP blocks) are exactly what an API absorbs. You send a route and a date, you get fares back, and the proxy pool and headless fleet are someone else’s operational problem.
DIY vs scraper API: which should you use?
The choice comes down to volume and how much browser-and-proxy plumbing you want to own.
| Factor | DIY (Playwright) | Scraper API |
|---|---|---|
| Handles JS rendering | Yes (you run the browser) | Yes (managed) |
| IP rotation / CAPTCHA | You build it | Included |
| Output | Raw DOM, you parse | Structured JSON |
| Cost model | Your CPU + proxy bill | Per request / plan |
| Breaks when Google changes classes | Yes, you fix it | Vendor’s problem |
| Good for | A few routes, occasional checks | Bulk routes, scheduled tracking |
Pick Playwright when you are tracking a handful of routes, you are comfortable maintaining selectors, and you do not want a vendor in the loop. Pick a scraper API when you need many routes on a schedule and the proxy-plus-CAPTCHA upkeep is not how you want to spend your week.
The deciding fact from my run: a plain request to the Flights page returns HTTP 200, 1.9 MB of HTML, and 0 prices, because every fare is painted in by JavaScript afterward. Closing the gap to reliable fare data means either running and babysitting headless browsers behind rotating proxies, or letting an API own that stack and hand you JSON. For most readers past a few routes, that is work an API already did. If you want to learn the parsing skills regardless, my BeautifulSoup guide covers extracting fields once you have rendered HTML in hand.
FAQ
No public one. Google shut the QPX Express flight API in 2018 and never replaced it for the public. The Travel Partner API and Hotel/Flight Ads feeds are for vetted travel partners, not open developer use. There is no documented endpoint that returns Google Flights fares as JSON, which is why every method here is a scrape, not an API call.
The fares load over an XHR call that the page's JavaScript fires after the initial HTML arrives. A plain GET returns the page shell plus airline labels baked into a script blob, but the price numbers are populated client-side. BeautifulSoup parses the shell and finds zero dollar amounts.
Yes, and it is often easier per-route. Many carriers expose a JSON fare endpoint their own booking widget calls. The tradeoff is coverage: one airline per integration versus Google Flights aggregating most carriers in one place. For multi-airline price tracking, the aggregator or a scraper API saves a lot of per-site work.