๐ŸŽ‰ Premium Proxies ยท 3-Day Free TrialClaim Now โ†’
Guides

How to Scrape Google Flights Data Without an API

Learn to scrape Google Flights data (fares, routes, dates, price calendar): decode the tfs token, render JavaScript, and geo-target with a SparkProxy example.

S SparkProxy 29 19 min read
Share
How to Scrape Google Flights Data Without an API

If you want to scrape Google Flights for fares, routes, and a price calendar, the first thing to accept is that there is no public API to call. Google killed the official one in 2018, so the live results page is the only programmatic surface left. That page is a JavaScript app that packs your entire search into one encoded token and fetches fares over an internal RPC, which is exactly why a plain HTTP request comes back empty. This guide walks the real mechanics: decoding the tfs token, rendering the page, geo-targeting the currency and market, and pulling the price calendar, with working SparkProxy code.

Why Google Flights has no public API

Google did once sell flight data through an official interface. The QPX Express API came out of its acquisition of ITA Software, a deal announced in 2010 and valued around $700 million. QPX Express returned structured fares and itineraries over HTTP, and travel sites like Orbitz and CheapTickets built on it. Google shut it down on April 10, 2018, citing low partner interest. QPX for enterprise stayed alive under private contracts, but the public feed was gone.

ITA's fare engine still powers a public tool, the ITA Matrix, which many analysts use to hunt fares. Matrix is genuinely useful and free, but it sells no tickets and ships no documented API, so it is a research UI, not a data source you can call.

The practical result: as of 2026 there is no first-party Google Flights API. If you need the data programmatically, you either license an airline or GDS feed (more on that at the end) or you scrape the public results page. Everything below is about the second path. The broader market-monitoring angle for travel data lives in datacenter proxies for travel fare aggregation.

How the results page actually loads data

Open a Google Flights results URL, view source, and search for a price. You won't find one. The initial HTML is an application shell. Fares arrive a moment later through Google's internal RPC transport, batchexecute, the same framework that powers Gmail, Maps, and most modern Google web apps.

Here's the flow on a normal page load:

  1. The browser loads /travel/flights and reads the search from the tfs URL parameter.
  2. Client-side JavaScript POSTs an f.req payload to a batchexecute endpoint (roughly https://www.google.com/_/TravelFrontendUi/data/batchexecute).
  3. The RPC responds with a nested-array payload (prefixed with the anti-hijacking )]}' marker) that holds the itineraries and prices.
  4. JavaScript renders that payload into the result cards you see.

Reverse-engineered clients skip the DOM and call that RPC directly. You'll see the flight-results call referred to as GetShoppingResults in some community scrapers. It works, but the rpcids, the app path, and the response shape are undocumented and rotate without notice, and the request needs a valid session token (at) lifted from a page load. That is a brittle contract to maintain. Calling an internal batchexecute RPC is the same discipline covered in how to scrape hidden JSON API endpoints, just with a harder token dance. For most teams, rendering the page is the more durable choice, so that's the route this guide takes.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

Decode the tfs parameter (base64 protobuf)

Every Google Flights search collapses into one field: tfs (think "travel flight search"). It is a URL-safe base64 string wrapping a Protocol Buffers message. Build the right tfs and the results page renders your query without ever touching the search form.

Peek at the raw bytes to see the structure:

import base64

tfs = "CBwQAhoeEgoyMDI2LTA5LTE1agcIARIDSkZLcgcIARIDTEhSGAFwAYIBCwj___________8BQAFIAZgBAQ"
# tfs is URL-safe base64, often stripped of '=' padding; add it back before decoding
padded = tfs + "=" * (-len(tfs) % 4)
raw = base64.urlsafe_b64decode(padded)
print(raw)  # protobuf wire bytes: field tags + airport codes + ISO dates in plain view

The message uses a fixed field layout that the community has mapped:

Protobuf fieldMeaningExample
`2` (repeated)One entry per flight legoutbound leg, return leg
leg `2`Departure date (`YYYY-MM-DD`)`2026-09-15`
leg `13`Origin airport (IATA)`JFK`
leg `14`Destination airport (IATA)`LHR`
`5`Cabin class`1` economy, `2` premium, `3` business, `4` first
`8`Passengersadults, children, infants counts
`9`Trip type`1` one-way, `2` round-trip, `3` multi-city

Hand-rolling protobuf bytes is fiddly and easy to get wrong. A maintained encoder saves the trouble. The open-source fast-flights package builds the token for you:

from fast_flights import FlightData, Passengers, create_filter

filter = create_filter(
    flight_data=[FlightData(date="2026-09-15", from_airport="JFK", to_airport="LHR")],
    trip="one-way",
    seat="economy",
    passengers=Passengers(adults=1),
)
tfs = filter.as_b64().decode("utf-8")   # the ready-to-use tfs token

Pin the version. These libraries reverse-engineer an undocumented format, and the API surface shifts between releases (the project moved off bundled Playwright between its 2.x and 3.x lines). Treat the encoder as a dependency you test, not a black box you trust forever.

Build the Google Flights search URL

With a tfs token in hand, the URL is short. Four parameters do the work:

ParameterPurposeExample
`tfs`Base64 protobuf trip definition (required)`tfs=CBwQAhoe...`
`hl`Interface language`hl=en`
`gl`Point-of-sale country (ISO alpha-2)`gl=us`
`curr`Currency (ISO 4217)`curr=USD`

A complete search URL for a US point of sale in dollars:

https://www.google.com/travel/flights/search?tfs=CBwQAhoe...&hl=en&gl=us&curr=USD

There is a second, separate token you'll meet at the price-calendar stage: tfu. It carries flexible-date and price-graph UI state. Keep tfs for the itinerary itself and reach for tfu only when you request the date views.

Why you can't scrape Google Flights without a browser

This is the point most tutorials skip. Google Flights is a single-page app, and the fares are injected by client-side JavaScript after the shell loads. A requests.get() against the search URL returns markup with zero prices in it. There is nothing to parse.

So JavaScript rendering is not optional on this target, the way it sometimes is for a static SERP. You need a real browser to execute the batchexecute fetch and hydrate the cards. The general pattern for these SPA targets is covered in how to scrape dynamic JavaScript websites; Google Flights is a strict case of it.

The second wrinkle is the anti-bot stack. A vanilla headless Chromium gets dropped on the first request from a datacenter IP, because Google reads the browser fingerprint (TLS handshake, client hints, automation flags) and the IP reputation together. Rendering alone isn't enough. You need a browser that looks real and an IP that looks residential. Running and hardening that yourself is a project; the trade against a managed API is broken down in web scraping API vs self-managed proxies.

Fetch flights with the SparkProxy Scraping API

Rather than assemble a stealth browser, a residential pool, and fingerprint patches by hand, send one request to the SparkProxy Scraping API. It renders the page, rotates the proxy, and hands back the hydrated HTML. You pass the Google Flights URL as url and authenticate with the X-API-Key header.

A cURL request for a rendered, residential-routed, US-market search:

curl -G "https://scrape.sparkproxy.io/api/v1" \
  -H "X-API-Key: YOUR_API_KEY" \
  --data-urlencode "url=https://www.google.com/travel/flights/search?tfs=CBwQAhoe...&hl=en&gl=us&curr=USD" \
  --data-urlencode "render_js=true" \
  --data-urlencode "country_code=us" \
  --data-urlencode "premium_proxy=true" \
  --data-urlencode "stealth=true" \
  --data-urlencode "wait=5"

The same call in Python, wrapped so you can reuse it:

import requests

def fetch_flights(flights_url, country_code="us"):
    resp = requests.get(
        "https://scrape.sparkproxy.io/api/v1",
        headers={"X-API-Key": "YOUR_API_KEY"},
        params={
            "url": flights_url,
            "render_js": "true",      # SPA target: no JS, no fares
            "country_code": country_code,  # exit IP in the point-of-sale country
            "premium_proxy": "true",  # residential pool survives the anti-bot stack
            "stealth": "true",        # pre-warm, forced referrer, idle delays
            "wait": "5",              # give the batchexecute results time to hydrate
        },
        timeout=120,
    )
    resp.raise_for_status()
    return resp.text

Each parameter answers a specific obstacle on this target:

ObstacleParameterWhy it helps
Fares render in JavaScript`render_js=true`Runs Chromium so the RPC fetch fires and cards hydrate
Headless dropped on sight`premium_proxy=true` + `stealth=true`Residential IP plus fingerprint and referrer hardening
Wrong market or currency`country_code` aligned with `gl`/`curr`/`hl`Consistent point of sale
Slow hydration`wait` (or `wait_for` a selector)Waits for the result list to populate

Mind the credit math, because it stacks. premium_proxy with render_js is 25 credits, country_code adds 5, and stealth adds 5, so a fully loaded flight search runs about 35 credits. If a given route survives without stealth, drop it and save the 5. If Google is quiet that day, a lighter request works; on hard days, keep all four on.

Parse fares, routes, and legs

Once you have hydrated HTML, resist the urge to hard-code Google's CSS classes. They are obfuscated and rotate often, so a selector like .pIav2d will break within weeks. The durable anchor on this page is the aria-label attribute: each result row carries a full human-readable summary that Google maintains for screen readers, and it contains the price, the airline, the stop count, the duration, and the times.

from bs4 import BeautifulSoup
import re

def parse_flights(html):
    soup = BeautifulSoup(html, "html.parser")
    out = []
    for li in soup.select("li[aria-label]"):
        label = li["aria-label"]
        price = re.search(r"(?:US)?\$([\d,]+)", label)
        if not price:
            continue  # rows without a fare are UI chrome, skip them
        out.append({
            "summary": label.strip(),                 # full readable itinerary line
            "price": int(price.group(1).replace(",", "")),
        })
    return out

Here's what you can pull, and where each field lives:

FieldWhere it isNotes
Price`aria-label`, fare cardIn the currency set by `curr`
Airline(s)`aria-label`, carrier rowMarketing carrier; may be operated by another
Stops`aria-label` ("Nonstop", "1 stop")Zero-stop is the cleanest signal
Duration`aria-label` ("7 hr 25 min")Total elapsed, not flight time
Departure / arrival`aria-label`Local times at each airport
Layover airportsexpanded leg panelNeeds a click to reveal full detail
Emissions`aria-label` ("... kg CO2e")Google's estimate, not the airline's

If you'd rather not maintain a parser, offload it to the API with extract_rules, which returns JSON instead of raw HTML:

params["extract_rules"] = '{"rows": {"selector": "li[aria-label]", "type": "list"}}'

The trade is the usual one: keep a parser for full control, or hand it off and accept the API's structure. Either way, store the raw aria-label too, so you can re-parse historical pulls when a field you ignored suddenly matters.

Scrape the price calendar and date grid

The "fares by date" views are a different data source, not a slice of the results DOM. Google Flights offers two: the price graph (lowest fare per departure date across a window, often two months) and the date grid (a matrix of outbound-versus-return date pairs). Both come from their own batchexecute fetch, keyed by that tfu token from earlier, and neither shows up in the itinerary cards.

You have two ways to collect a Google Flights price calendar.

The reliable, boring way is to iterate the departure date in tfs and record the cheapest fare per day. It costs one rendered request per date, so a 60-day window is 60 requests, but it reuses the exact fetch and parser you already have:

from datetime import date, timedelta

def price_calendar(origin, dest, start, days=60, gl="us", curr="USD"):
    calendar = {}
    for offset in range(days):
        day = start + timedelta(days=offset)
        tfs = build_tfs(origin, dest, day.isoformat())  # your tfs encoder from earlier
        url = (
            "https://www.google.com/travel/flights/search?"
            f"tfs={tfs}&hl=en&gl={gl}&curr={curr}"
        )
        fares = [f["price"] for f in parse_flights(fetch_flights(url, country_code=gl))]
        calendar[day.isoformat()] = min(fares) if fares else None
        human_delay(base=20)   # pace between requests, see below
    return calendar

The faster way is to render the price-graph view once and read the whole window from a single response, or to capture that batchexecute reply directly. It is cheaper on credits but pins you to the undocumented RPC shape, so it breaks more often. Start with the date-iteration method, and only graduate to the RPC when the request volume makes the credits hurt.

Regional fares: currency and country targeting

Airfares are set by point of sale. The same JFK to LHR itinerary can price differently for a shopper in New York, London, or Mumbai, because carriers file different fares per market and Google reflects that. If you want accurate local prices, two things have to agree:

  1. The URL's market signals. Set curr to the currency, gl to the country, and hl to the language.
  2. The exit IP. Route through a proxy in that same country with country_code, so Google sees local traffic.

Mismatch them and you get a blended result that matches no real customer. A US exit IP asking for curr=EUR is not the fare a German traveler sees. Keep the layers aligned:

MARKETS = [
    {"country_code": "us", "gl": "us", "hl": "en", "curr": "USD"},
    {"country_code": "gb", "gl": "gb", "hl": "en", "curr": "GBP"},
    {"country_code": "de", "gl": "de", "hl": "de", "curr": "EUR"},
    {"country_code": "in", "gl": "in", "hl": "en", "curr": "INR"},
]

def compare_markets(origin, dest, day):
    rows = {}
    for m in MARKETS:
        tfs = build_tfs(origin, dest, day)
        url = (
            "https://www.google.com/travel/flights/search?"
            f"tfs={tfs}&hl={m['hl']}&gl={m['gl']}&curr={m['curr']}"
        )
        fares = [f["price"] for f in parse_flights(fetch_flights(url, m["country_code"]))]
        rows[m["curr"]] = min(fares) if fares else None
        human_delay(base=20)
    return rows   # cheapest fare per market, for arbitrage or monitoring

This point-of-sale discipline is the same one behind cross-border price monitoring in general, which the travel fare aggregation piece covers from the business side. Hotels behave the same way, which is why scraping Booking.com hotel prices uses an identical currency-plus-geo setup.

Ethics, Terms of Service, and airline APIs

Set your guardrails before you scale a job like this.

  • Collect only public data. Query fares as an anonymous shopper sees them. Don't touch logged-in state, saved trips, or anything behind an account.
  • Know the Terms of Service. Google's ToS restrict automated access outside its own interface. In the US, the hiQ v. LinkedIn line of cases narrowed the Computer Fraud and Abuse Act as applied to public data, so scraping public fares is usually a contractual matter rather than a criminal one. It is still a real risk, so weigh it for your use case and jurisdiction.
  • Don't relabel fares as live inventory. Google's prices are aggregated and sometimes estimated. Presenting them as your own guaranteed, bookable feed misleads users and invites trouble. Treat them as market signal, not a booking source.
  • Throttle and cache. Most fare-monitoring needs a daily or hourly refresh, not a per-minute one. Cache aggressively and refresh only as often as decisions actually change.

If your goal is to actually book or resell flights, scraping is the wrong tool. Look at real airfare APIs instead: Amadeus and Sabre on the GDS side, Duffel and Kiwi's Tequila for modern booking flows, and aggregator programs like Travelpayouts for affiliate data. They cost money and paperwork, but they give you bookable inventory and a contract, which a scrape never will. Scrape Google Flights for research, monitoring, and analysis; license an API when money changes hands.

Frequently asked questions

FAQ

Not a public one. Google shut down its official QPX Express API on April 10, 2018, and ITA's Matrix tool ships no documented API. As of 2026 the only programmatic access to Google Flights data is scraping the public results page. For bookable inventory, use an airline or GDS API such as Amadeus, Duffel, or Kiwi's Tequila instead.

Scraping publicly visible fares is generally not a criminal act in most jurisdictions, and the hiQ v. LinkedIn rulings narrowed how the US Computer Fraud and Abuse Act applies to public data. It can still breach Google's Terms of Service, which restrict automated access outside Google's interface, so it is a contractual risk rather than a criminal one. Collect only public data, throttle your requests, and check the rules for your jurisdiction.

The tfs parameter is a URL-safe base64 string wrapping a Protocol Buffers message that encodes the whole search: the flight legs (origin, destination, and date per leg), the cabin class, the passenger counts, and the trip type. Build a valid tfs and the results page renders your query without you ever using the search form.

Yes. Google Flights is a single-page app that fetches fares over an internal batchexecute RPC after the initial HTML loads, so a plain HTTP request returns a shell with no prices. You need a real browser to hydrate the data, plus a residential IP and fingerprint hardening, because a vanilla headless browser gets dropped on the first request. With the SparkProxy API you set render_js=true and premium_proxy=true.

Set the curr parameter to an ISO 4217 code (for example curr=EUR), and align gl (country) and hl (language) to the same market. Then route the request through an exit IP in that country using country_code. If the IP and the URL parameters disagree, Google returns a blended fare that no real local shopper would see, so keep all of them consistent.

The price calendar (Google's price graph and date grid) is a separate data fetch, not part of the itinerary cards. The reliable method is to iterate the departure date inside the tfs token and record the cheapest fare per day, one rendered request per date. The faster method is to render the price-graph view or capture its batchexecute response directly, which returns the whole window at once but depends on an undocumented RPC shape.

Limited-time ยท 50% off

Get 50% off your first month

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Offer ends soon โ€” claim it before it's gone

Claim Discount

About the Author

This guide was written by the SparkProxy Technical Team. SparkProxy provides datacenter and residential proxies plus a managed Scraping API used for price monitoring, travel fare collection, and SERP data at scale. We deal with hard JavaScript targets and Google's anti-bot stack every day, from batchexecute-fed SPAs to point-of-sale currency quirks, so the advice here reflects what keeps a flight-scraping job running rather than what looks tidy in a one-off demo. Adapt the pacing, geo-targeting, and credit math to your own routes and volume.

Keep reading

Related articles

How to Scrape Baidu Search Results Accurately

How to Scrape Baidu Search Results Accurately

How to scrape Baidu search results: the pn parameter, GBK encoding traps, resolving baidu.com/link redirects, and parsing Baijiahao and Zhidao blocks.

SparkProxyยทGuides