# Flight Deals & Mistake Fare Scraper (`apt_marble/flight-deals-mistake-fare-scraper`) Actor

Collect flight deals, flash sales and error fares from four deal publishers into one newest-first table: airline, route, price, trip type and how old the deal is — plus both airport codes, fare class, routing, stopover rules and mileage where the deal spells them out.

- **URL**: https://apify.com/apt\_marble/flight-deals-mistake-fare-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 deal 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

## Flight Deals & Mistake Fare Scraper

Error fares, flash sales and mistake fares are published in public — and then they die, usually within a day or two. This actor collects them from four deal publishers at once — **The Flight Deal**, **Fly4free**, **Travelfree** and **CheapOair** — and returns one newest-first table: both cities, the fare (both fares, when a deal quotes a basic and a regular price), the airline and the trip type where the deal states them, and **how old the deal is**, so nothing stale is ever presented as live. The Flight Deal and the on-sale fares also carry both airport codes; the two European publishers write place names in prose and no codes at all, and those rows say so by leaving the code fields empty. Where a deal spells out the small print, you also get the fare class, the full routing, the stopover rules and the mileage it earns, right down to cents per mile. The archive behind it goes back to 2011, so you can either watch today's deals or build a decade of them.

### What you can do with it

- **Run a deal-alert product.** Collect what was published since your last run, filter to the departure cities your audience flies from, and push the fresh ones out.
- **Fill an affiliate or content pipeline.** Every deal arrives structured — airline, route, price, trip type, sample travel dates, validity window — instead of as a headline you have to read.
- **Study fare behaviour over years.** Fourteen years of published deals, by route, airline, season and price, is a research corpus you cannot buy off the shelf.
- **Spot mistake fares faster.** On-sale fares carry the seller's own Green/Red deal rating and the price bands the same route usually sells in, so an outlier stands out.
- **Feed a points-and-miles tool.** Fare class, routing, miles flown, elite-qualifying and redeemable miles, and cents-per-mile come straight off the deal.
- **Track who is discounting what.** Which airlines are dumping seats, on which routes, from which cities, and how often.

### What you get

One row per deal, newest first. A real example, abridged:

```
{
  "dealId": "theflightdeal:322415",
  "source": "theflightdeal",
  "title": "Cathay Pacific: San Francisco - Kota Kinabalu, Malaysia. $843. Roundtrip, including all Taxes",
  "permalink": "https://www.theflightdeal.com/2026/08/15/cathay-pacific-san-francisco-kota-kinabalu-malaysia-843-roundtrip-including-all-taxes/",
  "publishedAt": "2026-08-15T18:33:02Z",
  "ageDays": 0,
  "isLikelyExpired": false,
  "dealType": "flight",
  "airlines": ["Cathay Pacific"],
  "originCity": "San Francisco",
  "originIata": "SFO",
  "destinationCity": "Kota Kinabalu",
  "destinationIata": "BKI",
  "destinationCountry": "Malaysia",
  "priceLowest": 843,
  "priceBasic": null,
  "priceRegular": null,
  "currency": "USD",
  "currencySource": "symbol",
  "tripType": "roundtrip",
  "isBidirectional": null,
  "fareIncludesTaxes": true,
  "sampleTravelDates": "October 14th - 21st",
  "fareValidity": "Valid for travel until late October for Monday through Thursday departures and returns. Availability is limited. Must purchase at least 7 days in advance of departure.",
  "fareClass": "Q",
  "routing": "SFO - HKG (Hong Kong) - BKI (Kota Kinabalu) - HKG - SFO",
  "routingLegs": ["SFO", "HKG", "BKI", "HKG", "SFO"],
  "stopoverRules": "Two permitted at $100",
  "milesFlown": 16100,
  "eliteQualifyingMiles": 3464,
  "redeemableMiles": 3464,
  "centsPerMile": [{ "value": 5.2, "label": null }],
  "centsPerMileLowest": 5.2,
  "categories": ["San Francisco"],
  "alsoSeenIn": [],
  "dealDetailsIncluded": true,
  "isEstimated": false,
  "collectedAt": "2026-08-16T12:00:00.000Z"
}
```

An on-sale fare looks the same, and adds what that seller publishes: `dealBucket` (`Green` / `Red`), `priceQuantiles`, `flightNumbers`, `departureDate`, `numberOfStops` and `priceAdvertised`.

### Input reference

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `sources` | array | all four | Which publishers to collect from: `theflightdeal`, `fly4free`, `travelfree`, `cheapoair`. |
| `originCities` | array | — | Keep only deals leaving from these places: airport codes (`JFK`), city-wide codes (`NYC`), or names (`San Francisco`). Empty means everywhere. |
| `keyword` | string | — | Keep only deals whose text mentions this word — an airline, a country, a city, or a theme such as "business class". |
| `tripType` | string | `any` | `any`, `oneway` or `roundtrip`. Deals whose trip type was never stated are left out when you pick one. |
| `currency` | string | `USD` | Currency for the on-sale fares: `USD` or `GBP`. Deal write-ups keep whatever currency their author used. |
| `since` | string | — | Only deals published after this date (`YYYY-MM-DD`). Ideal for scheduled runs that should pick up only what is new. |
| `maxAgeDays` | integer | — | Drop anything older than this many days (0–3,650). Empty keeps every age. |
| `includeBackfill` | boolean | `false` | Work back through the historical archive as well as the latest deals. |
| `maxDeals` | integer | `200` | Most rows this run will produce, across every source, after filtering and merging (1–25,000). |
| `includeDealDetails` | boolean | `true` | Read the deal write-up as well as its headline. On by default. It is where the fare class, routing, stopover rules, mileage earning, flight numbers, price bands **and both airport codes** live, so turning it off leaves archive rows with empty code, routing and mileage fields and no publisher tags — each such row says `deal-detail-not-read` in `dataNotes`. |
| `onlyFlightDeals` | boolean | `true` | Leave out hotel and package posts, which some publishers mix into the same stream. |
| `requirePrice` | boolean | `true` | Leave out posts with no published fare, such as roundups and announcements. |
| `excludeLikelyExpired` | boolean | `false` | Leave out deals old enough to have probably died. |
| `likelyExpiredAfterDays` | integer | `2` | How old a deal must be to be marked as probably gone (1–30). |

### Output fields

| Field | Type | What it is |
| --- | --- | --- |
| `dealId` / `source` / `permalink` | string | A stable identity, which publisher it came from, and a link to the deal itself. |
| `title` / `summary` | string | The deal as published, and its opening lines. |
| `publishedAt` / `ageDays` | string / number | When it was published, in UTC, and how many whole days ago that was. |
| `isLikelyExpired` / `isMarkedGone` | boolean | null | Whether the deal is old enough to have died, and whether the publisher itself marked it dead. `null` means the age is unknown — never a guessed `false`. |
| `airlines` / `airlineCodes` | array | The airlines on the deal, in the order the deal names them. A code-share written as "Air China / China Eastern / Shanghai Airlines" is returned as three. Empty when the publisher never said who flies it, which is most of the time on the two European feeds. |
| `originCity` / `originIata` / `originCityCode` / `originText` | string | null | Where it leaves from: the city, the airport code where one was published, the city-wide code, and the text exactly as written. The two European publishers publish no airport codes at all, so those rows carry the text and leave the codes empty. |
| `destinationCity` / `destinationIata` / `destinationRegion` / `destinationCountry` | string | null | Where it goes. American deals are written "City, State", so the state goes to `destinationRegion` and the country reads `United States` with a note on the row saying it came from the state. Where a state name is also a country's — Georgia is the only one — the country is left empty rather than guessed, and the row says which. |
| `priceLowest` | number | null | The cheapest fare the deal quotes. `null` when no fare was published — never a guess. |
| `priceBasic` / `priceRegular` | number | null | Both fares when a deal quotes a basic and a regular price. Which is which comes from the label, never from the order they appear in. |
| `priceAdvertised` | number | null | The headline figure a seller prints beside its own total. It sits below the total and is kept separate for exactly that reason. |
| `currency` / `currencySource` | string | null | The currency the fare was published in, and whether that came from a stated field or from the symbol printed in the text. Never the currency you asked for. Where one deal is priced twice for two markets — "from £348/€439" — the fare and its currency are taken from the same quote, the first one, and the row notes that a second currency was also quoted. |
| `requestedCurrency` | string | null | What was asked of the on-sale list, so you can see when the two differ. |
| `tripType` / `isBidirectional` / `cabin` | string / boolean | null | Return or one-way, whether the deal works in both directions, and the cabin where stated. |
| `fareIncludesTaxes` | boolean | null | Whether the published price includes taxes. |
| `departureDate` / `returnDate` | string | null | Real dates, where the deal is tied to specific ones. |
| `sampleTravelDates` / `fareValidity` | string | null | The example dates and the validity window as the publisher wrote them. |
| `fareClass` / `fareClasses` | string / array | The booking class the deal is filed in. |
| `routing` / `routingLegs` / `routingCodes` | string / array | The full leg chain, as text and as a list of airports. |
| `stopoverRules` | string | null | Whether a stopover is allowed and what it costs. |
| `milesFlown` / `eliteQualifyingMiles` / `redeemableMiles` / `qualifyingDollars` | number | null | The mileage the trip earns, as the deal states it. |
| `centsPerMile` / `centsPerMileLowest` | array / number | null | Value per mile, per fare product, and the lowest of them. |
| `dealBucket` | string | null | The seller's own rating of an on-sale fare: `Green` or `Red`. |
| `priceQuantiles` | object | null | The price bands the same route usually sells in, as published. |
| `priceBaseline` | object | null | The seller's historical comparison figures, labelled `isReferenceNotLiveHistory: true` — see the limits below. |
| `numberOfStops` / `flightNumbers` | number / array | Stops and the real flight numbers, where a deal identifies specific flights. |
| `dealType` / `isDeal` | string / boolean | `flight`, `hotel`, `package` or `other`, and whether the post is a fare deal at all. |
| `categories` | array | The departure-city tags the publisher filed the deal under. |
| `alsoSeenIn` | array | Other publishers whose posting of this fare was matched to this row and merged into it. Empty is the normal case — it never names the row's own publisher. |
| `cacheExpiryMinutes` | number | null | How long an on-sale list stays current before the seller refreshes it. |
| `dealDetailsIncluded` | boolean | Whether the extended detail was attached to this row. |
| `isEstimated` | boolean | `true` only where the row carries the seller's own modelled comparison figures. The fare, the dates and the routing are always as published. |
| `dataNotes` | array | Anything worth knowing about this specific row: a currency read from a symbol rather than a stated field, a deal priced in two currencies, a country read from a US state, or a write-up left unread because the extended detail was off. |
| `collectedAt` | string | When the row was collected. |

### Pricing

You pay per result, not per run.

| What you pay for | Price |
| --- | --- |
| Deal found | **$4.00 per 1,000 rows** |
| Deal details added | **$1.50 per 1,000 rows** |

The first is charged for every deal delivered. The second is charged only when a row actually gained the extended detail — the fare class, routing, stopover rules, mileage earning, flight numbers or price bands. A deal whose publisher printed none of that is delivered without it and costs you nothing extra, and turning the detail off leaves only the first charge.

**Worked example.** A daily alerting run that collects 150 fresh deals, 120 of them with full detail: 150 × $0.0040 = **$0.60**, plus 120 × $0.0015 = **$0.18** — about **78 cents a day**, roughly **$23 a month**. A one-off research backfill of 5,000 archived deals with detail on all of them costs 5,000 × $0.0040 + 5,000 × $0.0015 = **$27.50**.

### Limits & what this actor cannot do

- **Deals die fast, and this actor says so rather than hiding it.** The publishers themselves warn that a fare more than two days old is usually gone. Every row carries `publishedAt`, `ageDays` and `isLikelyExpired`, and the table puts them in front of the price.
- **It reports what was published to the public.** It does not hold seats, does not book, and cannot guarantee a fare is still available — or that it ever loaded for everyone.
- **Nothing is invented.** A deal with no published price, no airport code or no travel dates comes back with those fields empty. No price is estimated, converted or carried across from another deal.
- **On-sale fares come from a list the seller refreshes on a cycle**, not from a live search run for you. Each row carries how long that list stays current, and the fare is a snapshot of it.
- **The historical comparison figures on on-sale fares are the seller's own reference records, not live price history.** They are labelled `isReferenceNotLiveHistory: true` and are years old. Never present `cheapestEver` as today's market low.
- **A seller's advertised headline figure is not the bookable price.** It runs materially below that seller's own total; both are returned, `priceLowest` is the total, and the difference is flagged on the row.
- **Currency is whatever the publisher used** — dollars, euros, pounds. Nothing is converted, and a `$` in free text is recorded as US dollars with a note saying it was read from a symbol. Where one deal is priced twice for two markets — "from £348/€439" — the fare and its currency are both taken from the first quote, never mixed, and the row notes the second currency.
- **Airport codes come from two of the four publishers.** The Flight Deal spells the codes out inside each deal, and the on-sale fares are published as codes; Fly4free and Travelfree write prose only, so **no row from those two carries an airport code** — "British Airways direct flights from London to San Francisco for £438" arrives with both cities as text and both code fields empty. Nothing is looked up to fill them in.
- **Not every deal names an airport at all.** Some publishers write a region — "the Baltic countries", "several German cities" — and those rows carry the text as published. Filtering by departure place keeps only the deals that can be shown to leave from there.
- **The airline is only there when the deal names one.** The Flight Deal and the on-sale list name a carrier on every deal, and Travelfree names one on most; Fly4free rarely does, so many of its rows arrive with an empty airline even where the headline mentions a carrier. Nothing is inferred from the route.
- **The same fare from two publishers is not always spotted as the same fare.** A repeat is removed when the route wording, price, currency, trip type and publication date all match, which reliably catches the same publisher posting twice. Two publishers writing the same deal in their own words — "UK to Jamaica from £371" against "UK cities to Montego Bay, JAMAICA from £377" — are delivered as two rows, and both are charged for. `alsoSeenIn` is filled in only where the match was certain, so an empty value means "not matched", not "not published elsewhere".
- **A fare re-posted on a later date is a new row, not a duplicate.** These publishers re-run a recurring deal months apart, and both postings are kept with their own dates and links, because that repetition is the signal a fare-trend study is looking for.
- **Some publishers mix hotel and package offers into the same stream.** They are labelled and left out by default; switch `onlyFlightDeals` off to keep them.
- **If you name no departure place, on-sale fares reflect wherever the run is made from**, because that is what the seller features. Name your departure cities to control which fares come back.
- **A source that refuses to answer is reported as a failed read, never as "no deals found".** The run summary counts sources that published nothing separately from sources that could not be read, and a run where every source failed ends as a failed run rather than a green one with an empty table.
- **Speed depends on the size of the job** and on how quickly the publishers answer; no fixed throughput is promised.
- **The publishers' terms govern automated access.** You are responsible for using the data lawfully and in line with those terms.

### FAQ

**Do I need an account with any of these publishers?**
No. Nothing is signed in to and nothing is bought.

**Does it need my login or password?**
No. There is nothing to configure beyond the search itself.

**Are these fares still bookable?**
Treat every row as a snapshot of what was published. Deal fares are the most perishable prices in travel — many last hours. `ageDays` and `isLikelyExpired` are there so you never have to guess, and every row links straight to the deal so you can check it.

**Why do some deals have two prices?**
Because the deal quotes two — a basic-economy fare and a regular-economy one, often hundreds apart. Both are returned in `priceBasic` and `priceRegular`, and `priceLowest` is the cheaper of them. Which label belongs to which number is taken from the text, never from the order.

**Do I get one row or two when two publishers post the same fare?**
Usually two. A repeat is merged only when both postings describe the route the same way and agree on the price, currency, trip type and date — which is what a second read of one publisher looks like. Two publishers wording the same deal differently, or quoting it a few pounds apart, are not recognised as one fare and arrive as two rows. When a merge does happen the survivor's `alsoSeenIn` names the other publisher, and it never names its own.

**How far back does the archive go?**
To 2011, over 68,000 published deals. Turn on `includeBackfill` and set `maxDeals` to how many you want; use `since` to take only what is new.

**Can I schedule it?**
Yes. A daily or twice-daily run with `since` set to your last run is the natural way to use it, and is how an alerting product would be built on top.

# Actor input Schema

## `sources` (type: `array`):

Which deal publishers to collect from. Leave all four on for the widest coverage. Where two of them post the same fare and describe the route in the same words at the same price, the repeat is merged into one row that records where else it appeared; publishers who word the same deal differently are delivered as separate rows.

## `originCities` (type: `array`):

Only keep deals leaving from these places. Use airport codes (JFK, LHR), city-wide codes (NYC, LON) or plain names ("San Francisco"). Leave empty to collect deals from everywhere. On-sale fares are published per city, so an airport code is read at city level and the run log says so.

## `keyword` (type: `string`):

Only keep deals whose own text mentions this word — the headline, the opening lines, either end of the route, the airline or the cabin. A publisher's unrelated tags are not matched, so a Rome to Hanoi fare tagged "budapest" is not returned as a Budapest deal. Leave empty to keep everything.

## `tripType` (type: `string`):

Keep only one-way deals, only return deals, or both. Deals whose trip type the publisher never stated are left out when you pick one.

## `currency` (type: `string`):

Which currency the on-sale fares are asked for. Deal write-ups are published in whatever currency their author used, so every row also carries the currency it was actually published in — that value is never overwritten with the one you picked here.

## `since` (type: `string`):

Only keep deals published after this date, as YYYY-MM-DD. Ideal for a scheduled run that should pick up only what is new since the last one.

## `maxAgeDays` (type: `integer`):

Drop anything published more than this many days ago. Leave empty to keep deals of any age. These fares move fast, so 2–7 days is the useful window for alerting.

## `includeBackfill` (type: `boolean`):

Go beyond the latest deals and work back through the archive, which reaches to 2011. Use this to build a history for research; leave it off for day-to-day alerting.

## `maxDeals` (type: `integer`):

The most rows this run will produce, across every source, after filtering and merging. Newest deals come first.

## `includeDealDetails` (type: `boolean`):

Read each deal write-up as well as its headline. The write-up is where the fare class, full routing, stopover rules, mileage earning, cents-per-mile AND both airport codes are stated, plus the flight numbers and price bands on on-sale fares. Turn it off for a faster, headline-only run — those rows then arrive with the airport codes, routing, mileage and publisher tags empty, and each one says so in its data notes.

## `onlyFlightDeals` (type: `boolean`):

Leave out hotel and holiday-package posts, which some publishers mix into the same deal stream.

## `requirePrice` (type: `boolean`):

Leave out posts where no fare was published, such as roundups and announcements. Prices are never inferred, so a post without one is returned empty rather than filled in.

## `excludeLikelyExpired` (type: `boolean`):

Leave out deals old enough to have expired. Off by default, so you see everything and can judge from the age on each row.

## `likelyExpiredAfterDays` (type: `integer`):

How old a deal has to be before it is marked as probably gone. Deal publishers themselves warn that a fare more than two days old has usually died, which is the default here.

## Actor input object example

```json
{
  "sources": [
    "theflightdeal",
    "fly4free",
    "travelfree",
    "cheapoair"
  ],
  "originCities": [
    "NYC",
    "SFO",
    "London"
  ],
  "keyword": "Tokyo",
  "tripType": "any",
  "currency": "USD",
  "since": "2026-08-01",
  "maxAgeDays": 7,
  "includeBackfill": false,
  "maxDeals": 200,
  "includeDealDetails": true,
  "onlyFlightDeals": true,
  "requirePrice": true,
  "excludeLikelyExpired": false,
  "likelyExpiredAfterDays": 2
}
```

# Actor output Schema

## `deals` (type: `string`):

One row per deal: route, airline, price, trip type, age, and the fare-class, routing and mileage detail where it was published.

## `overview` (type: `string`):

The same deals as a table, newest first.

## `spreadsheet` (type: `string`):

All deals in one CSV file.

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

Counts for this run: deals collected per publisher, deals filtered out and why, duplicates merged, sources that published nothing, and sources that 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 = {
    "maxDeals": 200
};

// Run the Actor and wait for it to finish
const run = await client.actor("apt_marble/flight-deals-mistake-fare-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 = { "maxDeals": 200 }

# Run the Actor and wait for it to finish
run = client.actor("apt_marble/flight-deals-mistake-fare-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 '{
  "maxDeals": 200
}' |
apify call apt_marble/flight-deals-mistake-fare-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,apt_marble/flight-deals-mistake-fare-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/BEAhgWIaWo9wNafAG/builds/jXgzqfGDmQj5reen5/openapi.json
