kestrel actors docs

kestrel actorsGuides

How to find the cheapest day to fly

Google Flights answers "which day is cheapest" with a calendar you read by eye and cannot export. The same answer as rows — one per departure day, ranked, with the airline and duration attached — is a single run, and once it is scheduled you also have the price history Google keeps to itself.

The short version

  1. Name the route and the first departure day. Airport codes, and a departure date that can be relative — "30 days" keeps a scheduled run at a fixed lead time forever.
  2. Set the window length. Every day in the window is its own search. Seven days is seven searches; a month is thirty. Sixty days is the cap per run.
  3. Let it keep the cheapest fare per day. One row per day: the cheapest itinerary Google lists for that day under your cabin and stop filters, with how many itineraries it beat.
  4. Read rank_in_window. Rank 1 is the cheapest day. The ranking is computed over the whole window before any price filter, so a filtered run still tells you where the day you kept sits.

One row per day is the whole idea

The question is rarely "what is the fare". It is "which day should I leave", and that is a ranking over a window.

from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("kestrel/flight-price-tracker").call(run_input={
    "routes": ["LIS-LHR"],
    "departDate": "30 days",   # relative: always 30 days out
    "days": 14,                # price each of the next 14 departure days
    "currency": "EUR",
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row["type"] == "fare":
        print(row["rank_in_window"], row["depart_date"], row["price"],
              row["airline"], f'{row["stops"]} stops', row["duration"])

Fourteen days is fourteen rows. The one with cheapest_in_window: true is the answer; rank_in_window orders the rest, so "the three cheapest departures in October" is a filter on one column rather than a spreadsheet exercise.

{
  "type": "fare", "route": "LIS-LHR", "origin": "LIS", "destination": "LHR", "trip": "one_way",
  "depart_date": "2026-10-05", "seat": "economy", "adults": 1, "currency": "USD",
  "price": 127, "round_trip_total": false,
  "airline": "Tap Air Portugal", "stops": 0,
  "depart_time": "8:00 PM", "arrive_time": "10:55 PM",
  "duration": "2 hr 55 min", "duration_minutes": 175,
  "co2_kg": 123,
  "itineraries_seen": 12, "cheapest_in_window": true, "rank_in_window": 1
}

itineraries_seen: 12 is the row's own audit trail: this fare was the best of twelve Google listed that day. A day with two itineraries and a day with forty are different kinds of cheap.

The cheap day is not always the cheap trip

Two columns stop you from booking a bad answer.

stops — the cheapest fare on a day is often the one with a connection. Filter to nonstop before ranking if a connection is not acceptable, and the ranking re-forms over the fares you would actually buy.

duration_minutes — a fare that is 8% cheaper and four hours longer is a different product. Sorting by price alone hides that; having both on the row means you can sort by whatever you actually optimise.

And if you want every itinerary on a day rather than the cheapest one — to compare airlines head to head, or to see the second-cheapest option — that is the Google Flights scraper instead, which bills per itinerary.

The history you cannot get any other way

Google's own price tracking emails you when a fare moves, on the routes it chooses, and keeps the series. Running this on a schedule gives you the series.

Every row carries fetched_at, so a daily run over a 30-day window is one point per route per departure day per day — a proper surface, not a snapshot. Two things fall out of it that a single search cannot answer:

Because the departure date is relative, the window moves with the calendar and never prices a day that has passed.

Alerts that cost nothing when nothing happens

A price ceiling filters days over the line before billing:

{"routes": ["LIS-LHR", "OPO-LGW"], "departDate": "14 days", "days": 30,
 "maxPrice": 90, "currency": "EUR", "maxStops": 0}

A run that finds no qualifying day delivers no rows and costs nothing, while the status row still reports how many days were filtered and what the window's cheapest fare was — so a quiet alert still tells you how far off the line you are. That is the whole cost model of an alert done properly: you pay when there is something to say.

The honest limitations

Run it

Flight Price Tracker — $0.004 per delivered fare row, one per day, with status rows, filtered days and empty routes free. For every itinerary rather than the daily cheapest, Google Flights Prices at $0.005 per itinerary. Related: scheduling with relative dates so the window never goes stale.

FAQ

Is there a Google Flights API?

No public one — Google retired QPX Express. The alternatives are aggregator APIs (Amadeus, Duffel, Kiwi) that sell fares from their own inventory and generally need a commercial agreement, and SERP resellers that charge per search whether or not it returned anything. Reading Google Flights directly and billing per delivered row means an empty route costs nothing.

Are round-trip prices per leg or for the whole trip?

The whole trip, as Google shows them. Set a trip length and every departure day becomes a round trip returning that many days later, with the price being the total for both legs and a round_trip_total flag on the row so the two can never be mixed up in a table.

Why are the results only in English?

Because the parser reads Google's own English accessibility labels, which is the most stable anchor on the page. Language does not change fares; currency and market do, and both are inputs.

What happens when two days share the lowest fare?

The earlier departure takes rank 1. It is an arbitrary rule, but it is a stated one, so a tie does not silently reorder between runs.

Run Flight Price Tracker on Apify →

Other actors used on this page: apify.com/kestrel/google-flights-prices.

More guides