Purpose
Search Google Flights without using the homepage or search form. Construct direct /travel/flights?q= URLs for ordinary, flexible-date, weekend, multi-origin, airline-constrained, and multi-window searches, then extract visible fare-bearing itinerary or calendar evidence sorted by price. Read-only: never select a flight or proceed to booking or payment.
When to Use
- Cheapest flights between airports, cities, or regions.
- Flexible-date or weekend searches across a month or date range.
- Searches from multiple origins or to multiple destinations.
- Airline, alliance, nonstop, cabin, passenger, currency, or locale constraints.
- Google Flights Explore results with visible destination fares.
- Any request requiring visible Google Flights prices without booking.
Workflow
- Prefer a clean direct natural-language URL; do not visit the homepage or type into the search form:
https://www.google.com/travel/flights?q={url-encoded-query}&curr={currency}&gl={country}&hl={language}Use explicit airport codes and include all constraints in {query}. For example, express a generalized request as round trip from {origin-airports} to {destination} on {airlines-or-alliance} for {trip-length}-night weekend trips between {start-date} and {end-date}. State permitted departure and return weekdays explicitly when they matter. Use curr=NOK&gl=NO&hl=en for Norwegian-localized results when requested; omit locale parameters otherwise.
- If exact fixed dates, passenger count, cabin, and trip type are known, a direct
tfsdeep link is more deterministic:
https://www.google.com/travel/flights/search?tfs={base64url-protobuf}&curr={currency}&hl={language}Build the protobuf with fields f2=0, repeated f3 legs containing f2=YYYY-MM-DD, f13 origin, and f14 destination, followed by f8={passengers}, f9={cabin} (1 economy, 2 premium economy, 3 business, 4 first), and f19=1 for round trip or 2 for one way. A round trip has reversed second-leg airports and the return date. Never fabricate opaque Explore identifiers or tfs values; use the natural-language URL when dates are flexible.
For multiple origins or date windows, issue one clean direct URL per requested comparison window, reusing the same residential proxy/profile. Navigate each URL with
waitUntil: "domcontentloaded", wait 8–10 seconds for dynamic results, and extend the wait if the result area is still loading. Batch navigation and evaluation in the fewest browser-agent calls possible.Run this evaluator on each loaded page:
(() => {
const clean = (s) => (s || "").replace(/\s+/g, " ").trim();
const money = (s) => {
const m = (s || "").match(
/(?:[$€£]|USD|EUR|GBP|NOK|kr|CZK|Kč)\s*[\d .\u00a0]+(?:[.,]\d{2})?|[\d .\u00a0]+(?:[.,]\d{2})?\s*(?:kr|NOK|USD|EUR|GBP|CZK|Kč)/i,
);
if (!m) return null;
const n = m[0].match(/[\d.,]+/);
return n
? Number(
n[0]
.replace(/[\s\u00a0]/g, "")
.replace(/\.(?=\d{3}(?:\D|$))/g, "")
.replace(",", "."),
)
: null;
};
const body = document.body?.innerText || "",
u = new URL(location.href),
blocked =
/sign in|unusual traffic|robot|captcha|verify you are human/i.test(body) &&
!/flight|depart|arriv/i.test(body);
const roots = [
...document.querySelectorAll(
'[role="main"] [role="listitem"],[role="main"] li,[role="main"] [role="button"][aria-label],li',
),
];
const seen = new Set(),
items = roots
.map((el) => {
const text = clean(el.innerText || el.getAttribute("aria-label"));
if (
!text ||
text.length > 900 ||
seen.has(text) ||
!/(?:[$€£]|\b(?:USD|EUR|GBP|NOK|CZK)\b|kr|Kč|from\s+[\d,.]+)/i.test(text)
)
return null;
seen.add(text);
const sm = text.match(/\b(nonstop|direct|\d+)\s+stop(?:s)?\b/i);
const times = [
...text.matchAll(
/\b((?:[01]?\d|2[0-3]):[0-5]\d|(?:[1-9]|1[0-2]):[0-5]\d)\s*(AM|PM)?(?:\s*\+1)?/gi,
),
].map((x) => x[0]);
return {
text,
price: money(text),
stops: sm ? (/nonstop|direct/i.test(sm[1]) ? 0 : Number(sm[1])) : null,
departTime: times[0] || null,
arriveTime: times[1] || null,
};
})
.filter(Boolean);
const fares = [...document.querySelectorAll('[aria-label^="From "]')]
.map((e) => {
const aria = clean(e.getAttribute("aria-label")),
text = clean(e.innerText),
price = money(aria) || money(text);
return price == null ? null : { price, ariaLabel: aria, text };
})
.filter(Boolean);
const unique = [],
keys = new Set();
for (const x of [...fares, ...items]) {
const k = x.text || x.ariaLabel;
if (x.price != null && !keys.has(k)) {
keys.add(k);
unique.push(x);
}
}
unique.sort((a, b) => a.price - b.price);
return {
url: location.href,
query: u.searchParams.get("q") || "",
mode:
u.pathname === "/travel/explore"
? "explore"
: u.pathname === "/travel/flights"
? "flights"
: "unknown",
currency: u.searchParams.get("curr") || null,
locale: {
country: u.searchParams.get("gl") || null,
language: u.searchParams.get("hl") || null,
},
blocked,
isFlightsPage: /^\/travel\/(?:flights|explore)/.test(u.pathname) && !blocked,
origin: document.querySelector('input[aria-label^="Where from"]')?.value || null,
destination:
document.querySelector('input[aria-label^="Where to"]')?.value || null,
departure:
document.querySelector('input[aria-label="Departure"]')?.value || null,
returnDate: document.querySelector('input[aria-label="Return"]')?.value || null,
priceOptions: unique.slice(0, 100),
bodyExcerpt: clean(body).slice(0, 1200),
};
})();For flexible-month or weekend searches, if a calendar is visible or can be opened through the rendered
Departurecontrol, inspectdiv[role="gridcell"][data-iso]and extract fare labels only for cells in{start-month}through{end-month}. Return these as date-level starting fares, not confirmed itinerary prices. If Google produces itinerary cards, extract their visible airline, dates, times, duration, stops, and total fare instead.Sort every non-null visible fare numerically when “cheapest” is requested. Deduplicate repeated cards by stable text or by departure, arrival, airline, and price. Verify visible origins, destination, dates, trip type, passengers, cabin, airline/alliance constraint, currency, and locale. Treat SkyTeam or airline wording as a requested filter to verify from rendered results, not as proof merely because it appeared in the query.
If natural-language parsing selects the wrong airport or dates, retry once with explicit IATA codes and fully written dates. If a CAPTCHA, consent, login, or unusual-traffic interstitial appears, report
blocked:trueand no inferred fares. Do not click “Select flight” or proceed to an airline booking page.
Site-Specific Gotchas
- Direct
/travel/flights?q=...is the shortest stable route; homepage visits and form typing are unnecessary. - The exact fixed-date
tfsformat usesf19=1for round trip andf19=2for one way; do not confusef19with the seat placeholderf2. - Flexible weekend or month wording may produce starting-fare evidence rather than a fully selected itinerary. Label the distinction clearly.
- Google may select an unintended airport or city, especially for multi-airport cities. Verify visible airport codes and controls.
- Airline and alliance constraints are not guaranteed by query text alone. Confirm the displayed carrier belongs to the requested set; do not assume every result is SkyTeam.
- Result markup is dynamic. Wait after navigation; a successful
gotodoes not mean fares have rendered. - The
[aria-label^="From "]selector commonly exposes calendar or Explore starting prices. It is not necessarily a confirmed fare for the requested exact dates. - Flight rows may be duplicated. Deduplicate before sorting and preserve raw visible evidence.
- Times are local to the relevant airport;
+1means arrival on the next calendar day. - Flight numbers generally require expanding a result and should be null for cheapest-list extraction unless explicitly requested.
- Use
curr,gl, andhlto stabilize currency and locale. Residential egress is recommended to reduce traffic challenges and locale drift; reuse the same proxy/profile across comparisons. - Google Flights has no dependable public JSON fare API; internal batchexecute/GRPC endpoints are obfuscated and unsuitable for this recipe.
- Never save or reuse session-specific challenge parameters, Explore opaque IDs, or viewer hashes unless supplied as part of the current request.
- Read-only: never select an itinerary, continue to booking, or submit payment information.
Expected Output
For each requested route or date window, return an ordered record containing window, url, query, mode, currency, locale, isFlightsPage, blocked, visible route/date controls, and sorted priceOptions. Each option may include price, ariaLabel, text, stops, departTime, and arriveTime. Distinguish date-level starting fares from confirmed itinerary fares and preserve raw visible evidence.
For blocked or empty pages, return the requested query/window, blocked or isFlightsPage:false, an empty result list, and a precise explanation rather than invented prices. Never claim completeness beyond the fares rendered in the current Google Flights page.