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.
/api/v1/flight-scraper/scrape/— submit (L1)/api/v1/flight-scraper/scrape/{job_id}/— pollBase URL: https://api.scrapeguys.com · Auth: X-API-Key: fsk_… with the makemytrip scope · See the Overview.
/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).
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
origin | string | Required | — | 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. |
destination | string | Required | — | 3-letter IATA or metro area code, ≠ origin. |
departure_date | date | Required | — | YYYY-MM-DD; not past, ≤ 1 year. |
return_date | date | Optional | null | ≥ departure, ≤ 1 year ahead; omit for one-way. Fine on a MakeMyTrip search — see Round trips. |
adults | int | Optional | 1 | 1–9. |
children | int | Optional | 0 | 0–8. |
infants_in_seat | int | Optional | 0 | 0–8. Summed with infants_on_lap before the request goes out — see below. |
infants_on_lap | int | Optional | 0 | 0–8; the sum of both infant fields must be ≤ adults. |
cabin_class | string | Optional | economy | economy, premium_economy, business, first. |
max_stops | int | Optional | null | 0–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. |
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_cheapest | bool | Optional | false | Google-only capability. Accepted here but silently has no effect — a MakeMyTrip result has no cheapest_tab key at all. |
include_price_lock | bool | Optional | false | Fetches MakeMyTrip's "Lock this price" premium — one extra request. See Price lock. |
source | string | Optional | google_flights | Set to "makemytrip" to use this source — the endpoint prices Google Flights when it is omitted, so every example on this page sends it. |
platform | string | Optional | app | app 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. |
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).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.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.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.
/api/v1/flight-scraper/scrape/{job_id}/result.flights[] carries the same fields as Google Flights (price, currency, segments[], etc.) with these differences:
| Field | On MakeMyTrip |
|---|---|
booking_url, listing | Always null (published shape, not an error) — MakeMyTrip has no equivalent to Google's Best/Other split or a direct booking link. |
booking_token | Carries MakeMyTrip's own rkey instead of a Google token — this is what fare families and review expand. |
segments[].aircraft, origin_airport, destination_airport, duration_minutes | Always null — not published on this source's listing page. |
segments[].arrival_time | null 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[].airline | One 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_key | Present on every row. Identifies the partner journey of a paired round-trip fare. |
price | Per adult, as listed. With several passengers the review total is higher than the listed price multiplied by one adult. |
price_lock | Always present as a key; filled in only when include_price_lock was set — see Price lock. |
fare_is_pair | true 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.
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.
| Field | Meaning |
|---|---|
flights[].slashed_price | The 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[].discount | slashed_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_count | Entries in coupons[]; itineraries carrying an offer; itineraries carrying a struck-through price. All three count flights + return_flights + nearby_airport_options together. |
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.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.
/api/v1/flight-scraper/batch//api/v1/flight-scraper/batch/results/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.
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.
| Field | Meaning |
|---|---|
flights[].price_lock | Always present as a key; null when not requested, or not advertised on that card. |
price_lock.text | MakeMyTrip's own wording, with its display markup stripped. |
price_lock.amount | The premium, parsed out of text. null when no figure was stated — never 0. |
price_lock.lockable | MakeMyTrip's own eligibility tag for the card, passed through unchanged. |
price_lock.detail_api_path | MakeMyTrip's own upstream path for the tiered lock options, passed through unchanged. |
search_context.price_lock_requested | Whether include_price_lock was set. |
search_context.price_lock_count | How many cards came back carrying one. |
price_lock comes back null, and the reason appears in search_context.warnings as price lock unavailable: ….price_lock_requested | price_lock_count | Means |
|---|---|---|
false | 0 | You didn't ask for it. |
true | 0 | You asked; MakeMyTrip advertises no lock on this route. |
true | > 0 | Cards 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_combo | Shape | Measured on |
|---|---|---|
false | Two independently priced legs, summed. Below. | Domestic routes — every one checked. |
true | One 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). |
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.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:
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:
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 trips | One job, both legs | One job, both legs — still one request |
include_price_lock | Supported — see Price lock | Rejected (400) |
/scrape/fares/, /scrape/review/, /scrape/farelock/, /scrape/checkout/ | Supported | Rejected (400) against a desktop parent job |
| Cabins | All four | economy, premium_economy, business; first only where the route sells it |
currency | Honoured | Honoured, but the client identifies as India, so answers are INR in practice |
region | Honoured | No 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.
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.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.
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.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).
/api/v1/flight-scraper/scrape/fares/fareis per adult.journeyIndexis0for the outbound leg and1for the return, so one call returns both directions' families on a round trip.slashedFareis the struck-through price on that family, when MakeMyTrip advertises one — a display string (e.g."₹ 6,593"), not a number, sent only when present. Unlike the L1 listing, this is populated on domestic routes too, so it can answer "what's the saving on this fare" where the listing's owndiscountreadsnull.- The
rkeyis reissued:result.search_context.rkeyin the response is a different key from the one the parent job produced — anything continuing the funnel must carry the reissued key, not the original. rkeys expire: a stale one returns no fare families and fails aserror_category: "fare_unavailable"— deliberately nottransient, since retrying can't help; re-run the search instead. Expand soon after searching.- A domestic round trip requires
return_flight_index— there's no server-side round-trip key, so the two legs' keys are joined and re-priced as a pair. Omitting it on a domestic round-trip parent is a400. An international combo parent (is_combo: true) needs noreturn_flight_indexat all — the chosen card already names its return leg. See Round trips. - Guards shared by every expansion (
/scrape/fares/,/scrape/review/,/scrape/farelock/,/scrape/checkout/):403without themakemytripscope;404ifjob_idisn't yours;400if the parent isn't a MakeMyTrip job, ran onplatform: "desktop", isn'tcompleted, the chosen flight has nobooking_token, or the chosen row is from a sequential-layout round trip (no named return flight; re-run the search and expand a row from the new result). An out-of-range index is400 {"flight_index": "index N out of range (M in `flights`)."}(return_flight_indexchecks againstreturn_flights). The expansion repeats the parent's passengers, cabin, region and currency, so there is nothing to resend.
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.
| Field | Meaning |
|---|---|
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_count | Length 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.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.
/api/v1/flight-scraper/scrape/review/| Field | Meaning |
|---|---|
review.itineraryId | MakeMyTrip's handle for this priced itinerary. |
review.totalAmount | The price, as a number. |
review.baseFare · review.tax | The 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.retrievedAt | When the page was fetched. |
search_context.itinerary_id / total_amount / base_fare / tax | Mirrors the same fields on review, at the top level for quick access. |
search_context.coupons_advertised | Count of review.coupons[]. |
search_context.coupons_priced | false by default. true only when price_coupons: true was sent and the tray was not empty — see Coupon pricing. |
search_context.coupons_shortlisted | Present 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_priced | Ranked out before pricing by their advertised value — not measured and rejected. |
search_context.coupons_disabled | Greyed out by MakeMyTrip (enabled: false), so never a candidate. coupons_shortlisted + coupons_not_priced + coupons_disabled == coupons_advertised. |
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./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.
| Field | Meaning |
|---|---|
coupons[].total | Net payable with this coupon. |
coupons[].saving | baselineTotal - total — what you actually pay less. Can be negative. |
coupons[].discount | What the "Discounts" breakup line credits — not always the same number as saving, see below. |
coupons[].cartChanged | The coupon attached a product (e.g. Trip Secure) rather than only discounting. |
coupons[].advertisedAmount | What the tray claimed this coupon was worth, before it was priced. |
coupons[].measured | false = 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[].applied | false = MakeMyTrip refused this coupon for this fare — a real answer, not an error. null = never asked, see measured. |
coupons[].failed | true = the apply never got an answer (timeout, dead exit) — unjudged, not rejected; note says why. |
pricedCount / trustedCount | How 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 / bestDiscount | The coupon with the lowest resulting total among those MakeMyTrip did not refuse — including any taken on trust. |
restored | Whether 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.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.
Measured live on DEL→DXB (29 Oct 2026), one journey, only fare_index changing:
fare_index | review.trips[].fareFamily | Total |
|---|---|---|
0 (or omitted) | Economy Super Saver | ₹21,400 |
1 | Economy Comfort | ₹23,862 |
2 | Economy 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.
/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).- Listing prices are index 0. A row's
priceis the default fare; for any other fare only the review's total is authoritative. - One-way, MakeMyTrip only.
fare_indexis400on a round-trip parent (expand each leg as a one-way search instead) and400on/scrape/fares/.
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.
/api/v1/flight-scraper/scrape/farelock/| Field | Type | Required | Notes |
|---|---|---|---|
job_id | uuid | Required | A completed MakeMyTrip search job (app platform). |
flight_index | int | Required | ≥ 0; position in that job's result.flights. |
return_flight_index | int | Optional | ≥ 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. |
| Field | Meaning |
|---|---|
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.cheapestPrice | The lowest price in options; null when there are none. |
farelock.retrievedAt | When the tiers were fetched (ISO 8601). |
search_context.tier_count | Length of options. |
search_context.cheapest_lock_price | Mirrors farelock.cheapestPrice. |
search_context.no_results | true when the itinerary advertised no tiers. That is a successful, empty answer — not a failure. |
search_context.applied_currency | The job's currency (INR when you sent none). |
search_metadata.leg, flight_index, return_flight_index | leg is "farelock"; the indices echo what you sent. |
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.
/api/v1/flight-scraper/scrape/checkout/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:
| Field | Meaning |
|---|---|
include_coupon_breakups | Request field, bool, default false. See below. |
checkout.checkoutId | MakeMyTrip's handle for the held itinerary — this is the write. |
checkout.bookingAmount / payableAmount | The fare total, before the convenience fee. |
payment.totalDue | The real final amount, convenience fee included. |
payment.convenienceFee | The figure this endpoint exists to reach — 0 everywhere else in the funnel. |
search_context.review_total / total_due | Mirror 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_checkout | Always true on success — states plainly that this job left a real checkout behind. |
search_context.traveller_fields_required | Which 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.rate | Present 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. |
panSubmitted | Alongside 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.panGateCleared | The 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. |
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.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.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.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.
| Field | Meaning |
|---|---|
coupon_breakups.baseline | The 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). |
trayCount | Coupons the review advertised. |
shortlistedCount | How many were attempted — the best candidates by advertisedAmount (a shortlist of 4 by default, floored at 3). |
notPricedCount | Ranked out before pricing by their advertised value — not measured and rejected. |
disabledCount | Greyed out by MakeMyTrip, so never a candidate. shortlistedCount + notPricedCount + disabledCount == trayCount. |
trustedCount | Of 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. |
pricedCount | How many were actually sent to MakeMyTrip and answered. |
failedCount | Of the shortlist, how many never answered. |
returnedCount | How many made the top-3 cut. |
skippedCount | How 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). |
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.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.
| Failure | error_category |
|---|---|
| Checkout switch off | error — nothing written |
| Itinerary's traveller form demands a field this endpoint doesn't generate | error, 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 cleared | pan_required — pick a different itinerary, not a retry; the message and panSubmitted say why and whether a PAN already went out — see below |
Stale rkey | fare_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 search | 400 at submit, before any job is created |
| Edge refusal / rate limiting | blocked / transient — as elsewhere on this source |
| Origin refuses the request itself | invalid_search — not retried, see Errors |
| Job ran too long and was stopped server-side | transient, error_message names the time limit — see the warning below |
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.
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.Next: Google Hotels →