I run a visa agency in Bali, and the flights half of my week goes into Apify Actors that read travel sites over HTTP. Momondo is the odd one in that set. Google Flights answers what does this itinerary cost; Momondo, being a metasearch engine, answers which of a dozen agencies — or the airline itself — is selling this exact itinerary cheapest right now, with the link to that price.
That extra dimension is also what quietly breaks your row budget, and it took me a while to see why.
The default shape: one row per flight, plus a count
Here is the input I use most. Bali to Singapore, a week in December, economy, 25 rows:
{
"origin": "DPS", "destination": "SIN",
"departureDate": "2026-12-10", "returnDate": "2026-12-17",
"cabinClass": "economy", "sortBy": "best",
"maxResults": 25, "allProviders": false
}
25 rows, 25 distinct itineraries, one row each. Four seller names across the whole set — Booking.com on 18 rows, Mytrip and Kiwi.com on three each, KLM on one — and a column that hints at what you are not seeing:
providersForThisFlight on the 25 rows:
6 sellers ×9 rows
7 sellers ×3
12 sellers ×2
18 sellers ×8
19 sellers ×2
1 seller ×1
So each row is a summary: the flight exists at 6 to 19 different prices, the row shows one, and providersForThisFlight says how much of the market you are looking past. Summed, those 25 rows stand in for 299 seller offers.
Flip one boolean and 25 rows become 2 flights
allProviders: true is documented as one row per booking provider instead of one row per itinerary. I expected a wider table and got a narrower one. Same dates, same maxResults: 25, nothing else touched — JFK to London this time, because the transatlantic market has more agencies in it:
allProviders: false → 25 rows, 25 itineraries, 7 seller names
allProviders: true → 25 rows, 2 itineraries, 20 seller names
The row budget is a budget of rows, not of flights, and with the flag on a single popular itinerary will eat all of it. Here is where my 25 rows went. British Airways BA174/BA173, nonstop both ways, providersForThisFlight: 43:
JustAirTicket 820 Booking.com 893
Cheapflightsfares 849 KAYAK 907
Justfly.com 871 Gotogate 914
FlightHub 871 Super.com 916
Travelocity 875 Mytrip 923
British Airways 875 Ovago 943
Expedia 875 Gurufare 944
Orbitz 875 BusinessClass 945
Priceline 875 Kissandfly.com 970
CheapOair 876
22 rows for one flight, against a claim of 43 sellers — the other 21 were cut by my own maxResults. The remaining three rows went to a Virgin Atlantic itinerary, and that was the whole run: two flights, $0.0175.
Two things in that table pay for the run on their own. Five sellers are tied at exactly $875 — Travelocity, British Airways, Expedia, Orbitz, Priceline — which is what an airline-set fare looks like from outside. And the airline's own price is not the cheapest: JustAirTicket sits $55 below British Airways on British Airways metal.
The summary row is not always the cheapest seller
That same itinerary appeared in the default run too, one row, and this is what it said:
default mode (one row per flight): Cheapflightsfares $849 providersForThisFlight: 15
expanded mode (one row per seller): JustAirTicket $820 22 rows
$29 between "the price of this flight" and "the cheapest price of this flight". On the Bali route it went the other way: three itineraries I expanded all had their default row sitting on the genuine minimum ($154, $154, $156). So the summary row is usually the cheapest offer and sometimes is not, and nothing in the row says which case you are in — expand the itineraries you shortlist rather than quoting the first pass.
The budget math is boring but worth writing down: with the flag on, set maxResults to flights wanted × sellers per flight. On JFK–LHR that is 25 × 43 ≈ 1,075 rows, $0.75 at $0.0007 a row. Cheap, as long as you asked for it on purpose.
The seller name is not a key
I assumed one row per seller meant distinct names. On the Bali route, one KLM itinerary expanded into nine rows:
provider providerCode price
KLM KL 157
KLM KL 208
KLM KL 228
KLM KL 305
KLM KL 320
KLM KL 425
KLM KL 533
KLM KL 663
KLM KL 847
Nine rows, one name, a 5.4× price spread. Every other field — flight numbers, times, duration, cabin, aircraft — is byte-identical across the nine; only price and bookingUrl move. Virgin Atlantic did the same thing on the London route at $851, $1,051 and $1,251.
These are fare brands sold by the airline, labelled with the airline's name and kept as separate offers. Consequence: do not GROUP BY provider and expect one row. The key is resultId + bookingUrl — cabinClass will not separate them, all nine say economy.
One run in three came back flat, and it looked fine
This is the failure mode I would want to know about before building on the field. My first expanded run of the Bali route gave 25 rows across 17 itineraries — one row each, all labelled Kiwi.com, providersForThisFlight: 1 on every one. Ten minutes later the identical input produced the 4-itinerary, 13-seller table above. Same build, same proxy, SUCCEEDED both times, no error row.
The flat run's prices were quietly worse: on the itineraries the two runs share, $159–183 from Kiwi.com against $154–156 from Booking.com. Momondo loads the seller list after the itinerary list, and when it does not arrive the row shape is unchanged — you get a plausible price from whichever seller was attached first.
There is no exception to catch, so the check is statistical and one line long:
rows = [r for r in items if r.get("type") == "flight"]
flat = sum(1 for r in rows if r["providersForThisFlight"] == 1) / len(rows)
if flat > 0.8:
retry() # seller lists did not load; prices are not the market minimum
A real result set has a spread of counts. If almost every row claims one seller, you are holding a half-loaded page.
Three fields that mean something other than what they look like
Measured across 300 rows from today's five runs plus the Actor's own recent datasets:
durationMinutes on a round trip is both directions added together. In 125 of 125 round-trip rows it equals the sum of the legs — the Virgin itinerary's 1,235 minutes is 425 out plus 810 back, not a 20-hour flight. And arriveAt is the return arrival, so arriveAt − departAt is a week: all 125 rows disagreed with durationMinutes by more than an hour. Sort by the field if you like, just do not label the column "flight time".
stops is per direction, not per trip. 24 of 175 older rows carry stops: 1 with two entries in layovers — KUL on the way out, KUL on the way back. stops is the worst leg; layovers is every stop in the trip. maxStops: 1 therefore buys you one stop each way, which is two planes changed, and len(layovers) is what a traveller would call the number of stops.
price is a whole dollar because something rounded it up. The booking link carries a number that tracks the price in cents and sits a few cents below the printed figure: ...sfE.15397. on a $154 row, 15620 on $157, 17397 on $174. 94 of the 120 rows I could parse showed that gap and none went the other way. I cannot prove what that link field is, so take it as a hint rather than a fact: price is good to about a dollar, not to the cent. Fine for monitoring fare moves, not for reconciling a receipt.
Calling it
45 seconds for a route, so the synchronous endpoint works:
curl -X POST "https://api.apify.com/v2/acts/lergassy~momondo-flights-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"origin":"JFK","destination":"LHR","departureDate":"2026-12-10",
"returnDate":"2026-12-17","allProviders":true,"maxResults":200}'
routes[] takes a list of origin/destination/date objects for a sweep in one run; there is no per-search fee, so extra routes only cost the rows you keep. Each run ends with a free notice row naming the charge (25 row(s) delivered, 0 free; charged flight ×25 = $0.0175) — filter type != "notice", but log it.
The Actor is Momondo Flights Scraper. Where it is the wrong tool: if you want the price for a route rather than the seller breakdown, a single-source scraper is cheaper and steadier — the seller list is the slowest and least reliable part of the page, as the flat run shows. If you want month-wide cheapest dates, this is not a calendar; it prices the dates you name. And if you need the same comparison with a different partner mix, Kayak Flights Scraper runs the same engine on KAYAK, whose agency list overlaps but is not identical.
Three changes in my own jobs after measuring this: maxResults from flights × expected sellers, dedupe on resultId + bookingUrl instead of provider, retry when over 80% of rows claim a single seller. The last one fired on one Bali run in three today.
Top comments (0)