# Flight Price Drop Monitor (`apt_marble/flight-price-drop-monitor`) Actor

Watch flight routes and departure dates on a schedule and get a row only when a fare actually moves — old price, new price, how far it fell, and whether it is a new low. It keeps the price history for you, so you never build one.

- **URL**: https://apify.com/apt\_marble/flight-price-drop-monitor.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 price change detecteds

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/actors/running/actors-in-store.md#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 Price Drop Monitor

Give it a list of routes and a departure window, run it on a schedule, and it returns **only what changed**: the old fare, the new fare, how far it moved, and whether it is the cheapest that route and date has ever been. It remembers every price it has seen for you, so you never have to build or maintain a price database of your own. No account and no login are needed — just run it.

Built for fare-alert apps, deal newsletters, affiliate travel sites and corporate travel teams who need a clean "this got cheaper" feed rather than a fresh dump of every fare on every date.

***

### What you can do with it

- **Power a fare-alert product.** Run it twice a day over a watchlist and push every drop straight to your users, with the previous price, the new price and the percentage move already worked out.
- **Catch new lows.** Every row says whether the fare is the lowest ever recorded for that route and date, even when the move itself is small.
- **Watch a corporate travel programme.** Track the routes your company flies most and get told when a booked-window fare falls far enough to be worth rebooking.
- **Fill a deals newsletter automatically.** Set a threshold like "only drops of 20% or more" and the run produces a ready-made shortlist.
- **Compare markets honestly.** Fares genuinely differ by country of sale, so each market keeps its own history and prices are never converted between currencies.
- **Track a low-cost carrier's own fares** on the routes it flies, including the departure and arrival times behind each day's cheapest seat.

***

### What you get

One row per watched route and departure date whose fare moved. A real record, abridged:

```json
{
  "watchlist": "default",
  "source": "google",
  "route": "JFK-LHR",
  "origin": "JFK",
  "originName": "John F. Kennedy International Airport",
  "destination": "LHR",
  "destinationName": "London Heathrow Airport",
  "departureDate": "2026-08-17",
  "daysToDeparture": 1,
  "market": "US",
  "currency": "USD",
  "cabinClass": "economy",
  "changeType": "changed",
  "alertReason": "new-low",
  "direction": "drop",
  "isPriceDrop": true,
  "isNewLow": true,
  "newPrice": 294.5,
  "previousPrice": 349.5,
  "changeAbs": -55,
  "changePct": -15.7368,
  "lowestSeen": 349.5,
  "highestSeen": 402,
  "priceChangesRecorded": 4,
  "firstSeenAt": "2026-08-02T06:00:11.482Z",
  "previouslySeenAt": "2026-08-15T06:00:09.771Z",
  "daysSinceLastSeen": 1,
  "carrier": "AA",
  "carrierName": "American",
  "flightNumber": "AA142",
  "departureTime": "09:35",
  "arrivalTime": "21:40",
  "stops": 0,
  "priceRating": null,
  "isEstimated": false,
  "collectedAt": "2026-08-16T18:41:02.004Z"
}
```

The flight behind the fare — airline, flight number and times — is filled in wherever the source you chose publishes it, and left empty rather than guessed where it does not. Sources that publish a day-level lowest price instead of a specific flight fill in `priceRating` and mark the row `isEstimated`.

Alongside the rows, every run writes a summary that keeps three things strictly apart: routes that were read, routes that were read and genuinely had no fare on sale, and routes that could not be checked at all.

***

### Input reference

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `routes` | array of text | `["JFK-LHR"]` | The routes to watch, written as from-to airport or city codes. Up to 25 in one run. `{"origin":"JFK","destination":"LHR"}` works too. |
| `fareSource` | choice | `kiwi` | Whose published fares to watch: **Kiwi.com**, **Skyscanner**, **Ryanair** or **Google Flights**. |
| `market` | choice | `US` | The country you are selling into. Fares and availability really do differ by market. |
| `currency` | choice | `USD` | The currency you would like fares in. Every row is labelled with the currency actually returned. Ryanair files its fares in a fixed list — EUR, GBP, USD, PLN, CZK, SEK, DKK, NOK, HUF, CHF, MAD — and asking that source for any other is refused before the run starts. |
| `cabinClass` | choice | `economy` | Which cabin to watch. Ryanair and Skyscanner publish economy day prices only. |
| `startDate` | date | tomorrow | The earliest departure date you care about. |
| `windowDays` | integer | `60` | How many days of departures to watch, counted from the first date. Maximum 330. |
| `maxDatesPerRoute` | integer | `60` | An upper limit on watched departure dates per route. Maximum 360. |
| `watchlistName` | text | `default` | Names the saved price history. Keep it the same between runs; separate names never mix. |
| `alertOn` | choice | `drop` | Report price drops, price rises, or any move. |
| `minChangePct` | integer | `5` | Moves smaller than this are counted but not reported. `0` reports every move. |
| `minChangeAbs` | integer | `0` | An extra floor in the fare's own currency. A move must clear both this and the percentage. |
| `alertOnNewLow` | boolean | `true` | Always report a fare that is the cheapest ever recorded, however small the move. |
| `emitNewDates` | boolean | `true` | Report a starting price for a departure date the watchlist has not seen before. |
| `emitBaselineOnFirstRun` | boolean | `true` | Report starting prices on the very first run for a watchlist. |
| `emitUnchanged` | boolean | `false` | Also return dates whose fare did not move. Produces many more rows. |
| `emitDisappeared` | boolean | `false` | Report watched dates whose fare is no longer published. |
| `resetHistory` | boolean | `false` | Forget everything recorded for this watchlist and start again. |

***

### Output fields

| Field | Meaning |
| --- | --- |
| `watchlist`, `source`, `market`, `currency`, `cabinClass` | What was being watched, and how. |
| `route`, `origin`, `originName`, `destination`, `destinationName` | The route, with readable airport or city names. |
| `departureDate`, `daysToDeparture` | The departure date this fare is for, and how far away it is. |
| `changeType` | `changed`, `new`, `baseline`, `reappeared`, `unchanged` or `withdrawn`. |
| `alertReason` | Why the row is here: `price-drop`, `price-rise`, `new-low`, `first-seen`, `fare-available-again`, `fare-withdrawn`, `baseline` or `no-change`. |
| `newPrice`, `previousPrice` | The fare now and the fare last time. `null` where there is nothing to compare. |
| `changeAbs`, `changePct`, `direction`, `isPriceDrop` | The size and direction of the move. `null` rather than zero when a comparison is impossible. |
| `isNewLow`, `lowestSeen`, `highestSeen` | Whether this is the cheapest recorded, and the range recorded so far. |
| `priceChangesRecorded`, `firstSeenAt`, `previouslySeenAt`, `daysSinceLastSeen` | How long this date has been watched and when it was last seen. |
| `carrier`, `carrierName`, `flightNumber`, `departureTime`, `arrivalTime`, `stops` | The flight behind the fare, where the source publishes it. |
| `priceRating` | The source's own "cheap / typical / expensive" label for that day, where it gives one. |
| `isEstimated` | `true` when the number is an indicative lowest price for the day rather than a quote for one specific flight. |
| `currencyKnown` | `false` on the rare row where the source did not state a currency. The currency is then `null` rather than guessed. |
| `collectedAt` | When this check happened. |

***

### Pricing

You pay per event, so a quiet watchlist is cheap and a busy one pays for itself.

| Event | When you are charged | Price |
| --- | --- | --- |
| **Price change detected** | For each row returned — a change, a new low, a first-seen price or, if you asked for them, unchanged and withdrawn fares. | **$4.00 per 1,000 rows** |
| **Route checked** | Once for every route the run checks, whether or not anything moved. | **$0.015 each** |

A route that turns out to be quiet is still real work, so it is charged. A route the source refuses to answer is **not** charged.

**Worked example.** A watchlist of 20 routes, 60 departure dates each, run once a day.

*The first run costs much more than the rest, and it is the one to size your cost limit against.* It has nothing to compare with, so by default it records a starting price for **every** watched date: 20 × 60 = 1,200 rows. That is 1,200 × $0.004 = $4.80, plus 20 × $0.015 = $0.30 — about **$5.10 once**. Set your maximum cost per run above that, or turn off *Report starting prices on the very first run* and pay only the $0.30 in route checks to establish the baseline silently.

*Every run after that reports movement only.* Each run checks 20 routes — 20 × $0.015 = **$0.30**. Say 45 fares moved enough to report: 45 × $0.004 = **$0.18**. That run costs about **$0.48**, so a daily fare-alert feed over 20 routes costs roughly **$14 a month** after the first day. The same is true whenever you add routes or widen the window: the newly watched dates each get one starting-price row.

Turning on unchanged rows would return about 1,200 rows every run instead of 45, so leave it off unless you want the full price table.

**If a run hits your cost limit**, it stops returning rows — and it does *not* record the changes it could not return. The saved history is left exactly as it was, so the next run finds those same changes again and returns them. You are told in the run summary how many were held back.

***

### Limits & what this actor cannot do

- **Fares are a snapshot at the moment of the check.** Airline pricing changes continuously, and a fare seen now may be gone minutes later.
- **It reports what the source publishes to the public. It cannot book, cannot hold a seat and cannot guarantee a fare is still available.** Treat every row as a signal to go and look, not as a reservation.
- **A check the source refuses to answer is reported as a failed check, never as "no fares found".** Those routes keep the prices they already had and are listed separately in the run summary — so a bad day never looks like a wave of fares disappearing.
- **Fares and availability differ by market and by currency, and are never converted.** A price in one market is not comparable with a price in another, and if a source answers in a different currency than the one you asked for, the row is labelled with what came back and the move is reported as not comparable rather than as a drop.
- **A fare is never invented.** A date with no published fare is simply not tracked — never a zero, never yesterday's price carried forward, never a gap filled in.
- **The first run for a watchlist is a starting point, not a change report.** Movement is reported from the second run onwards.
- **Fares do not move minute to minute.** Once or twice a day is the honest cadence for this; running it every few minutes would return a flat line and still be charged for.
- **Not every source publishes a price for every date.** Some publish a price only for the days they have something to sell, and those gaps are left as gaps.
- **Some sources give an indicative lowest price for a day rather than a quote for one specific flight.** Those rows are marked `isEstimated`, and the flight details are left empty rather than guessed.
- **Ryanair covers its own network only.** A route it does not fly is reported as such, not as a route without fares. On that source, a month with nothing on sale and a route the airline does not fly look identical from the outside, and the run summary says so rather than pretending to know which it was.
- **Ryanair prices in a fixed list of currencies** — EUR, GBP, USD, PLN, CZK, SEK, DKK, NOK, HUF, CHF and MAD. Ask it for any other and it does not object: it either prices the month in euro while claiming the currency you asked for, or returns an empty month that is indistinguishable from a route with nothing on sale. So that combination is refused when the run starts, rather than turned into wrong or missing fares.
- **If a run reaches your maximum cost before it has returned everything it found**, the changes it could not return are *not* recorded as delivered. The saved history is left untouched and the next run reports them again, so an alert is never quietly lost to a cost cap.
- **The Google Flights source is checked one departure date at a time**, so each route is limited to 14 departure dates in a run. For a wide window, choose one of the sources that publishes a whole calendar at once.
- **One cabin per run**, and two of the four sources publish economy day prices only.
- **Departure dates are published about a year ahead.** A window that reaches past that is shortened, and you are told.
- **The saved history keeps the most recent prices per watched date, not an unlimited archive.** "Lowest ever recorded" means the lowest inside what is kept for that date.
- **One watchlist name, one history.** Change the routes, market or cabin and you are watching something new — reset the history so the comparison stays meaningful.
- **Speed depends on how big the watchlist is and how quickly the source answers.** No fixed run time is promised.
- The source's own terms govern automated use. You are responsible for using the data lawfully and in line with those terms.

***

### FAQ

**Do I need an account on the travel site?**
No. Nothing is signed in to and nothing is signed up for.

**Does it need my login or password?**
No. There is nothing to connect and nothing to authorise.

**Can I schedule it?**
Yes, and that is the point of it. Set it to run once or twice a day with the same watchlist name, and each run compares against the last one automatically.

**Are these fares bookable?**
They are the fares the source was publishing at the moment of the check. Airline pricing moves constantly, so treat a row as a strong signal rather than a guarantee — this actor cannot book or hold anything.

**Why did I get no rows at all?**
Either nothing moved by more than your threshold, or the routes you watch genuinely had no fares on the dates you chose. The run summary tells you which, and lists separately any route that could not be checked.

**How do I get only the big drops?**
Raise `minChangePct`, and add `minChangeAbs` if you also want a floor in money. Leave `alertOnNewLow` on if you still want to hear about a record-low fare even when the move itself is small.

**Can I watch the same routes in several markets?**
Yes — run it once per market with a different `watchlistName` for each. Each market keeps its own history, which is the only way a comparison stays meaningful.

**What happens when a route cannot be checked?**
Nothing is recorded for it, its saved prices are left untouched, and it is listed in the run summary as a failed check. It is never reported as a route whose fares vanished.

# Actor input Schema

## `routes` (type: `array`):

The routes you want watched, written as from-to airport or city codes, for example JFK-LHR. Up to 25 routes in one run.

## `fareSource` (type: `string`):

Which travel site's published fares you want to watch. Each one sells a different mix of airlines, so pick the one your buyers actually book on. Kiwi.com and Skyscanner cover many airlines; Ryanair covers its own network only; Google Flights is checked one departure date at a time and gives you the exact flight behind the price.

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

The country you are selling into. Fares and availability genuinely differ by market, so this is part of what makes a price comparable from one run to the next.

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

The currency you would like fares in. Every row is labelled with the currency the source actually returned, which is not always the one requested, and prices are never converted between currencies. Ryanair files its fares only in EUR, GBP, USD, PLN, CZK, SEK, DKK, NOK, HUF, CHF and MAD — ask it for any other and the run is refused rather than answered with euro prices or an empty month.

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

Which cabin to watch. Ryanair and Skyscanner publish economy day prices only; choose economy for those two.

## `startDate` (type: `string`):

The earliest departure date you care about, as 2026-11-03. Leave empty to start from tomorrow.

## `windowDays` (type: `integer`):

The size of the departure window, counted from the first date. A wider window watches more dates and produces more alerts.

## `maxDatesPerRoute` (type: `integer`):

An upper limit on how many departure dates are tracked for each route, counted from the earliest. Keeps a wide window from turning into a very large watchlist.

## `watchlistName` (type: `string`):

Names the saved price history this run compares against. Use one name per watchlist and keep it the same between runs, so each schedule builds its own history. Separate names never mix.

## `alertOn` (type: `string`):

Report only fares that fell, only fares that rose, or every move.

## `minChangePct` (type: `integer`):

Moves smaller than this are counted but not reported, so small day-to-day noise does not fill your dataset. Set 0 to report every move.

## `minChangeAbs` (type: `integer`):

An extra floor in the fare's own currency. A move must clear both this and the percentage to be reported. Set 0 to use the percentage alone.

## `alertOnNewLow` (type: `boolean`):

Report a fare that is the cheapest ever recorded for that route and date, even when the move is smaller than your threshold.

## `emitNewDates` (type: `boolean`):

Report a starting price for a departure date the watchlist has not seen before. Turn this off to receive changes only.

## `emitBaselineOnFirstRun` (type: `boolean`):

The first run for a watchlist has nothing to compare against, so it records a starting price for every watched date — routes × dates rows, which is far more than a normal run returns and is what your first run is billed for. Leave this on to receive those starting prices, or turn it off to receive nothing until something actually moves.

## `emitUnchanged` (type: `boolean`):

Also return a row for every watched date that stayed exactly the same. Useful for building a full price table; it produces many more rows.

## `emitDisappeared` (type: `boolean`):

Report a watched date whose fare is no longer published. Only dates the source was actually able to answer for are ever reported this way.

## `resetHistory` (type: `boolean`):

Forget everything recorded for this watchlist and treat this run as a fresh starting point. Use it after changing routes, market or cabin.

## Actor input object example

```json
{
  "routes": [
    "JFK-LHR",
    "LGW-BCN"
  ],
  "fareSource": "kiwi",
  "market": "US",
  "currency": "USD",
  "cabinClass": "economy",
  "windowDays": 60,
  "maxDatesPerRoute": 60,
  "watchlistName": "default",
  "alertOn": "drop",
  "minChangePct": 5,
  "minChangeAbs": 0,
  "alertOnNewLow": true,
  "emitNewDates": true,
  "emitBaselineOnFirstRun": true,
  "emitUnchanged": false,
  "emitDisappeared": false,
  "resetHistory": false
}
```

# Actor output Schema

## `priceChanges` (type: `string`):

One row per watched route and departure date whose fare changed.

## `priceChangesCsv` (type: `string`):

The same rows as a spreadsheet download.

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

Counts of routes checked, routes with no published fare, and routes that could not be checked at all.

## `consoleRun` (type: `string`):

This run in the console, with its full log.

# 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 = {
    "routes": [
        "JFK-LHR",
        "LGW-BCN"
    ],
    "fareSource": "kiwi",
    "market": "US",
    "currency": "USD",
    "cabinClass": "economy",
    "windowDays": 60,
    "maxDatesPerRoute": 60,
    "watchlistName": "default",
    "alertOn": "drop",
    "minChangePct": 5,
    "minChangeAbs": 0
};

// Run the Actor and wait for it to finish
const run = await client.actor("apt_marble/flight-price-drop-monitor").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 = {
    "routes": [
        "JFK-LHR",
        "LGW-BCN",
    ],
    "fareSource": "kiwi",
    "market": "US",
    "currency": "USD",
    "cabinClass": "economy",
    "windowDays": 60,
    "maxDatesPerRoute": 60,
    "watchlistName": "default",
    "alertOn": "drop",
    "minChangePct": 5,
    "minChangeAbs": 0,
}

# Run the Actor and wait for it to finish
run = client.actor("apt_marble/flight-price-drop-monitor").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 '{
  "routes": [
    "JFK-LHR",
    "LGW-BCN"
  ],
  "fareSource": "kiwi",
  "market": "US",
  "currency": "USD",
  "cabinClass": "economy",
  "windowDays": 60,
  "maxDatesPerRoute": 60,
  "watchlistName": "default",
  "alertOn": "drop",
  "minChangePct": 5,
  "minChangeAbs": 0
}' |
apify call apt_marble/flight-price-drop-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,apt_marble/flight-price-drop-monitor"
        }
    }
}

```

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/dpryPdcHPwVtTU0la/builds/haHSbeWmkxQm3q7aU/openapi.json
