Scraper · OTA

MakeMyTrip Flights

OTA flight pricing straight from MakeMyTrip's own site — the same job-based /scrape/ contract as Google Flights, with MakeMyTrip's own funnel layered on top: fare families, a price-lock premium, full review detail (baggage, cancellation, discounts and coupons) with on-request coupon pricing, the tiered price-lock options, and — the one stage that writes rather than quotes — a checkout that reaches the convenience fee, which nothing earlier in the funnel states.

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

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

Shared endpoint, different contract. /api/v1/flight-scraper/scrape/ also prices Google Flights — set "source": "makemytrip" to get this OTA instead of the default. The flight object is the same shape as Google Flights, so a client already reading flights[] needs no changes for the common fields, but several fields behave differently or are always null here — see below. A key without the makemytrip scope gets 403 {"detail": "This API key does not have the 'makemytrip' scope."} on submit and on poll, and on every expansion below (/scrape/fares/, /scrape/review/, /scrape/farelock/, /scrape/checkout/). Status codes, rate limits and what is billed are the same as for Google Flights — see Errors, limits & billing.

Submit a search — L1

Omit return_date for a one-way search; include it for a round trip (see Round trips — MakeMyTrip prices these differently from Google Flights).

FieldTypeRequiredDefaultNotes
originstringRequired—3-letter IATA, e.g. DEL — or a metro area code such as NYC. MakeMyTrip resolves a metro code itself; see below for how the response matches it.
destinationstringRequired—3-letter IATA or metro area code, ≠ origin.
departure_datedateRequired—YYYY-MM-DD; not past, ≤ 1 year.
return_datedateOptionalnull≥ departure, ≤ 1 year ahead; omit for one-way. Fine on a MakeMyTrip search — see Round trips.
adultsintOptional11–9.
childrenintOptional00–8.
infants_in_seatintOptional00–8. Summed with infants_on_lap before the request goes out — see below.
infants_on_lapintOptional00–8; the sum of both infant fields must be ≤ adults.
cabin_classstringOptionaleconomyeconomy, premium_economy, business, first.
max_stopsintOptionalnull0–3. Applied after the fetch, not upstream — doesn't reduce bandwidth, and an itinerary whose stop count couldn't be read is kept rather than dropped. The cheapest_* prices in search_context are computed after this filter, so each is always a fare you can find in flights[] / return_flights[]; if the filter removes everything they are null and no_results is true. stops_filtered_out tells you how many itineraries it dropped — re-run with a higher max_stops to price them.
regionstringOptionalinOne of in, us, gb, eu, ae, ca, au, th, my, id, sg.
currencystringOptionalregion default3-letter ISO.
include_cheapestboolOptionalfalseGoogle-only capability. Accepted here but silently has no effect — a MakeMyTrip result has no cheapest_tab key at all.
include_price_lockboolOptionalfalseFetches MakeMyTrip's "Lock this price" premium — one extra request. See Price lock.
sourcestringOptionalgoogle_flightsSet to "makemytrip" to use this source — the endpoint prices Google Flights when it is omitted, so every example on this page sends it.
platformstringOptionalappapp or desktop — which MakeMyTrip client prices the job (Paytm, the only other source with two clients, accepts it too). A caller that never sends it keeps the app behaviour it already has. See Platform.
Rejected at submit, not silently ignored: include_calendar: true and per_leg_fares: true — both Google Flights-only capabilities — return 400 on a makemytrip search (sending them as false is accepted) rather than being accepted and dropped. include_cheapest is the one exception: it's accepted and silently a no-op (see the table above).
The rule runs the other way too. include_price_lock is MakeMyTrip-only and platform applies only to MakeMyTrip and Paytm — sending either of them with source: "google_flights" (or omitting source) is rejected with 400, not silently ignored.
Seated passengers (adults + children + infants_in_seat) must total ≤ 9, same as Google Flights. Unlike Google Flights, MakeMyTrip's passenger token carries only one infant field, so infants_in_seat + infants_on_lap are summed into a single count before the request goes out; that sum must still be ≤ adults.
POST /api/v1/flight-scraper/scrape/ — MakeMyTrip
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, "source": "makemytrip" }' # → 202: { "job_id": "...", "status": "pending", "poll_url": "..." }

Poll for results

Poll every 1–2 seconds until status is completed or failed. 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. Same envelope shape as Google Flights.

The job record also carries leg, the funnel stage it ran: search for /scrape/ and /batch/, fare_families for /scrape/fares/, review for /scrape/review/ (including coupon pricing), farelock for /scrape/farelock/ and checkout for /scrape/checkout/. It is set at submit time, so a job that failed before answering still names its stage, and the jobs of one chain are distinguishable without inspecting result.

GET/api/v1/flight-scraper/scrape/{job_id}/

result.flights[] carries the same fields as Google Flights (price, currency, segments[], etc.) with these differences:

FieldOn MakeMyTrip
booking_url, listingAlways null (published shape, not an error) — MakeMyTrip has no equivalent to Google's Best/Other split or a direct booking link.
booking_tokenCarries MakeMyTrip's own rkey instead of a Google token — this is what fare families and review expand.
segments[].aircraft, origin_airport, destination_airport, duration_minutesAlways null — not published on this source's listing page.
segments[].arrival_timenull on every segment but the last — the listing publishes only the final arrival. segments[].via lists any technical stops on that segment (usually empty).
duration_minutes (itinerary level)MakeMyTrip's own stated flying time for the journey. On international journeys this no longer equals arrival minus departure (those are two local clock times), and the date of arrival_time follows MakeMyTrip's "+1 DAY" count.
airlines, segments[].airlineOne entry per carrier (["Air India", "British Airways"], not a single comma-joined string), and filled on business-class and domestic round-trip listings as well.
pair_journey_keyPresent on every row. Identifies the partner journey of a paired round-trip fare.
pricePer adult, as listed. With several passengers the review total is higher than the listed price multiplied by one adult.
price_lockAlways present as a key; filled in only when include_price_lock was set — see Price lock.
fare_is_pairtrue when price pays for this flight and its return leg together, not this leg alone — international round trips only. See Round trips.

search_context.platform and search_metadata.platform echo which client (app or desktop) priced the job — present on every MakeMyTrip result, including failed ones, so provenance is readable from the job alone. See Platform.

Empty is not failure. A completed job with flights: [] and search_context.no_results: true is a successful search that found nothing. MakeMyTrip signals an empty route in two ways: an HTTP 404 (usually carrying its "No Flights Found" page, sometimes nothing at all), and (on app) a 200 carrying a complete listing page with no flights on it. Either way the search is repeated once from a fresh exit and, if it agrees, completes this way rather than failing — neither is reported as rotation. On platform=app the confirming attempt appears in result.timings.failed_attempts as "HTTP 404 (confirming)" for a bare 404, "MakeMyTrip page No-Flights-Found (confirming)" for the page, or "MakeMyTrip page Empty-Listing (confirming)" for the empty listing. A page that may still hold flights this parser cannot read (inventory present but unparsed, a partial or cut-off stream, or unread data in the response) is not treated as empty and still fails rotation. A 400 is different: it is a real refusal and still fails as invalid_search on the first attempt.

Discounts & coupons — free with every listing

No flag, no extra request: every /scrape/ and /batch/ result on MakeMyTrip carries two kinds of coupon data, and they are not the same set.

FieldMeaning
flights[].slashed_priceThe pre-discount price that card strikes through. null = not advertised, never "no discount". Measured live: an international-only thing — every struck-through card on file was on a DEL→DXB search, none on DEL→BOM.
flights[].discountslashed_price - price. null when there's no strike-through.
flights[].offers[]Coupons that card advertises: code (string, or null when the saving is already baked into price rather than something you'd type in), amount (rupees off) or percent (percent off) — at most one of the two is set (both are null when the card names a code without a figure, or its copy wasn't recognised) — and text, MakeMyTrip's own copy. Amounts are per flight: the same code has read differently on two cards in the same listing.
coupons[]The route-level tray — the same codes on every card, no amounts. Each entry: code, description, item_code, icon, benefits[], terms_title.
search_context.coupon_count / offers_count / discounted_countEntries in coupons[]; itineraries carrying an offer; itineraries carrying a struck-through price. All three count flights + return_flights + nearby_airport_options together.
The card tray and the route tray name different codes — read both. A domestic listing's route-level coupons[] can offer five bank codes while its cards advertise only two of them (and vice-versa on an international one); neither list replaces the other.
These are advertised figures, not quotes. Nothing in a search applies a coupon — offers[] and discount are MakeMyTrip's own claims about its own card. Only price_coupons on /scrape/review/ applies a code against a live itinerary and reads the real net fare back. A bank-linked code (e.g. HSBCINT) also needs that card at payment, which this API cannot verify either way.

All three counts at 0 together on a route that had them yesterday is worth checking — that's the signal that distinguishes "this route genuinely advertises nothing" from "the fields weren't populated".

Batch scraping

Search up to 20 routes that share dates/region in one call, the same /batch/ + /batch/results/ pair as Google Flights, with source: "makemytrip" at the batch level.

POST/api/v1/flight-scraper/batch/
POST/api/v1/flight-scraper/batch/results/
A makemytrip batch carrying include_calendar or per_leg_fares is rejected with 400 and creates no jobs at all — the same capability rule as a single search, checked before any job in the batch is created. Conversely, a batch on a source other than makemytrip carrying include_price_lock is rejected the same way, as is one carrying platform on any source other than makemytrip or paytm — all share the same validation check. return_date (round trips) and include_price_lock are both fine on a makemytrip batch.
platform follows source exactly: one platform per batch request, not per route, checked before any job is created. A platform: "desktop" batch carrying include_price_lock is rejected as a whole with 400, for the same reason it is on a single search — see Platform.

/batch/ and /batch/results/ behave exactly as described for Google Flights: 1–20 routes per batch, a shared set of dates and options, one request unit per route against the per-minute rate limit, and result records that may come back in any order and omit IDs you cannot read. Shared MakeMyTrip options: source, platform, include_price_lock.

Quota: each route consumes one request unit; include_price_lock adds one upstream call per route, not a quota unit (the whole listing is enriched in a single call per route, not per flight — see Price lock).

Price lock — include_price_lock

MakeMyTrip sells an option to hold a fare for a while before committing — "Lock this price @ ₹179". That premium isn't part of the listing response: filling it in costs one extra request per search, so it's off by default. Set include_price_lock: true on /scrape/ or /batch/ to fetch it.

POST /scrape/ — MakeMyTrip, price lock
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-10-17", "source": "makemytrip", "include_price_lock": true }' # → poll as usual. Every card that advertises a lock then carries one: # "price_lock": { "text": "Lock this price @ ₹ 179", "amount": 179, # "lockable": "payLaterCardTagLockable", "detail_api_path": "/api/v2/review-farelock" }
FieldMeaning
flights[].price_lockAlways present as a key; null when not requested, or not advertised on that card.
price_lock.textMakeMyTrip's own wording, with its display markup stripped.
price_lock.amountThe premium, parsed out of text. null when no figure was stated — never 0.
price_lock.lockableMakeMyTrip's own eligibility tag for the card, passed through unchanged.
price_lock.detail_api_pathMakeMyTrip's own upstream path for the tiered lock options, passed through unchanged.
search_context.price_lock_requestedWhether include_price_lock was set.
search_context.price_lock_countHow many cards came back carrying one.
One request per search, not per flight. The whole listing is enriched in a single call — both directions of a round trip included — so a 200-flight page costs the same one round trip as a 5-flight one.
A refusal here never fails the search. The fares are the product; the lock is a bonus. If MakeMyTrip refuses the enrichment, the job still completes with every flight priced, every price_lock comes back null, and the reason appears in search_context.warnings as price lock unavailable: ….
price_lock_requestedprice_lock_countMeans
false0You didn't ask for it.
true0You asked; MakeMyTrip advertises no lock on this route.
true> 0Cards without a lock are ones MakeMyTrip doesn't offer it on.

What this gives you is the teaser only: the single headline premium a traveller sees on the card. The tiered options behind the CTA — 12 hours / 1 day / 2 days / 4 days, each at its own price — come from a separate expansion, POST /scrape/farelock/; detail_api_path is passed through so you can see where MakeMyTrip keeps them.

include_price_lock is MakeMyTrip-only. Sending it with source: "google_flights" (or omitting source) is a 400, not a silent no-op — Google publishes no equivalent premium.

Round trips — two different pricing shapes

Send return_date and both directions come back in one job — but MakeMyTrip prices a round trip two different ways, and which one you get depends on the route, not on anything you send. result.is_combo (top level, present on every completed and failed job — false rather than absent on the error path, so a caller never has to tell "not a combo" apart from "the job never got far enough to say") tells you which shape you're looking at. It's meaningful only for round trips: a one-way search (no return_date) has no return leg to pair a fare with, so is_combo is always false there too — check search_context.trip_type if you need to tell "one-way" apart from "round trip, not a combo".

is_comboShapeMeasured on
falseTwo independently priced legs, summed. Below.Domestic routes — every one checked.
trueOne card, one combined fare covering both directions. Below.International routes — every one checked, in either of the two combo layouts: paired cards (below) or the sequential layout, which has the same flag but no named return flight (see the note under the table).
Some international round trips come back in a sequential layout. On a few routes MakeMyTrip answers with outbound flights at round-trip prices and no named return partner. The job completes with is_combo: true, fare_is_pair: true on each outbound row (its price is the cheapest round trip starting with that flight), pair_journey_key unset, an empty return_flights, and a warning in search_context.warnings. As on any combo, cheapest_matching_price and cheapest_return_price are null and cheapest_round_trip_price is the cheapest of those pair fares, so the guidance below holds in this layout too. Those rows cannot be expanded: fare families, review and the other expansion endpoints answer 400 for them.
Caller-visible change: is_combo and flights[].fare_is_pair are new on the wire. A client that picks an outbound from flights[] and a return from return_flights[] and adds their two prices will now double-count on international routes — both entries carry the same pair fare there (fare_is_pair: true on each), so summing them counts it twice. Check fare_is_pair before summing individual flight prices, or just read cheapest_round_trip_price, which is correct on both shapes without needing the check — cheapest_matching_price / cheapest_return_price are null on a combo precisely so a naive sum of those two fields can't happen either (see below).

Domestic — two independently priced legs

MakeMyTrip prices a domestic round trip as two separately selectable legs, each in its own list, each carrying its own fare:

result (domestic round trip, abridged)
{ "is_combo": false, "flights": [ /* outbound, each its own fare, fare_is_pair: false */ ], "return_flights": [ /* inbound, each its own fare, fare_is_pair: false */ ], "search_context": { "trip_type": "round_trip", "cheapest_matching_price": 8268, "cheapest_return_price": 7578, "cheapest_round_trip_price": 15846, "route_honored": true, "nearby_airport_count": 0 } }

cheapest_matching_price and cheapest_return_price are the cheapest fare left in flights[] and return_flights[] respectively, after the nearby-airport split and max_stops (applied to each direction independently). cheapest_round_trip_price is those two added together — a sum, not a quote — and is null unless both directions came back priced. There is no return-expansion endpoint here: unlike Google Flights' /scrape/return/, there's nothing to expand, since both directions are already in the response. Fare families and review accept a return_flight_index to price both legs of a round trip together — required on this shape, since which return leg to join is a real choice.

International — one card, one pair price (combo)

An international round trip is not two independently-priced flights. MakeMyTrip lists it as one card holding both journey keys at one combined price — the pair is the product, not a bundle of two separately-priced ones. Both halves still land in flights[] / return_flights[] as separate entries — a client already reading those lists needs no shape change — but each carries the same fare, flagged fare_is_pair: true, and no per-direction price exists to report:

result (international round trip, combo, abridged)
{ "is_combo": true, "flights": [ { "price": 23471, "fare_is_pair": true, /* ... */ } ], "return_flights": [ { "price": 23471, "fare_is_pair": true, /* ... */ } ], "search_context": { "trip_type": "round_trip", "cheapest_matching_price": null, "cheapest_return_price": null, "cheapest_round_trip_price": 23471, "route_honored": true } }

cheapest_matching_price and cheapest_return_price are null here by design, not a gap — no outbound-only or return-only price exists on a combo listing, and reporting half the pair price as a one-way is exactly the misprice these fields exist to stop. cheapest_round_trip_price carries the real quoted total instead of a sum.

Fare families, review and checkout all treat a combo card the same way: the outbound card you pick (flight_index) has already chosen its return, so return_flight_index is not required on this shape — send it anyway and it's silently ignored, never a 400. This is the one behaviour that differs from the domestic shape, where naming the return leg is a real, required choice.

Platform — app vs desktop

MakeMyTrip has two clients behind this source, and they don't answer the same question equally well. platform picks one; it defaults to app, so a caller that never sends it keeps exactly the behaviour it already had. The makemytrip scope grants both — platform is a per-job argument, not a separate purchase.

app (default)desktop
Round tripsOne job, both legsOne job, both legs — still one request
include_price_lockSupported — see Price lockRejected (400)
/scrape/fares/, /scrape/review/, /scrape/farelock/, /scrape/checkout/SupportedRejected (400) against a desktop parent job
CabinsAll foureconomy, premium_economy, business; first only where the route sells it
currencyHonouredHonoured, but the client identifies as India, so answers are INR in practice
regionHonouredNo effect — pinned to the India storefront
Typical latency—~1 second

Why choose desktop. It has fewer moving parts behind it, so it fails less often for reasons that have nothing to do with the route being priced. Why stay on app. Fare families, the review page, price lock, the price-lock tiers and checkout only exist there — if you plan to expand an itinerary past the initial listing, use app.

Round trips on desktop are still one request, and follow the exact same is_combo shape rules as app — see Round trips: domestic routes still get two independently priced legs, international routes still return the combo pair fare on both flights and return_flights (fare_is_pair: true on each — summing them still double-counts). What differs on desktop is volume, not shape: a round trip returns more inventory per direction than the same route searched one-way, so expect a larger response and higher billed bandwidth than an equivalent one-way search.
⚠️ cabin_class: "first" on a route with no first-class service can read as blocked on desktop. MakeMyTrip's edge refusal and "this route doesn't sell that cabin" can look identical from the response alone, so the scraper reports error_category: "blocked" either way. If a first search on desktop reports blocked on a domestic route, check whether the route sells the cabin at all before treating it as an edge problem.
Completeness — check search_context.warnings. The desktop listing occasionally schedules more journeys than it prices. When it does, the result carries a warning naming how many, e.g. "3 of 99 scheduled journeys returned no fare; the cheapest fare for this route may be among them". Treat a non-empty warnings here as "this listing may understate the route", not as noise — the unpriced journeys are disproportionately likely to include the cheapest fare on the route.

⚠️ Nearby-airport itineraries are split out

MakeMyTrip's listing mixes in itineraries that don't fly the requested route — e.g. a DEL→BOM search returning flights to Navi Mumbai (NMI) or from nearby airports. Measured live: 9 of 100 itineraries on that route. Left in flights[], a naive min(price) would hand you a price for a route the flight doesn't actually fly.

flights[] (and return_flights[], checked against the reversed route) therefore contains only itineraries matching the requested route. Itineraries on another airport pair are in nearby_airport_options[]; ones whose route couldn't be read are dropped and counted in search_context.unroutable_count, and ones removed by max_stops are counted in search_context.stops_filtered_out (max_stops is not applied to nearby_airport_options[]). search_context.route_honored is false when any were split out or had an unreadable route, or when the listing is empty, and cheapest_matching_price is the on-route cheapest fare.

Not the same as Google Flights' search_context.nearby_airports — that's a suggestion list shown only when a search finds zero flights. nearby_airport_options is a set of real, priced itineraries split out of an otherwise successful listing.
Metro codes are expanded before matching, not compared literally. NYC, LON, PAR and the rest of the metro area codes name a city, not an airport — MakeMyTrip resolves them server-side and answers with real inventory into that city's airports. A DEL→NYC search returns flights into JFK, EWR and LGA, and every one of those counts as on-route: they land in flights[], and cheapest_matching_price is the cheapest among them. A flight to somewhere that is not that city — the same search can also return, say, Milan (MXP) — still lands in nearby_airport_options[]. Codes that name both a metro and a working airport (BKK, DXB, IST, KUL, SHA, TPE) are not expanded — a DXB search means Dubai International only, never Sharjah. Same table as Google Flights, used here to recognise the reply rather than to build the request — unlike Google Flights, a MakeMyTrip result carries no origin_airports / destination_airports field.

Errors on this source

The error_category vocabulary is Google Flights' with two MakeMyTrip-only additions, fare_unavailable and pan_required (see checkout failures); the shared categories carry different meanings, and on this source they differ again by platform: transient means the request never got a usable answer (a dropped connection, a timeout, a 429 from MakeMyTrip that outlasted every attempt, a 5xx from MakeMyTrip's own servers, or our own time limit) — it is safe to resubmit a search, fare-families or review job, but not a checkout: a checkout that fails transient may already have held an itinerary, so check result.checkout first (see checkout failures). An app search, fare-families expansion or review that gets no usable answer is retried inside the job (up to 3 attempts), each from a fresh exit, before it reports transient, so a failing search can take longer than a successful one (the review that opens a checkout is the exception on 5xx: it is retried on a hang, a truncated body, a 403 or a 429, but a 5xx from MakeMyTrip is reported as transient without a retry); blocked means the request was refused at the edge — resubmit, it rotates exit IP; rotation means the wire format changed upstream (engineering fix, not retryable) — never an empty listing page, which is the empty result above; invalid_input covers requests rejected before sending; fare_unavailable means the flight or fare you picked from a search is no longer on sale — its rkey has expired (on the fare families (L2), review (L3) or checkout expansion), or MakeMyTrip answered 404 to one of those expansions — not retryable, run a new search and pick again; a search that MakeMyTrip answers with "no flights" is unchanged — a completed job with an empty result, as described below; invalid_search means MakeMyTrip's own origin read the request and refused it — not retried, since a fresh exit earns the same answer. On app that's an HTTP 400, and the one measured trigger is cabin_class: "first" on a domestic route — MakeMyTrip sells first class internationally only, and a domestic search carrying it fails this way rather than degrading to a lower cabin. On a search, a refusal page under a 400, or a bare 400, is also invalid_search — but never a 404: MakeMyTrip's 404 on a search means no flights, which is asked once more from a fresh exit and then returned as a completed job with an empty result, with or without a page in the body; on the fare families, review and checkout expansions a 404 is fare_unavailable instead (the request was fine, the fare is gone). /scrape/farelock/ is not covered by fare_unavailable. A search MakeMyTrip answers with "no flights" is confirmed once from a fresh exit and then returned as a completed job with an empty result, not as a failure. On desktop, MakeMyTrip instead answers with its own refusal envelope for the same kind of request (a bad route, a delisted sector) rather than an HTTP 400 — see also the first-cabin gotcha under Platform. POST /scrape/ still answers 202 for such a search either way (the API can't know a route is domestic, or delisted, before trying it); the job then fails on poll.

Fare families (L2)

Expand one MakeMyTrip itinerary into the fare families sold on it (e.g. SAVER / FLEXI / MMTSPECIAL, or airline tiers). MakeMyTrip only, and app platform only — this endpoint returns 400 for a Google Flights job, and the same 400 for a job that ran on platform: "desktop" (re-run the search with platform: "app" to expand it — see Platform).

POST/api/v1/flight-scraper/scrape/fares/
POST /scrape/fares/
# one-way curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/fares/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_id": "<completed makemytrip job>", "flight_index": 0 }' # domestic round trip — the two legs are priced together as a pair, so name both curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/fares/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_id": "<completed job>", "flight_index": 0, "return_flight_index": 0 }' # international combo round trip (is_combo: true) — the card already names its # return leg, so return_flight_index is neither needed nor accepted as meaningful curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/fares/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_id": "<completed combo job>", "flight_index": 0 }' # → 202 with a new job_id. Poll it: result.fare_families[] = # { name, cabinClass, fareType, fare, finalFare, slashedFare, fareId, journeyIndex } # result.offers[] = the fare sheet's own coupon strip — see below

Coupons on the fare sheet — offers[]

No flag, no extra request: every fare-families result also carries the coupon strip MakeMyTrip draws under the fare cards (e.g. "Get Flat 12% OFF using MMTHSBC code"), in the same four keys as the L1 listing's flights[].offers[] — code, amount, percent, text.

FieldMeaning
offers[]The fare sheet's coupon strip. Not per fare family, even though MakeMyTrip's app repeats the strip under every card — it sits at the top level of the result, alongside fare_families[]. Repeated tags are deduplicated; genuinely different ones are all kept.
search_context.offers_countLength of offers[] — not the same thing as the L1 search_context.offers_count, which counts itineraries advertising an offer rather than offers themselves.
offers: [] with offers_count: 0 means MakeMyTrip advertised none on this sheet — the keys are always present, on failed expansions too, so their absence is never something a client has to guess at.
Not to be confused with L1's coupons[]. At L1, coupons[] is the route-level tray — a different structure, carrying codes but no amounts. This is the per-card kind, which is why it's named offers here too. It also isn't guaranteed to match the same flight's L1 flights[].offers[] — the two come from different MakeMyTrip responses.

Review (L3, read-only)

Expand one MakeMyTrip itinerary into its review page — the richest response in the funnel: the fare split into base and tax with the line items behind it, the baggage allowance per leg and passenger type, the cancellation and date-change rules, and the coupon tray as advertised. MakeMyTrip only, and app platform only — this endpoint returns 400 for a Google Flights job, and the same 400 for a job that ran on platform: "desktop" — see Platform. Same request shape as fare families (L2) — a completed search plus which itinerary — and the same 202-then-poll flow; the two expansions are independent, both keyed off the original L1 search job.

POST/api/v1/flight-scraper/scrape/review/
POST /scrape/review/
# one-way curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/review/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_id": "<completed makemytrip job>", "flight_index": 0 }' # domestic round trip — both legs' baggage and rules in one call, name both legs curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/review/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_id": "<completed job>", "flight_index": 0, "return_flight_index": 0 }' # international combo round trip (is_combo: true) — no return_flight_index needed curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/review/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_id": "<completed combo job>", "flight_index": 0 }' # → 202 with a new job_id. Poll it: result.review = { itineraryId, totalAmount, # baseFare, tax, breakup[], baggage[], cancellation[], dateChange[], coupons[], # trips[] }
FieldMeaning
review.itineraryIdMakeMyTrip's handle for this priced itinerary.
review.totalAmountThe price, as a number.
review.baseFare · review.taxThe split, read from the response's numeric tracking block, not the rendered currency strings. totalAmount comes from the fare breakup and the two are not reconciled, so check rather than assume they sum to it (baseFare is the adult base fare).
review.breakup[]The same split as display line items: title + amount.
review.baggage[]Per leg and paxType: cabin and checkin allowance. On a round trip, both legs come back in the one call.
review.cancellation[] / review.dateChange[]Per leg: title, plus a raw block carrying the airline's full slabs — kept as-is rather than forced into one schema, since slab structure varies between airlines.
review.coupons[]code, title, message, description — plus the coupon's own claimed value: advertisedAmount, capped (true = "up to ₹X", a ceiling rather than a promise), types[] (MakeMyTrip's own classification — ANCILLARY_CT bundles a product rather than only discounting), enabled (false only when the page explicitly greys it out). Advertised, not priced (see below).
review.trips[]One entry per leg — airline, flightNumber, cabin, fareFamily — read from the header card the review page renders, not the listing. Empty ([]) on a page with no header card; every other field still parses.
review.retrievedAtWhen the page was fetched.
search_context.itinerary_id / total_amount / base_fare / taxMirrors the same fields on review, at the top level for quick access.
search_context.coupons_advertisedCount of review.coupons[].
search_context.coupons_pricedfalse by default. true only when price_coupons: true was sent and the tray was not empty — see Coupon pricing.
search_context.coupons_shortlistedPresent only when coupons were actually priced (price_coupons: true and a non-empty tray) — as are coupons_not_priced, coupons_disabled and coupons_restored. How many of the advertised tray were actually attempted — the best candidates by advertisedAmount.
search_context.coupons_not_pricedRanked out before pricing by their advertised value — not measured and rejected.
search_context.coupons_disabledGreyed out by MakeMyTrip (enabled: false), so never a candidate. coupons_shortlisted + coupons_not_priced + coupons_disabled == coupons_advertised.
By default, coupons here are advertised, not priced. The tray now states what each one claims to be worth — advertisedAmount — at no extra request cost, and on every tray measured it matched what pricing it actually returned. Use it to rank, not to bill: it's MakeMyTrip's word, not a measurement, until you opt in with price_coupons: true, covered next in Coupon pricing.
trips[] is what was actually priced — totalAmount alone isn't proof of which flight or fare family produced it. MakeMyTrip can reprice between the listing and the review: a measured DEL→BOM search had the listing card quote ₹6,079 for a SAVER itinerary and the review price the same flight, same fare family, at ₹6,530. Compare trips[].fareFamily against the fare families L2 returned for the same booking_token — matching the family L1 quoted means the gap is MakeMyTrip repricing, not the token resolving to a different flight or a pricier family. Note that an L2 family can undercut what the listing itself advertised as cheapest, since a listing card carries fewer fare families than /scrape/fares/ returns.
Reading goes further than it used to — writing is now possible too, on its own endpoint. The upstream client's review stage continues into traveller-info, form-submit and ancillary-submit calls that issue a real checkoutId against a real held itinerary — every full run leaves an abandoned checkout behind on MakeMyTrip. /scrape/review/ itself still issues a single GET and nothing past it; that write chain is now exposed separately, deliberately opt-in, at POST /scrape/checkout/ — read that section's warnings before calling it.

Same guards and rkey rules as fare families: parent must be a completed MakeMyTrip job, return_flight_index is required on a domestic round-trip parent (not on an international combo one — see Round trips), and a stale key fails as error_category: "fare_unavailable" rather than transient — re-run the search instead of retrying. Unlike fare families, the review response does not reissue a new rkey; it's the last stop in the read-only part of the funnel.

Coupon pricing — price_coupons

Add price_coupons: true to a /scrape/review/ request and, instead of just listing the advertised tray, each coupon is actually applied to the live itinerary and read back priced. This is a different list from the two the search already gave you (see Discounts & coupons) — the search advertises, this is the only thing on this API that prices.

POST /scrape/review/ — price the coupon tray
curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/review/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_id": "<completed makemytrip job>", "flight_index": 0, "price_coupons": true }' # → poll as usual: result.coupon_pricing = # { "baselineTotal": 8268, # "coupons": [ # {"code": "MMTSUPER", "total": 7402, "discount": 866, "saving": 866, # "advertisedAmount": 866, "measured": true, "applied": true}, # {"code": "MMTSECURE", "total": 8268, "discount": 0, "saving": 0, # "advertisedAmount": 0, "measured": true, "applied": false, "note": "…"} ], # "bestCode": "MMTSUPER", "bestTotal": 7402, "bestDiscount": 866, # "pricedCount": 2, "trustedCount": 0, "restored": true }
FieldMeaning
coupons[].totalNet payable with this coupon.
coupons[].savingbaselineTotal - total — what you actually pay less. Can be negative.
coupons[].discountWhat the "Discounts" breakup line credits — not always the same number as saving, see below.
coupons[].cartChangedThe coupon attached a product (e.g. Trip Secure) rather than only discounting.
coupons[].advertisedAmountWhat the tray claimed this coupon was worth, before it was priced.
coupons[].measuredfalse = this saving is the tray's claim, taken on trust after nearby coupons on this itinerary matched their own claim exactly — never applied. applied is then null (never asked), which is a different answer from false (MakeMyTrip refused it).
coupons[].appliedfalse = MakeMyTrip refused this coupon for this fare — a real answer, not an error. null = never asked, see measured.
coupons[].failedtrue = the apply never got an answer (timeout, dead exit) — unjudged, not rejected; note says why.
pricedCount / trustedCountHow many coupons were actually sent to MakeMyTrip and answered, and how many were instead taken at their advertised figure once nearby coupons on this itinerary matched their own claim exactly. trustedCount: 0 means everything returned was measured.
bestCode / bestTotal / bestDiscountThe coupon with the lowest resulting total among those MakeMyTrip did not refuse — including any taken on trust.
restoredWhether the itinerary was put back the way it was found — see below.
discount and saving are different numbers — saving is the honest one. A coupon that attaches a product can credit a discount while making the total dearer: measured live, one code credited ₹259 while raising the total by ₹90, because it also added ₹349 of Trip Secure. Rank or filter on saving, not discount.
Why this is opt-in: applying is stateful — and now it's selective, too. A new coupon replaces the previous one, and re-applying the same code toggles it off, so coupons are priced down one live itinerary, sequentially, in order — a cart-changing coupon can leave its product attached for every coupon priced after it, understating their saving. Rather than exhausting the whole tray, only the best candidates by advertisedAmount are actually applied (a shortlist of 4 by default), and once the first two of those match their own claim exactly, the rest of the shortlist is taken at its advertised figure instead of applied — see measured / trustedCount above. One mismatch anywhere in that check — a refusal counts — and every remaining candidate is priced for real. The pricer always finishes by taking the last applied coupon back off — always check restored: false means that didn't take, and that itinerary is left discounted (any later read, including a re-fetched review, will show a reduced fare that isn't the base fare). coupon_pricing.restoreNote says what happened. Measured live: a 32-coupon tray that used to cost 11 MB and 137 s of coupon pricing now returns in 1.2 MB and 14.9 s, with the same top coupon.

Sending price_coupons: true to /scrape/checkout/ is rejected with 400 rather than silently ignored — a checkout's itinerary is the one being held, and pricing coupons down it the way review does could alter that cart irreversibly. Use include_coupon_breakups there instead: it prices the tray on a disposable itinerary and never touches the held one.

Choosing a fare — fare_index

By default every expansion prices the cheapest fare on the itinerary you pick. Add fare_index to ask for another one — it needs nothing extra from the search.

POST /scrape/review/ — the third fare on row 0
curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/review/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_id": "<completed makemytrip job>", "flight_index": 0, "fare_index": 2 }'

Measured live on DEL→DXB (29 Oct 2026), one journey, only fare_index changing:

fare_indexreview.trips[].fareFamilyTotal
0 (or omitted)Economy Super Saver₹21,400
1Economy Comfort₹23,862
2Economy Flex₹30,448
3 and up—fare_unavailable

fare_index works on /scrape/review/, /scrape/farelock/ and /scrape/checkout/, so the convenience fee can be read per fare. search_context.fare_index echoes what you asked for.

The index is MakeMyTrip's own, not a position in /scrape/fares/. On the journey above the fare sheet listed six families and fare_index reached three (MMTSPECIAL and both Business fares were not reachable). Read review.trips[].fareFamily to see which fare answered. The number of fares available is not published, so an index past the end is found by asking: it fails as fare_unavailable (not retryable).

Price-lock tiers — POST /scrape/farelock/

The price-lock teaser on a listing card is one headline premium. This expansion returns the tiered options behind it for one itinerary — what it costs to hold the fare for 12 hours, 1 day, 2 days or 4 days, each at its own price. MakeMyTrip only, and app platform only — 400 for a Google Flights job and for a platform: "desktop" parent, like the other expansions. Read-only: it reads the advertised tiers and selects nothing.

POST/api/v1/flight-scraper/scrape/farelock/
FieldTypeRequiredNotes
job_iduuidRequiredA completed MakeMyTrip search job (app platform).
flight_indexintRequired≥ 0; position in that job's result.flights.
return_flight_indexintOptional≥ 0; position in result.return_flights. Required for a domestic round trip (the two legs are priced as a pair); not needed for an international combo (is_combo: true) — see Round trips.
POST /scrape/farelock/
curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/farelock/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_id": "<completed makemytrip job>", "flight_index": 0 }' # → 202: { "job_id": "...", "status": "pending", "poll_url": "..." } # Poll it like any job. result.farelock = # { "options": [ # { "duration": "12 hours", "price": 305, "itemCode": "FL12H", "selected": false }, # { "duration": "1 day", "price": 356, "itemCode": "FL1D", "selected": true }, # { "duration": "2 days", "price": 407, "itemCode": "FL2D", "selected": false }, # { "duration": "4 days", "price": 458, "itemCode": "FL4D", "selected": false } ], # "cheapestPrice": 305, "retrievedAt": "2026-10-15T09:41:07+00:00" }
FieldMeaning
farelock.options[]One entry per tier MakeMyTrip advertises, normally four. duration is MakeMyTrip's own label, price the premium for that tier (an integer), itemCode MakeMyTrip's code for the tier, and selected marks the tier MakeMyTrip pre-selects.
farelock.cheapestPriceThe lowest price in options; null when there are none.
farelock.retrievedAtWhen the tiers were fetched (ISO 8601).
search_context.tier_countLength of options.
search_context.cheapest_lock_priceMirrors farelock.cheapestPrice.
search_context.no_resultstrue when the itinerary advertised no tiers. That is a successful, empty answer — not a failure.
search_context.applied_currencyThe job's currency (INR when you sent none).
search_metadata.leg, flight_index, return_flight_indexleg is "farelock"; the indices echo what you sent.
Billing and quota. A price-lock job is one request unit against your rate limit and quota, like every other submit, and is billed when it finishes whether it completes or fails. Like the other expansions it repeats the parent search's passengers, cabin, region and currency.
Availability. This expansion is switched on per deployment (not per key) and is not available everywhere. Where it is not enabled, the job is still accepted (202) and then finishes failed with error_category: "error" and an error_message explaining that it is not enabled — nothing is fetched. Contact us if you need it. Other failures use the usual categories.

Checkout — POST /scrape/checkout/ ⚠️ writes

Every other endpoint on this source quotes. This one holds inventory. It runs the itinerary through MakeMyTrip's own checkout chain to a live checkoutId and reads the resulting payment page — the only place the convenience fee and the true final total appear. Every other total in this funnel, including the review, excludes that fee. app platform only — 400 for a platform: "desktop" parent job, same as fare families and review — see Platform.

POST/api/v1/flight-scraper/scrape/checkout/
It stops short of paying, but it still writes. Registering passengers, binding a contact and email to the itinerary, and issuing a checkoutId leaves a real, abandoned checkout behind on MakeMyTrip's systems on every single call. Do not put this on a bulk or exploratory path, and do not call it to price things — /scrape/review/ prices things.

Same request shape as fare families / review — a completed MakeMyTrip search job, flight_index, and (round trips, domestic only — see Round trips) return_flight_index — plus one flag of its own:

POST /scrape/checkout/
curl -X POST https://api.scrapeguys.com/api/v1/flight-scraper/scrape/checkout/ \ -H "X-API-Key: fsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "job_id": "<completed makemytrip job>", "flight_index": 0 }' # → 202 with a new job_id. Poll it: result.checkout = { checkoutId, itineraryId, # travellerCount, bookingAmount, payableAmount, ancillary: { breakup[], addOns[] } } # result.payment = { checkoutId, totalDue, convenienceFee, breakup[] }
FieldMeaning
include_coupon_breakupsRequest field, bool, default false. See below.
checkout.checkoutIdMakeMyTrip's handle for the held itinerary — this is the write.
checkout.bookingAmount / payableAmountThe fare total, before the convenience fee.
payment.totalDueThe real final amount, convenience fee included.
payment.convenienceFeeThe figure this endpoint exists to reach — 0 everywhere else in the funnel.
search_context.review_total / total_dueMirror the fee-excluded and fee-included totals at the top level; the gap between them is the whole point of this endpoint.
search_context.held_checkoutAlways true on success — states plainly that this job left a real checkout behind.
search_context.traveller_fields_requiredWhich passenger fields this itinerary's own traveller form demanded, e.g. ["FIRST_NAME","GENDER"] or, on a route that wants travel documents, ["DOB_ADULT","FIRST_NAME","GENDER","NATIONALITY","PASSPORT_DATE_OF_EXPIRY","PASSPORT_ISSUING_COUNTRY","PASSPORT_NUMBER"]. Field names only — see below.
tcs.payableAmount / tcs.ratePresent only on a pan_required failure, at the top level of the result — the tax collected at source that triggered the gate, named exactly as MakeMyTrip's response stated it.
panSubmittedAlongside the top-level tcs on a pan_required failure. true when a real PAN already reached MakeMyTrip before the gate stood anyway (a refusal, an unreachable widget, a failed consent call) — see below.
checkout.tcs / checkout.panSubmitted / checkout.panGateClearedThe same shape, nested inside checkout instead, whenever this itinerary raised the gate at all — including once it clears. panGateCleared: true means the submitted PAN is why a checkoutId exists. Absent from checkout on an itinerary that never raised the gate.
It takes no passenger details. travellers, contact and email used to be required here and are now rejected with 400 if sent — refused rather than silently dropped, because a caller who sends a real passenger has every reason to believe it was used. MakeMyTrip's checkout chain needs a filled-in traveller form to reach the convenience fee but verifies none of it, so the form is generated instead: one synthetic adult per adults on the parent search, a fresh invented name and a @example.com email each run (RFC 2606 reserved — it reaches nobody), and a contact number the operator controls and points at deliberately, since — unlike the invented email — every syntactically valid Indian mobile number belongs to a real subscriber, and an abandoned checkout is exactly what an OTA sends "complete your booking" messages about. None of the generated identity is echoed back in the result; only a traveller count, and now the field names the form demanded, are.
Some itineraries also want a passport. The traveller form is not the same on every route, and it is not a domestic-versus-international split — measured the same day, BOM→DXB (SalamAir) demands passport number, expiry, issuing country and date of birth, while DEL→DXB (IndiGo), also international, asks only for a name and a gender. When the itinerary's own form asks for travel documents, this endpoint generates a syntactically valid Indian passport number, date of birth and expiry to match — nothing is inferred from the route. search_context.traveller_fields_required reports which fields the form asked for; a route whose form asks only for the original five fields is unaffected and gets exactly the body it always did.
Adults only. A parent search carrying children or infants is refused with 400 at submit time. The adult date-of-birth field is now known (DOB_ADULT, DD/MM/YYYY, with the valid window read straight off the form), but the child and infant variants of the form have not been seen, so a guessed field name there would still be rejected mid-chain — this is checked before anything is written instead.
Gated behind its own switch — and it ships on. The switch defaults to enabled, so the endpoint reaches all the way through with no configuration on the operator's side. It can be turned off independently as a dedicated kill switch; with it off, a checkout job fails error_category: "error" and nothing is written. The scraper's own top-level enable switch still gates this too, same as every other MakeMyTrip endpoint.

Coupon breakups — include_coupon_breakups

Add include_coupon_breakups: true and the result gains coupon_breakups: the cart with no coupon, plus the three coupons that actually save the most, each with its own fully repriced breakup. This is the checkout's equivalent of price_coupons on review — deliberately a separate flag, because it works differently under the hood.

FieldMeaning
coupon_breakups.baselineThe cart with no coupon on it — compare every entry below against this.
coupon_breakups.coupons[]0–3 entries, best saver first, ranked on saving (baseline.totalAmount - total) — not the discount line, for the same reason as coupon pricing: they disagree whenever a coupon changes the cart. Each entry also carries advertisedAmount (what the tray claimed) and measured (false = taken on trust, not applied — see below).
trayCountCoupons the review advertised.
shortlistedCountHow many were attempted — the best candidates by advertisedAmount (a shortlist of 4 by default, floored at 3).
notPricedCountRanked out before pricing by their advertised value — not measured and rejected.
disabledCountGreyed out by MakeMyTrip, so never a candidate. shortlistedCount + notPricedCount + disabledCount == trayCount.
trustedCountOf the shortlist, how many were taken at their advertised figure instead of applied — only once the first candidates (2 by default) matched their own claim exactly. 0 means everything returned was measured.
pricedCountHow many were actually sent to MakeMyTrip and answered.
failedCountOf the shortlist, how many never answered.
returnedCountHow many made the top-3 cut.
skippedCountHow many were never attempted because the wall-clock budget below ran out first. Non-zero means the answer is partial — the coupons returned are the best of what was measured, not the best of the tray.
failedCoupons[]Which codes never got a price back — unjudged, not rejected (different from a coupon MakeMyTrip actively refused).
This one doesn't touch your held booking. The tray is priced against a second, throwaway itinerary allocated alongside the held one, never the checkout you're about to be charged for — because applying a coupon can attach a product (Trip Secure, say) that removing it does not take back off, and no coupon can be identified as one of those in advance. The throwaway is abandoned afterwards without even trying to restore it — nothing reads that itinerary again, so toggling the last coupon back off would only spend one more request tidying a cart nobody will see.
Cost: at most a shortlist of coupons applied for real (4 by default, sequential — they mutate the same throwaway itinerary), plus one review fetched concurrently with the checkout's own write chain so it adds nothing to the job's critical path by itself. Once the first two applied coupons match their advertised figure exactly, the rest of the shortlist is taken at that figure instead of applied — see trustedCount above. Measured live: a 26-coupon tray that used to cost 9.3 MB and 230 s of coupon pricing alone now costs 0.56 MB and 7.2 s for the same top 3 coupons, and a whole checkout job end to end measured 15.2 s and 749 KB. It runs after the checkout itself completes, so a coupon-pricing failure never costs you a checkoutId that was already issued — on total failure, coupon_breakups.error says why and result.checkout is still returned in full.
Bounded by wall clock, not by coupon count. Each attempted coupon gets two attempts at the client timeout, so the shortlist's worst case still runs longer than its typical one — but since only the best few candidates are ever attempted rather than the whole tray, the 240-second wall-clock budget is now rarely the limiting factor. Checked between coupons, never mid-coupon, so a half-applied one never leaks into the result. When the budget does run out, skippedCount says how many were never attempted — read it before treating the returned coupons as the tray's best.

What comes back on failure

The chain is not atomic. A partial run is reported, not discarded — if a checkoutId was issued before a later step failed, it's in result.checkout; a checkoutId that exists and isn't returned would be an abandoned booking nobody could trace.

Failureerror_category
Checkout switch offerror — nothing written
Itinerary's traveller form demands a field this endpoint doesn't generateerror, error_message names the field — nothing written, caught before the first traveller-info call
Itinerary is gated on a PAN before MakeMyTrip issues a checkoutId, and the gate could not be clearedpan_required — pick a different itinerary, not a retry; the message and panSubmitted say why and whether a PAN already went out — see below
Stale rkeyfare_unavailable — nothing written, the chain never got past the review; re-run the search
MakeMyTrip answers 404 inside the chain, before a checkoutId exists (the fare has sold out)fare_unavailable — re-run the search. Earlier steps may already have registered the passengers, so check result.checkout for anything issued; a 404 from the payment page, after a checkoutId was issued, is not reclassified as fare_unavailable. pan_required takes precedence whenever the chain itself concludes the PAN gate stands; only a bare 404 that surfaces without that conclusion is fare_unavailable
MakeMyTrip's own servers fail (5xx)transient — not retried anywhere in the chain, the opening review included; running the checkout again is your call
Children/infants on the parent search400 at submit, before any job is created
Edge refusal / rate limitingblocked / transient — as elsewhere on this source
Origin refuses the request itselfinvalid_search — not retried, see Errors
Job ran too long and was stopped server-sidetransient, error_message names the time limit — see the warning below
The PAN gate is answered automatically where the operator has configured it to be — callers never supply a PAN. India collects tax at source on some overseas bookings, and MakeMyTrip demands a PAN before it will issue a checkoutId on the ones this applies to. This endpoint can clear that gate on the operator's own configuration — a small pool of real PANs, rotated deterministically per booking so a retry of the same held booking reuses the same number rather than attaching a second person to it. A caller of this API never chooses, sends, or sees which number was used; the response only ever states whether one was, via panSubmitted.

If the gate clears, the job succeeds as normal and checkout additionally carries tcs (what MakeMyTrip named as the tax), panSubmitted: true and panGateCleared: true alongside the usual checkoutId. pan_required is what's left when the gate could not be cleared — nothing configured on the deployment, the submitted PAN was refused, the widget was unreachable, or the call that mints the checkoutId failed after a PAN had already gone out — and the message names which. panSubmitted on this failure says whether a real PAN already reached MakeMyTrip even though the job failed: true on a refusal, an unreachable widget or a failed consent call; false when nothing was configured to submit in the first place. Either way it is not a retryable failure — pick a different itinerary; another one will check out normally.

The convenience fee is reachable without clearing the gate regardless: measured flat at ₹1,700 across gated and ungated international itineraries alike. /scrape/review/ still prices a gated itinerary normally — only holding one is gated.

The one call retried inside a checkout is the review that opens the chain, because it only reads: a hang, a truncated body, a 403 or a 429 is asked again from a fresh exit, up to three attempts in all. A 400, a 404, a 5xx or a stale key is not retried, and nothing after the review ever is.

A job stopped this way may already have written. Every job on this source is bounded by a server-side wall-clock limit — long enough for a normal checkout, short enough to stay comfortably inside the delivery window our own queue relies on to guarantee each job runs at most once. If a checkout is still mid-chain when that limit hits, the job fails transient with a message naming the limit — but a checkoutId it had already issued by that point is gone with the worker and is not traceable from the job row. Treat a timed-out checkout as possibly-held, not as never-started, the same as any other transient checkout failure.
This is not a booking product. It stops at the payment page and never pays. A real booking belongs on an authorised MakeMyTrip channel with a support path, a reconciliation route and a traveller who can actually be contacted — a checkout run through this endpoint has none of that, so the traveller behind the generated identity could never receive a confirmation, a schedule change, or a refund.

Next: Google Hotels →