~ / guides / How to Scrape Flight & Fare Data (Python Guide)

How to Scrape Flight & Fare Data (Python Guide)

MR
Marcus Reed
Founder & lead tester · about the author
the short version
  • 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.

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.

FieldWhere it livesIn the static HTML?
Price (per passenger)Rendered result rowNo (loads via XHR)
Airline / operating carrierJS payload + rendered rowPartial (names only)
Departure / arrival timesRendered result rowNo
Total durationRendered result rowNo
Number of stopsRendered result rowNo
Layover airportsRendered result rowNo
Origin / destinationURL query + rendered rowURL only
CO2 estimateRendered result rowNo
Booking provider linkBooking 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.

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:

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.

FactorDIY (Playwright)Scraper API
Handles JS renderingYes (you run the browser)Yes (managed)
IP rotation / CAPTCHAYou build itIncluded
OutputRaw DOM, you parseStructured JSON
Cost modelYour CPU + proxy billPer request / plan
Breaks when Google changes classesYes, you fix itVendor’s problem
Good forA few routes, occasional checksBulk 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

Is there an official Google Flights API for fares?

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.

Why does my requests call to Google Flights return no prices?

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.

Can I scrape fares straight from airline sites instead?

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.

MR
Marcus Reed
I've built and run web scrapers for the better part of a decade. On this site I put scraper APIs and scraping tools through real jobs against real targets, then write up what actually holds up.