DEV Community

Matvey
Matvey

Posted on

25 Rows Covered Two Flights: One Momondo Itinerary Was Sold by 43 Sellers

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

$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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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)