# Kayak Multi-OTA Fare Comparison Scraper (`apt_marble/kayak-multi-ota-fare-comparison-scraper`) Actor

Every seller's price for the same flight, side by side. One row per flight with up to 40 agency and airline prices, carry-on and checked-bag fees, fare types and a direct booking link. Pick a route, a date and a market, and see who sells that exact seat cheapest.

- **URL**: https://apify.com/apt\_marble/kayak-multi-ota-fare-comparison-scraper.md
- **Developed by:** [Hamza](https://apify.com/apt_marble) (community)
- **Categories:** Travel
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 flight with seller prices founds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Kayak Multi-OTA Fare Comparison Scraper

Most flight scrapers give you one price per flight. This one gives you **every
price per flight** — the same seat, the same aircraft, the same departure time,
quoted by every online travel agency and airline selling it, side by side on one
row, with the carry-on and checked-bag fees that decide the real all-in cost.
On a single New York to Los Angeles flight that is eight sellers between $176 and
$181; on London to New York it is twenty. If you price flights for a living — an
agency, an airline revenue team, a metasearch product, a corporate travel desk —
this is the row you have never been able to buy off the shelf.

***

### What you can do with it

- **See who undercuts you, and by how much.** One row per flight tells you the
  cheapest seller, the typical seller, the dearest, and the gap between them —
  measured only across sellers quoting the same cabin, so a first-class fare
  never masquerades as a competitor's economy price.
- **Compare the real all-in price, not the teaser.** Carry-on status, first and
  second checked-bag fees and the fare types each seller offers come attached, so
  a $176 fare that costs $67 for a bag can be ranked against a $199 fare that
  includes one.
- **Watch a route across markets.** The same seat is often sold by eleven
  agencies in one country's storefront and two in another, at genuinely different
  prices. Choose the market and the fares come back the way travellers there see
  them.
- **Build fare and repricing models.** Airline, flight number, aircraft type,
  departure and arrival times, journey length, stops and every layover airport
  are on the row, so seller pricing can be joined to schedule and equipment.
- **Track a route over time.** Schedule a daily run on the routes you care about
  and keep the history: which seller was cheapest, how wide the field was, when a
  seller entered or left.
- **Feed a booking or comparison product.** Every seller price carries a direct
  link straight to that seller's offer for that exact flight.

***

### What you get

One record per flight. Abridged — a real record from a New York to Los Angeles
search, trimmed to fit here:

```
{
  "origin": "JFK",
  "destination": "LAX",
  "departureDate": "2026-12-08",
  "tripType": "one_way",
  "cabinClass": "economy",

  "price": 176,
  "currency": "USD",
  "cheapestProviderName": "Cheapflightsfares",
  "cheapestFareCabin": "economy",
  "medianSellerPrice": 179,
  "highestSellerPrice": 181,
  "priceSpreadAbs": 5,
  "priceSpreadPct": 2.8,
  "sellerCount": 8,
  "comparableSellerCount": 8,
  "offerCount": 22,

  "carryOnBagStatus": "included",
  "carryOnBagFee": 0,
  "checkedBagStatus": "fee",
  "checkedBag1Fee": 67,
  "checkedBag2Fee": 149,
  "priceWithFirstCheckedBag": 243,
  "isFreeCancellation": true,

  "airlineNames": ["JetBlue"],
  "flightNumbers": ["B6701", "B62501"],
  "aircraftTypes": ["Airbus A220-300", "Airbus A318/A319/A320/A321"],
  "departureAirport": "JFK",
  "arrivalAirport": "LAX",
  "departureLocal": "2026-12-08T13:45:00",
  "arrivalLocal": "2026-12-08T20:52:00",
  "totalDurationMin": 607,
  "stops": 1,
  "layoverAirports": ["FLL"],
  "layoverDurationsMin": [66],

  "sellerPrices": [
    { "provider": "CHEAPFLIGHTSFARES", "providerName": "Cheapflightsfares",
      "price": 176, "currency": "USD", "cabin": "economy",
      "checkedBag1Fee": 67, "isFreeCancellation": true, "bookingUrl": "https://www.kayak.com/…" },
    { "provider": "EXPEDIAAIR", "providerName": "Expedia",
      "price": 178, "currency": "USD", "cabin": "economy",
      "checkedBag1Fee": null, "isFreeCancellation": true, "bookingUrl": "https://www.kayak.com/…" },
    { "provider": "CHEAPOAIR", "providerName": "CheapOair",
      "price": 178, "currency": "USD", "cabin": "economy",
      "checkedBag1Fee": 49, "isFreeCancellation": true, "bookingUrl": "https://www.kayak.com/…" }
  ],

  "bagAllowances": [
    { "airline": "B6", "bagType": "personal", "dimensions": "Up to 16 x 12 x 7in", "weight": null },
    { "airline": "B6", "bagType": "carryon",  "dimensions": "Up to 21 x 13 x 8in", "weight": null }
  ],

  "fareFamilies": [
    { "fareName": "Main Base", "price": 176, "carryOnBag": "included", "checkedBag": "fee",
      "checkedBag1Fee": 67, "seatSelection": "fee", "changes": "fee", "refundable": "not_available" },
    { "fareName": "Main", "price": 215, "carryOnBag": "included", "checkedBag": "fee",
      "seatSelection": "included", "changes": "included", "refundable": "not_available" }
  ],

  "market": "US",
  "collectedAt": "2026-08-16T18:42:11.204Z",
  "bookingUrl": "https://www.kayak.com/…"
}
```

`sellerPrices` is the whole point: one entry per published offer, cheapest first,
each with its own price, cabin, bag fee, free-cancellation flag and direct link.
A busy route returns up to forty of them on a single flight.

***

### Input reference

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `origin` | text | — | Where the trip starts. An airport code (`JFK`), a city-wide code covering every airport in the city (`NYC`, `LON`, `PAR`), a city name, or a comma-separated list of airports (`JFK,LGA,EWR`). |
| `destination` | text | — | Where the trip ends. Same formats as `origin`. |
| `departureDate` | date | — | Outbound date, `YYYY-MM-DD`. Today or later, up to about a year ahead. |
| `returnDate` | date | empty | Return date, `YYYY-MM-DD`. Leave empty for one way. |
| `extraSearches` | list | empty | Run several routes and dates in one go — each entry takes `origin`, `destination`, `departureDate` and optionally `returnDate`. Up to 30 searches per run. |
| `market` | choice | `US` | Which country's storefront to price against: United States, United Kingdom, Germany, France or Spain. Decides the currency **and** which sellers appear. |
| `cabinClass` | choice | `economy` | `economy`, `premiumEconomy`, `business` or `first`. |
| `adults` | number | `1` | Adult travellers, 1 to 8. |
| `childAges` | list | empty | One entry per child, giving each child's age in years (0–17). Up to 6. |
| `stopsFilter` | choice | `any` | `any`, `nonstop`, or `upToOneStop`. |
| `sortBy` | choice | `best` | `best`, `cheapest`, `fastest`, `earliestDeparture` or `lowestEmissions`. Decides which flights you keep when you cap the result count. |
| `maxResultsPerSearch` | number | `50` | Flights returned per route and date, 1 to 200. |
| `maxSellersPerFlight` | number | `40` | Seller prices kept on each flight, cheapest first, 1 to 40. |
| `searchesInParallel` | number | `2` | How many of your searches run at once, 1 to 4. |

You do not need an account anywhere, and you never supply a login, a password or
anything private. Fill in a route and a date and press start.

***

### Output fields

| Field | What it is |
| --- | --- |
| `origin`, `destination`, `departureDate`, `returnDate`, `tripType`, `cabinClass`, `adults`, `childAges` | The search this row answers. |
| `price`, `currency` | The cheapest published price for this exact flight, in the currency the market actually quoted. |
| `cheapestProvider`, `cheapestProviderName`, `cheapestProviderCountry` | Who is selling it at that price. |
| `cheapestFareCabin` | The cabin that cheapest offer is actually selling. |
| `medianSellerPrice` | The middle of the field — what this seat normally sells for. |
| `highestSellerPrice`, `priceSpreadAbs`, `priceSpreadPct` | The dearest comparable seller and the gap to the cheapest, in money and as a percentage. |
| `sellerCount` | How many different sellers published a price for this flight. |
| `comparableSellerCount` | How many of them quoted the same cabin as the cheapest — the number the gap is measured across. |
| `offerCount`, `distinctFareCount` | Total offers on the flight, and how many distinct fare types. |
| `sellerPrices[]` | Every offer: `provider`, `providerName`, `sellerCountry`, `price`, `currency`, `cabin`, `carryOnBagFee`, `checkedBag1Fee`, `checkedBag2Fee`, `priceWithCarryOn`, `priceWithFirstCheckedBag`, `isFreeCancellation`, `isSplitBooking`, `bookingUrl`. |
| `carryOnBagStatus`, `carryOnBagFee` | Whether a carry-on is included on the cheapest offer, and what it costs if not. |
| `checkedBagStatus`, `checkedBag1Fee`, `checkedBag2Fee` | First and second checked-bag fees on the cheapest offer. |
| `priceWithCarryOn`, `priceWithFirstCheckedBag` | The all-in price once the bag is added. Empty when the fee was not published — never assumed to be free. |
| `bagAllowances[]` | Published size and weight limits per carrier, by bag type. |
| `fareFamilies[]` | Each fare type on the flight with its price and what it includes: carry-on, checked bag and its fees, seat selection, legroom, changes, refundability. |
| `isFreeCancellation`, `freeCancellationSellers`, `hasSplitTicketOffer` | Cancellation terms, and whether any offer is two separate tickets rather than one. |
| `airlineCodes`, `airlineNames`, `flightNumbers`, `aircraftTypes` | Who operates it and on what. |
| `departureAirport`, `arrivalAirport`, `departureLocal`, `arrivalLocal` | Where the outbound leg starts and finishes, and the local times. |
| `outboundDurationMin`, `outboundStops`, `returnDepartureLocal`, `returnArrivalLocal`, `returnDurationMin`, `returnStops` | The same per direction on a return trip; empty on a one-way. |
| `totalDurationMin`, `stops`, `layoverAirports`, `layoverDurationsMin` | Journey length across all legs, the most stops on any leg, and every layover with its length in minutes. |
| `legs[]` | Full per-leg detail, down to each individual flight: airline, flight number, airports, times, minutes, aircraft. |
| `bookingUrl`, `flightUrl` | A direct link to the cheapest offer, and to the flight itself. |
| `market`, `source`, `collectedAt`, `itineraryId`, `searchKey` | Which market this was priced in, when it was collected, and stable identifiers for joining runs together. |
| `resultRank`, `isBestValue`, `isCheapestOnRoute`, `isCheapestNonstop` | How the source itself ranked this flight against the others in the same search. |
| `isCachedSnapshot` | `true` when the search was answered from recently stored results rather than searched fresh. Worth knowing before you treat a price as this minute's. |

Alongside the table, every run writes a summary recording how many searches ran,
how many returned flights, how many genuinely had none, and how many could not be
completed — kept strictly apart, so "nothing found" and "could not be read" are
never confused for one another.

It also carries one line per route and date, with `cheapestPrice`, `currency`,
`cheapestSeller` and `sellersOnCheapest` for that search. Those are **measured
across the rows that search actually delivered**, not read off the top of the
table, so they mean the same thing whichever `sortBy` you chose — handy when you
run thirty routes and want the best fare on each without opening the dataset.

***

### Pricing

You pay for what you get, with no monthly fee.

| What is charged | Price |
| --- | --- |
| Each flight returned, with all its seller prices | **$4.00 per 1,000** |
| Each route-and-date searched | **$0.030 each** |

A flight row is priced above an ordinary marketplace row because it is not one
price — it is up to forty of them for the same seat, plus a normalised baggage
matrix and the fare types behind each one. The per-search charge exists because a
search is real work whatever it returns: one route on one date is taken all the
way to completion before anything is returned, and a quiet route that yields six
flights costs the same effort as a busy one that yields two hundred.

**Worked example.** Five routes, one date each, 50 flights per search:

- 250 flights returned → 250 × $0.004 = **$1.00**
- 5 route-and-date searches → 5 × $0.030 = **$0.15**
- **Total: $1.15** — for roughly two thousand individual seller prices.

A search on a route that genuinely has no flights on that date still counts as a
route-and-date searched, because the work was done and the answer is a real one.
A search that could not be completed is **not** charged.

***

### Limits & what this actor cannot do

- **Fares are a snapshot.** Airline pricing changes continuously and a fare seen
  now may be gone minutes later. Treat every row as true at `collectedAt`.
- **It reports what the source publishes to the public.** It does not hold seats,
  does not book, and cannot guarantee a fare is still available when you go to
  buy it.
- **A refused search is reported as a failed search, never as "no flights
  found".** If the source declines to answer, you get an explicit failure in the
  run summary and no fabricated empty result. This matters more than it sounds:
  an empty table that should have been an error is the one outcome you cannot
  detect after the fact.
- **Paid placements are dropped, not published.** The source repeats its top
  flight as an advertisement carrying a shortened seller list and a promotional
  link. That copy is discarded and the flight's own row is kept instead, so the
  seller count, the price gap and the booking link on every row describe the real
  market for that seat rather than one agency's placement.
- **Nothing is estimated.** A fee the source did not publish comes back empty,
  not as zero, and an all-in price is only produced where the fee was actually
  published. No price is ever carried across dates or filled in.
- **Fares and sellers differ by market.** The same seat can be sold by eleven
  agencies in one market and two in another at genuinely different prices. Those
  are different inventories, not a currency conversion — never convert between
  them, and compare only within one market.
- **A wide price gap usually means different fare rules, not a bargain.** The gap
  is measured only across sellers quoting the same cabin, but within one cabin a
  fully flexible ticket and a basic one are still both "economy". `medianSellerPrice`
  is the more stable comparison.
- **Some searches are answered from recently stored results.** Those rows are
  flagged `isCachedSnapshot`. They can carry fewer sellers and a higher price
  than a fresh search of the same route.
- **Routes are seasonal.** A real route with nothing flying on the dates you
  searched is reported as having none — that is an answer, not an error.
- **Multi-city trips are not offered.** One way and return only.
- **The source publishes a limited number of results for any one search**, so a
  very broad search returns what it makes available rather than an unlimited
  list.
- **Speed depends on the size of the job** and on how quickly the source itself
  answers. No fixed throughput is promised; a return trip takes appreciably
  longer than a one-way.
- **The source's terms govern automated access.** You are responsible for using
  the data lawfully and in line with those terms, and for handling any personal
  data in line with applicable privacy law.

***

### FAQ

**Do I need an account on Kayak?**
No. Nothing is signed into and nothing is registered.

**Does it need my login or password?**
No. The only things you provide are a route, a date and a few preferences.

**Can I schedule it?**
Yes. Schedule it daily or twice daily on the routes you care about and keep the
history — that is where the value compounds, because you can see which seller
moved and when. Anything tighter than that is not worth paying for: prices on
this kind of route do not move minute to minute.

**Are these fares bookable?**
They are the prices published at the moment of collection, and every one carries
a direct link to that seller's offer. They are not held, not reserved, and not
guaranteed — airline pricing moves continuously, and a fare can be gone before
you reach the checkout.

**Why does one flight have eight sellers and another only one?**
Because that is what the market publishes. Popular routes attract many agencies;
a thin route or a premium cabin may only be sold by the airline itself. The row
tells you the truth either way, and `sellerCount` is never padded.

**Why is one seller $2,000 dearer than another for the same flight?**
Almost always because they are selling a different fare type — a fully flexible
or refundable ticket rather than a basic one. `cheapestFareCabin` and each
offer's own `cabin` let you tell them apart, and `medianSellerPrice` gives you the
typical price without the outlier.

**What happens if a search cannot be completed?**
It is recorded as a failed search in the run summary, with the reason, and it is
not charged. It is never quietly turned into an empty result.

**Can I get more than 200 flights on one route and date?**
No. The source publishes a bounded set of results for any single search. If you
need broader coverage, split the job across more dates or more airports rather
than expecting one search to return everything.

# Actor input Schema

## `origin` (type: `string`):

Where the trip starts. Use a three-letter airport code (JFK), a city-wide code that covers every airport in the city (NYC, LON, PAR), or a city name. You can also list several airports separated by commas, for example JFK,LGA,EWR.

## `destination` (type: `string`):

Where the trip ends. Same formats as From: an airport code, a city-wide code, a city name, or a comma-separated list of airports.

## `departureDate` (type: `string`):

The outbound date, written as YYYY-MM-DD. It must be today or later, and no more than about a year ahead — fares are not published beyond that.

## `returnDate` (type: `string`):

The date you come back, written as YYYY-MM-DD. Leave this empty for a one-way search. A return search takes longer and is priced as one round trip rather than two separate flights.

## `extraSearches` (type: `array`):

Run several searches in one go. Each entry takes the same four fields as above: origin, destination, departureDate and, optionally, returnDate. Cabin, travellers, market and sorting are shared by every search in the run. Up to 30 searches per run.

## `market` (type: `string`):

Which country's storefront to price against. This decides the currency AND which sellers appear — the same seat is often sold by eleven agencies in one market and by two in another, at genuinely different prices. Every row carries the currency that actually came back.

## `cabinClass` (type: `string`):

Which cabin to search. Sellers often list several fare types within the same cabin, and every one of them is returned.

## `adults` (type: `integer`):

How many adult travellers to price for. Prices are quoted the way the source quotes them for that party size.

## `childAges` (type: `array`):

Add one entry per child travelling, giving each child's age in years (0–17). Leave empty if no children are travelling. Up to 6 children.

## `stopsFilter` (type: `string`):

Limit the search to non-stop flights, to flights with at most one stop, or leave it open to any number of stops.

## `sortBy` (type: `string`):

Which flights you want first. This decides which flights you get when you cap the number of results — sorting by cheapest and capping at 20 gives you the 20 cheapest flights.

## `maxResultsPerSearch` (type: `integer`):

How many flights to return for each route and date. Every flight returned carries its full list of sellers, so 50 flights can be several hundred prices.

## `maxSellersPerFlight` (type: `integer`):

How many seller prices to keep on each flight, cheapest first. Leave at the maximum to get every seller the source publishes.

## `searchesInParallel` (type: `integer`):

How many of your searches to run at once. Two is the sweet spot: higher is not faster, because each search has to be taken to completion before its results are final.

## Actor input object example

```json
{
  "origin": "NYC",
  "destination": "LAX",
  "departureDate": "2026-12-08",
  "extraSearches": [
    {
      "origin": "SFO",
      "destination": "JFK",
      "departureDate": "2026-12-10"
    },
    {
      "origin": "LHR",
      "destination": "DXB",
      "departureDate": "2026-12-12",
      "returnDate": "2026-12-19"
    }
  ],
  "market": "US",
  "cabinClass": "economy",
  "adults": 1,
  "childAges": [
    "4",
    "9"
  ],
  "stopsFilter": "any",
  "sortBy": "best",
  "maxResultsPerSearch": 50,
  "maxSellersPerFlight": 40,
  "searchesInParallel": 2
}
```

# Actor output Schema

## `flights` (type: `string`):

One row per flight, carrying every seller price found for that exact seat.

## `flightsCsv` (type: `string`):

The same rows as a CSV file.

## `runSummary` (type: `string`):

How many searches ran, how many returned flights, how many genuinely had none, and how many could not be read.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "origin": "JFK",
    "destination": "LAX",
    "departureDate": "2026-12-08",
    "extraSearches": [],
    "childAges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("apt_marble/kayak-multi-ota-fare-comparison-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "origin": "JFK",
    "destination": "LAX",
    "departureDate": "2026-12-08",
    "extraSearches": [],
    "childAges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("apt_marble/kayak-multi-ota-fare-comparison-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "origin": "JFK",
  "destination": "LAX",
  "departureDate": "2026-12-08",
  "extraSearches": [],
  "childAges": []
}' |
apify call apt_marble/kayak-multi-ota-fare-comparison-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,apt_marble/kayak-multi-ota-fare-comparison-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ANTMXW2heC8LM0t1L/builds/BsHfZn0KJIUMUTDLb/openapi.json
