Google Flights
Structured, fully-priced flight offers with per-segment detail for any route and date — priced by Google Flights, a metasearch aggregator across airlines and sellers.
/api/v1/flight-scraper/scrape/— submit/api/v1/flight-scraper/scrape/{job_id}/— pollBase URL: https://api.scrapeguys.com · Auth: X-API-Key: fsk_… with the google_flights scope · See the Overview.
/api/v1/flight-scraper/scrape/ is shared by both sources via a source field — google_flights (this guide, and the default when the field is omitted) or makemytrip. This page documents Google Flights end-to-end; for the OTA source, see the standalone MakeMyTrip FlightsCleartrip Flights guide — the two request/response shapes differ enough that each is documented on its own page.Submit a search
Omit return_date for a one-way search; include it for a round trip.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
origin | string | Required | — | 3-letter IATA airport code, e.g. DEL — or a metro area code such as NYC, expanded to that metro's airports. |
destination | string | Required | — | 3-letter IATA airport code or metro area code, ≠ origin. |
departure_date | date | Required | — | YYYY-MM-DD; not past. At most 330 days ahead — Google lists no further out, so a later date is refused at submit with a 400 naming the latest searchable date. |
return_date | date | Optional | null | ≥ departure; same 330-day limit as departure_date. Omit for one-way. |
adults | int | Optional | 1 | 1–9. |
children | int | Optional | 0 | 0–8. |
infants_in_seat | int | Optional | 0 | 0–8. |
infants_on_lap | int | Optional | 0 | 0–8; ≤ adults. |
cabin_class | string | Optional | economy | economy, premium_economy, business, first. |
max_stops | int | Optional | null | 0–3; 0 = nonstop, null = no limit. |
region | string | Optional | in | One of in, us, gb, eu, ae, ca, au, th, my, id, sg. |
currency | string | Optional | region default | 3-letter ISO. |
include_calendar | bool | Optional | false | Adds a date-grid price calendar. See Calendar & per-leg. |
calendar_days | int | Optional | 45 | 0–180. Calendar window end: days after departure. |
calendar_days_before | int | Optional | null | 0–180. Start the calendar window this many days before departure; default (null) starts the window at today. |
per_leg_fares | bool | Optional | false | Round-trip only (silently ignored on one-ways); adds outbound/return one-way comparison. See Calendar & per-leg. |
include_cheapest | bool | Optional | false | Adds Google's Cheapest tab. Slow (~10–30s) and best-effort — see Cheapest tab before switching it on. |
source | string | Optional | google_flights | Leave unset, or set explicitly to google_flights. Setting makemytrip routes to a different source with a different request/response contract — see MakeMyTrip FlightsCleartrip Flights. Any other value is a 400. |
adults + children + infants_in_seat) must total ≤ 9, and each lap infant needs an adult (infants_on_lap ≤ adults). origin and destination must differ.include_price_lock belongs to the MakeMyTrip source and platform to MakeMyTrip and Paytm, which are the only sources with two clients; setting include_price_lock: true or any platform value on a Google Flights search is a 400 (include_price_lock: false is accepted). Google Flights has a single client, so the polled job echoes platform as an empty string rather than naming one.source (or sending google_flights explicitly) behaves exactly as before. See Authentication → Scopes.Metro area codes
A fixed set of metropolitan area codes is accepted alongside airport codes and expanded to every airport in that metro, searched together as one group — NYC covers JFK, LGA and EWR in a single search rather than just one of them.
| Metro code | Airports covered |
|---|---|
BHZ | CNF, PLU |
BJS | PEK, PKX |
BUE | EZE, AEP |
CHI | ORD, MDW |
JKT | CGK, HLP |
LON | LHR, LGW, STN, LTN, LCY, SEN |
MIL | MXP, LIN, BGY |
MOW | SVO, DME, VKO, ZIA |
NYC | JFK, LGA, EWR |
OSA | KIX, ITM, UKB |
PAR | CDG, ORY, BVA |
RIO | GIG, SDU |
ROM | FCO, CIA |
SAO | GRU, CGH, VCP |
SEL | ICN, GMP |
SPK | CTS, OKD |
STO | ARN, BMA, NYO, VST |
TYO | HND, NRT |
YMQ | YUL, YMX |
YTO | YYZ, YTZ, YHM |
BKK, DXB, IST, KUL, SHA, and TPE each name both a metro and a real airport with the same code — searching them returns just that airport, not the wider metro, so a search for the airport you actually named is never silently widened. Any code not in the table above is treated as a plain airport code.origin or destination is a metro code, the response's search_metadata gains origin_airports / destination_airports — the list of airports that metro actually expanded to for that search. See Response schema.Poll for results
Every search is an asynchronous job: the submit call answers 202 with a job_id and does no scraping itself. Poll every 1–2 seconds until status is completed or failed (it moves pending → processing → one of those). The poll body is the full job record — keyed id (not job_id) — echoing your search parameters alongside status, result, error_message, error_category and timing fields. While the job is pending or processing, result is null.
Every job also echoes source, platform and leg — which stage of the funnel it ran: search, fare_families, review, farelock, checkout, return_options or booking_options. A /scrape/ or /batch/ job is search; each expansion endpoint stamps its own, so the jobs of one chain can be told apart without reading result — including ones that failed before producing one. Rows that predate the field may read unknown; nothing writes it now.
/api/v1/flight-scraper/scrape/{job_id}/completed job with flights: [] and search_context.no_results: true is a successful search that found nothing — check nearby_airports for alternatives.Job record
| Field | Description |
|---|---|
id | The job's UUID — the job_id from submit. |
status | pending, processing, completed or failed. |
origin, destination, departure_date, return_date, trip_type | The search as accepted. trip_type is one_way or round_trip. |
adults, children, infants_in_seat, infants_on_lap, cabin_class, max_stops, region, currency | Echoed request parameters, with defaults filled in (currency is "" when you did not send one). |
include_calendar, calendar_days, calendar_days_before, per_leg_fares, include_cheapest, include_price_lock, source, platform, resolve_deep_links | Echoed options. platform is only meaningful on MakeMyTrip and Paytm jobs; on Google Flights and Cleartrip it is echoed as an empty string. |
parent_job, selected_outbound | null on a search. On an expansion job (return, booking), the job it expands and the flight that was chosen. |
result | The scrape output described below; null until the job finishes. |
error_message / error_category | Both empty strings ("") on success. On failure, the reason and one of the categories listed under Key fields. |
created_at, started_at, completed_at, duration_seconds | Timestamps; duration_seconds is null until the job has both started and finished. |
An unknown job ID — or one that belongs to another client, or has aged out of retention — is 404 {"error": "Job not found."}.
Response schema
The result object holds flights[], search_context, search_metadata, and optionally calendar / per_leg_fares / cheapest_tab. Each flight carries a segments[] array.
Key fields
| Field | Description |
|---|---|
flights[].price | Total price for the offer (round-trip total on round trips). |
flights[].stops / layover_airports | Stop count and connecting airport codes. |
flights[].listing | top (best/featured) or other. |
flights[].booking_url | Public Google Flights booking URL for the offer. null on round-trip departing flights (and on any offer a link can't be built for) — expand the round trip first (see Round-trip expansion). |
flights[].outbound_index | Round-trip searches only: index to pass to /scrape/return/. |
flights[].standalone_one_way_price | With per_leg_fares: true: the flight's one-way fare, when matched. |
segments[] | Per-leg detail: airports & names, times, duration, flight number (also split as carrier + number), airline, aircraft, legroom. |
search_context.no_results | true when a valid search returned no flights. |
search_context.nearby_airports | Alternative airports [{ code, distance, price }]. |
search_context.currency_confirmed | Whether Google confirmed pricing in your requested currency. |
search_metadata.origin_airports / destination_airports | Present only when that side of the search was a metro code — the list of airports it expanded to, e.g. ["JFK", "LGA", "EWR"] for NYC. |
error_category | On failure: transient, blocked, empty_page, rotation, parse, invalid_search, invalid_input, http, error. empty_page is a bad exit IP serving an HTTP 200 page with no data at all — the scraper already retries it on a fresh proxy before giving up, same as blocked; rotation and parse mean Google changed the page structure and are not retried; invalid_search means Google would not answer the search on any exit (e.g. an airport code Google doesn't recognise; a date past the 330-day window is refused at submit) and is not retried either. A job that runs past the server-side time limit (10 minutes) is stopped and fails as transient. |
booking_url to the Google Flights booking page, and the booking-options endpoint resolves an offer to its actual sellers with per-seller prices and links.Batch scraping
Search up to 20 routes that share dates/region in one call.
/api/v1/flight-scraper/batch//api/v1/flight-scraper/batch/results/Batch-level (shared) fields: departure_date, return_date, region, currency, include_calendar, calendar_days, calendar_days_before, per_leg_fares, include_cheapest, source (the MakeMyTrip-only include_price_lock and the MakeMyTrip/Paytm platform are also batch-level — see the MakeMyTrip guide). Per-route fields: origin, destination, adults, children, infants_in_seat, infants_on_lap, cabin_class, max_stops — dates, region, currency and source cannot vary per route. Each field has the same range and defaults as on a single search; a batch holds 1–20 routes (otherwise 400), and is validated as a whole before any job is created.
/batch/results/ returns the same job records as GET /scrape/{job_id}/, but with no ordering guarantee — match on id. It never errors on an individual ID: malformed IDs, IDs that belong to another client and jobs from a source your key is not scoped for are simply left out, so results can be shorter than job_ids. An empty list, more than 20 IDs, or no valid ID at all is 400 {"error": …}. Poll a job directly to get an explicit 404 or 403.
429 says how many it needed and how many remain, and no route is submitted — nothing is billed and no slot is used. A batch larger than your per-minute limit (20 routes against the default 10/min) is always refused: split it.per_leg_fares, include_calendar and include_cheapest each add upstream work (latency, not quota units) to every route. include_cheapest is the one to weigh hardest: it's per-route work, and on cold routes it also needs a browser mint per route — serialised, so a large batch will mostly come back without Cheapest data. See Cheapest tab.Round-trip expansion
On a round-trip search, each departing flight is priced as a round-trip total. To see the return options for a chosen outbound, submit an expansion job referencing the completed parent job and the outbound's index.
/api/v1/flight-scraper/scrape/return/Guards: 404 if the parent job isn't yours; 400 if the parent job isn't completed, isn't a Google Flights round trip, outbound_index is out of range, or the chosen flight has no segments. Your key needs the google_flights scope (403 otherwise — checked before the parent is looked up). The expansion job repeats the parent's passengers, cabin, stops, region and currency. Expansion jobs never include calendar or per_leg_fares. Google Flights only — MakeMyTrip prices round trips differently, and not always the same way twice: two independently-selectable legs on domestic routes, one combined-fare card on international ones; see the MakeMyTrip Flights guide.
Booking options
Resolve a chosen flight to its actual sellers — airlines and OTAs with per-seller prices, booking links and baggage info. Submit an expansion job referencing a completed parent job and the flight's index.
/api/v1/flight-scraper/scrape/booking/resolve_deep_links: truefollows each seller's redirect to its real site URL intodeep_link(slower;nullon failure).- Works on one-way jobs and return-expansion jobs. A round-trip
/scrape/job returns 400 — pick an outbound via/scrape/return/first, then book from that job. - Other 400 guards: parent not
completed, parent not a Google Flights job, index out of range, or the chosen flight has no booking token/segments. An unknown parent is404. flight_indexcounts intoresult.flightsfor a one-way parent and intoresult.return_optionsfor a return-expansion parent.search_metadata.marketplace_attestedsignals whether the seller list is trustworthy; whenfalse, some sellers may be missing. Whentrue, also checkmarketplace_partial— attestation alone doesn't guarantee the list is complete.search_metadata.marketplace_partial—truewhen the server has evidence the returned seller list is short of what Google serves for this fare, so resubmitting the booking job may return more sellers. Alwaysfalsewhenmarketplace_attestedisfalse(that case is already covered above). A resubmitted job repeats the whole lookup and is billed again, so branch onmarketplace_attested/marketplace_partialrather than retrying blind.search_metadata.marketplace_readings— how many marketplace readings were combined to build the seller list (0means the job failed before reading any). Normally1on an unattested job and3on an attested one; higher (up to ~8) when a thin reading needs extra re-reads to fill out. A thin first reading is automatically re-read from fresh exits before the job answers, so seller lists come back fuller — at the cost of longer booking jobs.legroomis the seat pitch as Google worded it, e.g."32 in"— but the unit isn't pinned by the request. The same fare has been served as both"71 cm"and"28 in". Parse the unit before reading the number; don't assume inches.
Google Flights only — MakeMyTrip has no booking-options expansion; its own funnel is fare families and review, documented on the MakeMyTrip FlightsCleartrip Flights guide.
Calendar & per-leg fares
Calendar pricing — set include_calendar: true to get a date-grid summary: window_start/end, basis (always "one_way"), priced_days, cheapest, most_expensive, median, cheapest_dates, and a per-day days[] array of { date, return_date, price, booking_token } (price is null on days Google couldn't price). The window runs from today (or calendar_days_before before departure, if set — never earlier than today) through calendar_days after departure, and is capped at 180 days — an over-long window is trimmed from the front, keeping the dates nearest departure. Prices are one-way even on round trips. A calendar failure never fails the job — it surfaces as search_metadata.calendar_error with calendar: null.
Per-leg fares — set per_leg_fares: true on a round trip to compare booking the round trip vs. two one-ways: currency, outbound_one_way, return_one_way, two_one_ways_total, round_trip_total, round_trip_saving, round_trip_saving_pct, cheaper_as ("round_trip", "two_one_ways", or "either" when within 1%). If any leg couldn't be priced the object is null and the reason appears as search_metadata.per_leg_fares_error.
Both are Google Flights only — MakeMyTrip rejects include_calendar and per_leg_fares with 400 rather than accepting and ignoring them.
Attested Best readings
Not a request option — this is a server-side quality safeguard for the main flights[] results, and it's off by default. On some routes Google withholds entire itineraries — including the cheapest multi-airline connections — from a plain, unverified request, and returns them only to a caller it can verify. When the safeguard is enabled operator-side, the server spends a second, verified request on eligible searches and, if that reading is genuinely fuller or cheaper, serves it as flights[] in place of the plain one. There is nothing to set per request — you always get whichever reading the server judged best.
flights[] stand unchanged.| Field | Meaning |
|---|---|
search_metadata.attested_best | true when a verified reading was attempted for this search. Absent when it was not — see attested_best_skipped_reason. |
search_metadata.attested_best_applied | true when the verified reading was adopted — flights[] above is that reading. false means the plain results stand; check attested_best_error or attested_best_comparison for why. |
search_metadata.attested_best_offers / attested_best_min_price | Present when applied — offer count and floor price of the adopted (attested) reading, i.e. of flights[] itself. |
search_metadata.unattested_offers / unattested_min_price | Present when applied — the same figures for the plain reading that was replaced, so you can see what changed. |
search_metadata.attested_best_error | Present when a verified reading was attempted and the request itself failed. |
search_metadata.attested_best_comparison | Present when a verified reading came back but didn't beat the plain one — { attested_offers, attested_min, incumbent_offers, incumbent_min, exits_tried }. This is not distinguishable from a still-suppressed reading; the plain results stand either way. |
search_metadata.attested_best_skipped_reason | Why no verified reading was attempted at all: not enabled (the default — the safeguard is off for this deployment), full_results disabled, no attestation available for this search yet (one is being prepared in the background — a repeat of this search may pick it up), or pooled token predates body capture. |
Google Flights only. Independent of the Cheapest tab below — that's a separate, opt-in side channel returned in cheapest_tab; this instead replaces the main flights[] in place and needs no request flag.
Cheapest tab
Set include_cheapest: true on /scrape/ (or on /batch/) to also get Google's Cheapest tab: the same itineraries priced at the best seller's fare rather than the headline one. Results land in result.cheapest_tab, in the same shape as result.flights — match entries between the two on flight_numbers.
include_cheapest: false (default) | include_cheapest: true | |
|---|---|---|
| Typical time | ~2–4s | ~10–30s |
| Extra data transferred | none | ~0.6–1.2 MB |
| Cold route (first search) | — | no Cheapest data; a token is prepared in the background |
search_metadata.cheapest_tab_cheaper_count already does this comparison for you.Best-effort, and often returns nothing. cheapest_tab is null, or present with no cheaper prices, whenever the route genuinely has no cheaper seller right now, this is the first search of that route/date/passenger combination (no attestation exists yet — one is minted in the background; try again shortly), or Google served a suppressed reading despite a valid attestation. A job never fails because of the Cheapest tab — the Best-tab flights are unaffected either way.
| Field | Meaning |
|---|---|
search_metadata.cheapest_tab_attested | true if an attestation was on the wire for the Cheapest request. false with cheapest_tab: null is not an error. |
search_metadata.cheapest_tab_skipped_reason | Why it was not attempted — not requested, full_results disabled, cheapest tab is disabled, no attestation available for this search yet. |
search_metadata.cheapest_tab_flight_count | Offers returned, when it succeeded. |
search_metadata.cheapest_tab_cheaper_count | How many of those are actually cheaper than the same flight in flights. A zero is meaningful: every flight came back at the Best-tab price — cheapest_tab looks full either way, so check this before treating it as a discount. |
search_metadata.cheapest_tab_error | Set only when the request was attempted and failed. |
Google Flights only — accepted but silently a no-op on a MakeMyTrip job. On /batch/, remember it's per-route: ten routes means ten Cheapest requests and, on cold routes, ten serialised browser mints — a large batch will mostly come back without Cheapest data.
Errors, limits & billing
These rules are shared by every /api/v1/flight-scraper/ endpoint, Google Flights and MakeMyTrip alike. Keys, rate limits and quotas are described in the Overview.
| Status | When |
|---|---|
202 | A job was accepted (single search, batch, or expansion). Nothing has been scraped yet — poll. |
400 | The request failed validation, or an expansion was refused. Validation errors are keyed by field ({"departure_date": ["…"]}); expansion refusals are {"error": "…"}. |
401 | Missing, invalid or deactivated key. Detail is "Invalid API key." or "API key is deactivated."; a request with no X-API-Key header is also a 401. |
403 | The key is valid but lacks the scope for the source: {"detail": "This API key does not have the 'google_flights' scope."}. Returned on submit, batch, every expansion and when polling a job of a source the key cannot call. Scopes are google_flights and makemytrip; there is no wildcard. |
404 | {"error": "Job not found."} — unknown job, another client's job, or a job past retention. |
429 | Rate limit or quota exceeded; detail is one of "Rate limit exceeded (10/min). Try again shortly.", "Daily quota exceeded (1000/day). Resets at midnight UTC.", "Monthly quota exceeded (20000/month).", or, for a /batch/ that does not fit, "This batch needs 20 requests but only 10 remain in this minute (10/min). Nothing was submitted; retry next minute or split the batch." (the same wording, naming the daily or monthly quota, when a quota is what it exceeds) — with your limits in the brackets. |
Keys start with fsk_ and are sent as X-API-Key. A hotel-API key (gsk_…) does not authenticate here. A scope is checked before the request uses any of your rate limit, so a 403 costs nothing.
What is billed
- One request unit per job you submit: a search, each route in a batch, and each expansion (return, booking, and the MakeMyTrip expansions). Optional add-ons — calendar, per-leg fares, Cheapest tab, price lock, deep-link resolution — do not change the unit count, only the work behind it.
- Usage is recorded when a job finishes, as a success or a failure — a
failedjob is billed like acompletedone. Your daily and monthly quotas are checked against what has been recorded so far, so jobs still running are not yet counted. - The per-minute rate limit is checked when you submit. Expansion requests are rate-checked before they are validated, so a refused expansion (
400/404) still uses a slot of the per-minute allowance; a refused search or batch does not. - Polling,
/batch/results/andGET /usage/are never counted. Quotas are pooled per client across both sources.GET /api/v1/flight-scraper/usage/reports your limits, billing period and daily counts.
Next: MakeMyTrip Flights →