Scraper · Meta

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.

POST/api/v1/flight-scraper/scrape/— submit
GET/api/v1/flight-scraper/scrape/{job_id}/— poll

Base URL: https://api.scrapeguys.com · Auth: X-API-Key: fsk_… with the google_flights scope · See the Overview.

This endpoint also prices MakeMyTrip. /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.

FieldTypeRequiredDefaultNotes
originstringRequired—3-letter IATA airport code, e.g. DEL — or a metro area code such as NYC, expanded to that metro's airports.
destinationstringRequired—3-letter IATA airport code or metro area code, ≠ origin.
departure_datedateRequired—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_datedateOptionalnull≥ departure; same 330-day limit as departure_date. Omit for one-way.
adultsintOptional11–9.
childrenintOptional00–8.
infants_in_seatintOptional00–8.
infants_on_lapintOptional00–8; ≤ adults.
cabin_classstringOptionaleconomyeconomy, premium_economy, business, first.
max_stopsintOptionalnull0–3; 0 = nonstop, null = no limit.
regionstringOptionalinOne of in, us, gb, eu, ae, ca, au, th, my, id, sg.
currencystringOptionalregion default3-letter ISO.
include_calendarboolOptionalfalseAdds a date-grid price calendar. See Calendar & per-leg.
calendar_daysintOptional450–180. Calendar window end: days after departure.
calendar_days_beforeintOptionalnull0–180. Start the calendar window this many days before departure; default (null) starts the window at today.
per_leg_faresboolOptionalfalseRound-trip only (silently ignored on one-ways); adds outbound/return one-way comparison. See Calendar & per-leg.
include_cheapestboolOptionalfalseAdds Google's Cheapest tab. Slow (~10–30s) and best-effort — see Cheapest tab before switching it on.
sourcestringOptionalgoogle_flightsLeave 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.
Seated passengers (adults + children + infants_in_seat) must total ≤ 9, and each lap infant needs an adult (infants_on_lap ≤ adults). origin and destination must differ.
Options that belong to other sources are rejected here, not ignored. 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.
Existing keys keep working. If your key predates per-source scopes it was migrated to full access automatically — omitting 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 codeAirports covered
BHZCNF, PLU
BJSPEK, PKX
BUEEZE, AEP
CHIORD, MDW
JKTCGK, HLP
LONLHR, LGW, STN, LTN, LCY, SEN
MILMXP, LIN, BGY
MOWSVO, DME, VKO, ZIA
NYCJFK, LGA, EWR
OSAKIX, ITM, UKB
PARCDG, ORY, BVA
RIOGIG, SDU
ROMFCO, CIA
SAOGRU, CGH, VCP
SELICN, GMP
SPKCTS, OKD
STOARN, BMA, NYO, VST
TYOHND, NRT
YMQYUL, YMX
YTOYYZ, YTZ, YHM
Codes that are also a working airport are not expanded. 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.
When 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.
POST /api/v1/flight-scraper/scrape/
curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "origin": "DEL", "destination": "BOM", "departure_date": "2026-08-19", "adults": 1, "cabin_class": "economy", "max_stops": 0 }' # → 202: { "job_id": "...", "status": "pending", "poll_url": "..." }

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.

GET/api/v1/flight-scraper/scrape/{job_id}/
GET /scrape/{job_id}/
curl https://api.scrapeguys.com/api/v1/flight-scraper/scrape/YOUR_JOB_ID/ \ -H "X-API-Key: fsk_your_api_key" # Repeat every 1–2 s until status is "completed" or "failed" (budget ~30 s)
Empty is not failure. A completed job with flights: [] and search_context.no_results: true is a successful search that found nothing — check nearby_airports for alternatives.

Job record

FieldDescription
idThe job's UUID — the job_id from submit.
statuspending, processing, completed or failed.
origin, destination, departure_date, return_date, trip_typeThe search as accepted. trip_type is one_way or round_trip.
adults, children, infants_in_seat, infants_on_lap, cabin_class, max_stops, region, currencyEchoed 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_linksEchoed 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_outboundnull on a search. On an expansion job (return, booking), the job it expands and the flight that was chosen.
resultThe scrape output described below; null until the job finishes.
error_message / error_categoryBoth empty strings ("") on success. On failure, the reason and one of the categories listed under Key fields.
created_at, started_at, completed_at, duration_secondsTimestamps; 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.

result (abridged)
{ "flights": [ { "airlines": ["SpiceJet"], "carrier_code": "SG", "flight_numbers": ["SG510"], "origin": "DEL", "destination": "BOM", "departure_time": "2026-08-19T04:00:00", "arrival_time": "2026-08-19T06:15:00", "duration_minutes": 135, "stops": 0, "layover_airports": [], "price": 6277, "currency": "INR", "booking_token": "CjRIUEJyeEhwTElYRFFB...", "listing": "top", "booking_url": "https://www.google.com/travel/flights/booking?tfs=...", "segments": [ { "origin": "DEL", "origin_airport": "Indira Gandhi International Airport", "destination": "BOM", "destination_airport": "Chhatrapati Shivaji Maharaj Intl", "departure_time": "2026-08-19T04:00:00", "arrival_time": "2026-08-19T06:15:00", "duration_minutes": 135, "flight_number": "SG510", "airline": "SpiceJet", "aircraft": "Boeing 737", "legroom": "29 in" } ] } ], "search_context": { "currency": "INR", "applied_currency": "INR", "currency_confirmed": true, "top_offer_count": 3, "other_offer_count": 65, "no_results": false, "nearby_airports": [] }, "search_metadata": { "origin": "DEL", "destination": "BOM", "trip_type": "one_way", "cabin_class": "economy", "region": "in", "applied_currency": "INR", "full_results": true, "attested_best_skipped_reason": "not enabled", "cheapest_tab_attested": false, "cheapest_tab_skipped_reason": "not requested" }, "calendar": null, "per_leg_fares": null, "cheapest_tab": null, "error": null, "error_category": null }

Key fields

FieldDescription
flights[].priceTotal price for the offer (round-trip total on round trips).
flights[].stops / layover_airportsStop count and connecting airport codes.
flights[].listingtop (best/featured) or other.
flights[].booking_urlPublic 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_indexRound-trip searches only: index to pass to /scrape/return/.
flights[].standalone_one_way_priceWith 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_resultstrue when a valid search returned no flights.
search_context.nearby_airportsAlternative airports [{ code, distance, price }].
search_context.currency_confirmedWhether Google confirmed pricing in your requested currency.
search_metadata.origin_airports / destination_airportsPresent 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_categoryOn 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 links: every offer carries a ready-made 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.

POST/api/v1/flight-scraper/batch/
POST/api/v1/flight-scraper/batch/results/
POST /flight-scraper/batch/
curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/batch/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "departure_date": "2026-08-19", "region": "in", "routes": [ { "origin": "DEL", "destination": "BOM" }, { "origin": "BLR", "destination": "GOI", "max_stops": 0 } ] }' # → 202: { "jobs": [ { "job_id", "origin", "destination", "poll_url" }, ... ] }
POST /flight-scraper/batch/results/
curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/batch/results/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_ids": ["uuid-1", "uuid-2"] }' # → 200: { "results": [ <job record>, <job record> ] } (1–20 IDs)

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.

Rate limit: a batch is admitted or refused as a whole. It needs one per-minute slot per route, checked before any job is created. If it does not fit within the allowance you have left, the 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.
Quota: each route in a batch consumes one request unit. The shared options multiply across the batch though — 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.

POST/api/v1/flight-scraper/scrape/return/
POST /scrape/return/
curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/return/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_id": "<completed round-trip job>", "outbound_index": 3 }' # → 202 with a new job_id. Poll it: result.return_options[] holds each # full round-trip total for that outbound + inbound pairing, and every # option carries a populated booking_url.

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.

POST/api/v1/flight-scraper/scrape/booking/
POST /scrape/booking/
curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/booking/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_id": "<completed job>", "flight_index": 0, "resolve_deep_links": false }' # → 202 with a new job_id. Poll it: result.booking_options[] = # { provider, provider_code, is_airline, price, currency, display_domain, # booking_url, deep_link, flight_numbers, legroom, # baggage: { free_checked_bags, free_carry_on, checked_bag_fees } } # plus price_insights, booking_url, selected_itinerary, baggage_policy_urls.
Polling budget: booking-options jobs can take meaningfully longer than searches while thin marketplaces are re-read. Budget up to ~60 seconds before treating a booking job as stuck (search jobs: ~30 s).

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.

This never fails a job. When no verified reading is available, or one comes back no better than the plain results, the plain flights[] stand unchanged.
FieldMeaning
search_metadata.attested_besttrue when a verified reading was attempted for this search. Absent when it was not — see attested_best_skipped_reason.
search_metadata.attested_best_appliedtrue 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_pricePresent when applied — offer count and floor price of the adopted (attested) reading, i.e. of flights[] itself.
search_metadata.unattested_offers / unattested_min_pricePresent when applied — the same figures for the plain reading that was replaced, so you can see what changed.
search_metadata.attested_best_errorPresent when a verified reading was attempted and the request itself failed.
search_metadata.attested_best_comparisonPresent 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_reasonWhy 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.

It is opt-in, and deliberately so — by far the most expensive option in this API. Leaving it off costs nothing at all: no extra request, no extra bytes, no change to timings.
include_cheapest: false (default)include_cheapest: true
Typical time~2–4s~10–30s
Extra data transferrednone~0.6–1.2 MB
Cold route (first search)—no Cheapest data; a token is prepared in the background
Do not diff the cheapest minimum against the Best minimum. A discount often lands on a mid-priced flight while the lowest fare is identical on both tabs — comparing minima reports "no saving" on searches that plainly have one. Compare per flight; 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.

FieldMeaning
search_metadata.cheapest_tab_attestedtrue 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_reasonWhy 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_countOffers returned, when it succeeded.
search_metadata.cheapest_tab_cheaper_countHow 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_errorSet 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.

StatusWhen
202A job was accepted (single search, batch, or expansion). Nothing has been scraped yet — poll.
400The request failed validation, or an expansion was refused. Validation errors are keyed by field ({"departure_date": ["…"]}); expansion refusals are {"error": "…"}.
401Missing, 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.
403The 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.
429Rate 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


Next: MakeMyTrip Flights →