DEV Community

Gio Rich
Gio Rich

Posted on Fully Autonomous

Tracking Google Flights prices with Python (a fare watcher in 40 lines)

I built this actor; it's a paid tool on Apify with a free trial credit.

Google shut down its QPX Express flight API in 2018, and there's no public Google Flights API to replace it. If you want to watch a fare for a trip, compare dates, or feed real itineraries into a travel app, you either click through the site every day or scrape it.

I'm an 18-year-old engineering student, and I built Google Flights Scraper for that. This post covers the Python and Node calls, the output, and a small fare watcher for a trip from Charlotte to Tokyo.

How it works

The actor makes plain HTTP requests (no headless browser) and reads the same results the Google Flights site shows. A one-way search is one request, takes about a second and usually returns 10-40 itineraries. A round trip lists outbound flights, then fetches the returns for the top few (3 by default) and outputs one row per complete outbound + return pair. It runs fine at 256 MB of memory.

Python example

pip install apify-client
Enter fullscreen mode Exit fullscreen mode
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("rel8ble/google-flights-scraper").call(run_input={
    "origin": "JFK",
    "destination": "LAX",
    "departureDate": "2026-11-12",
    "adults": 1,
    "cabinClass": "economy",
    "currency": "USD",
    "maxResults": 10,
})

for f in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(f["price"], f["airlines"], f["stops"], f["totalDuration"], f["departureDateTime"])
Enter fullscreen mode Exit fullscreen mode

Node example

import { ApifyClient } from "apify-client";

const client = new ApifyClient({ token: "<YOUR_APIFY_TOKEN>" });

const run = await client.actor("rel8ble/google-flights-scraper").call({
  origin: "SFO",
  destination: "ORD",
  departureDate: "2026-10-20",
  returnDate: "2026-10-23",
  maxStops: "0",
  returnFlightsForTopOutbound: 3,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const t of items) {
  console.log(t.price, t.flightNumbers.join("+"), "back:", t.returnFlight?.flightNumbers.join("+"));
}
Enter fullscreen mode Exit fullscreen mode

origin and destination also take several airports (JFK,EWR,LGA) or a Google city ID like /m/02_286 for all New York airports.

What comes back

One row per itinerary. A real row from a test run, Charlotte to Tokyo Narita one-way, trimmed:

{
  "searchId": "CLT-NRT 2026-12-03",
  "tripType": "one_way",
  "currency": "USD",
  "category": "best",
  "price": 1014,
  "priceLevel": "typical",
  "airlines": ["Air Canada"],
  "flightNumbers": ["AC 8746", "AC 9"],
  "stops": 1,
  "totalDuration": "17 hr 35 min",
  "departureDateTime": "2026-12-03T08:55:00-05:00",
  "arrivalDateTime": "2026-12-04T16:30:00+09:00",
  "arrivalDayOffset": 1,
  "layovers": [
    { "airport": "YYZ", "city": "Toronto", "durationMinutes": 98, "durationText": "1 hr 38 min" }
  ],
  "segments": [
    { "flightNumber": "AC 8746", "operatedBy": "Air Canada Express - Jazz", "aircraft": "Embraer 175", "legroom": "31 in", "emissionsKg": 142 },
    { "flightNumber": "AC 9", "aircraft": "Boeing 777", "legroom": "31 in", "emissionsKg": 582 }
  ],
  "emissionsKg": 724,
  "typicalEmissionsKg": 817,
  "emissionsDifferencePercent": -11,
  "googleFlightsUrl": "https://www.google.com/travel/flights/booking?tfs=..."
}
Enter fullscreen mode Exit fullscreen mode

Times are local airport times with the UTC offset, so you can compute real elapsed time across time zones. priceLevel is Google's own low / typical / high verdict for that route and date, which is handy for alerts.

Use case: watch a trip across several dates

Say you're flexible on when you fly Charlotte to Tokyo in December. The searches input takes many routes and dates in one run, so you can check a week of departure dates at once and log the cheapest option for each.

import csv
import datetime
import os
from apify_client import ApifyClient

DATES = [f"2026-12-{d:02d}" for d in range(1, 8)]

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("rel8ble/google-flights-scraper").call(run_input={
    "searches": [{"origin": "CLT", "destination": "NRT", "departureDate": d} for d in DATES],
    "maxStops": "1",
    "currency": "USD",
})

cheapest = {}
for f in client.dataset(run["defaultDatasetId"]).iterate_items():
    day = f["searchId"]
    if f["price"] is not None and (day not in cheapest or f["price"] < cheapest[day]["price"]):
        cheapest[day] = f

today = datetime.date.today().isoformat()
new_file = not os.path.exists("clt-nrt.csv")
with open("clt-nrt.csv", "a", newline="", encoding="utf-8") as out:
    w = csv.writer(out)
    if new_file:
        w.writerow(["checked", "search", "price", "priceLevel", "airlines", "stops", "duration"])
    for day, f in sorted(cheapest.items()):
        w.writerow([today, day, f["price"], f["priceLevel"], "+".join(f["airlines"]), f["stops"], f["totalDuration"]])
        if f["priceLevel"] == "low":
            print(f'LOW FARE: {day} ${f["price"]} {f["googleFlightsUrl"]}')
Enter fullscreen mode Exit fullscreen mode

Run it once a day. The CSV gives you a price history per departure date, and the priceLevel == "low" check prints a booking link when Google itself says the fare is below normal.

What it costs

$0.50 per 1,000 results, one result = one itinerary row. A one-way search usually returns 10-40 itineraries, so a single route/date costs about $0.005-$0.02.

  • The 7-date watcher above, daily for a month: roughly 7 × 25 × 30 ≈ 5,000 rows ≈ $2.60/month
  • Set maxResults to 5 and it drops to about $0.50/month
  • Apify's free plan gives $5 of monthly credit, about 10,000 itineraries

Limits

  • One-way and round trip only. No multi-city (3+ legs) yet.
  • No per-agency booking prices (Expedia vs the airline) and no baggage fees. googleFlightsUrl opens the itinerary on Google Flights, where those options are listed.
  • Initial results page only. Google sometimes loads extra "other flights" later; in tests I got 36 JFK-LAX options, 18 Madrid-Barcelona and 7 Charlotte-Tokyo.
  • Up to 9 passengers, about 11 months ahead (Google's own limits).
  • Fares change constantly. scrapedAt tells you when each row was captured.

If a round-trip return lookup fails after retries you still get the outbound row with returnFlight: null, not a crashed run. Bugs go to the Issues tab on the actor page.

Google Flights Scraper on Apify


This article was drafted with AI and published by me.

Top comments (0)