# Flight Fare History API: book-now / wait from your own history (`frameprobe/flight-fare-history-api`) Actor

Book-now or wait, scored against the fare history YOU accumulate in your own key-value store, not a generic average. With no history yet it prices a spread of nearby dates and labels exactly what it could establish: every row names its basis and confidence, or says plainly it has neither.

- **URL**: https://apify.com/frameprobe/flight-fare-history-api.md
- **Developed by:** [FrameProbe](https://apify.com/frameprobe) (community)
- **Categories:** Travel, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 fare observation stored and scoreds

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 Fare History API

**Is this a good price, compared with what you have actually been offered?**

Every flight scraper tells you what a fare costs today. None of them tells you whether today is a
good day to book, because none of them remembers yesterday. This one keeps a price history per
itinerary in **your own** Apify key-value store and scores each new observation against it.

```
POST /acts/~flight-fare-history-api/runs
{ "origin": "LAX", "destination": "JFK" }
```

```json
{
  "rowType": "observation",
  "itinerary": "LAX-JFK 2026-11-02, economy",
  "price": 412.0,
  "currency": "USD",
  "carrier": "Delta",
  "stops": 0,
  "verdict": "BOOK_NOW",
  "basis": "history",
  "trend": "FALLING",
  "confidence": "high",
  "reason": "412 is the cheapest seen across 14 observations (cheapest 412, median 505).",
  "basisReference": 505.0,
  "basisCheapest": 412.0,
  "basisCount": 14,
  "pctVsBasis": -18.42,
  "routeDatesPriced": 0,
  "deltaVsPrevious": -38.0,
  "deltaVsMedian": -93.0,
  "pctVsMedian": -18.42,
  "pctVsCheapestSeen": 0.0,
  "observationsUsed": 14,
  "historyCount": 15,
  "historyFirstSeenAt": "2026-08-22T09:00:04+00:00",
  "historyCheapest": 412.0,
  "historyMedian": 505.0,
  "notes": []
}
```

### Built for pipelines

- **One job.** Route in, verdict out. No modes, no flags that change the response shape.
- **Two required fields.** `origin` and `destination`, and that is the whole call. With no dates
  it watches a single departure 60 days out, computed at run time, so a stored request never goes
  stale.
- **Same keys on every row**, including rows that failed. Nulls, never absent keys, so you never
  branch on which fields exist.
- **Idempotent.** Two runs an hour apart do not double-count: an observation inside the
  one-hour window is reported and not appended.
- **Deterministic ordering.** Rows come back in the order you listed the dates, then any
  route-baseline samples, summary last. A diff between two runs shows price changes, not row
  shuffling.

### The verdicts

| Verdict | Means |
|---|---|
| `BOOK_NOW` | At the cheapest seen, materially below the median, and in the cheapest quarter. Needs 5+ prior observations of this itinerary. **`basis: history` only.** |
| `GOOD_PRICE` | Meaningfully below the baseline, but not at the floor. |
| `TYPICAL` | Within 3% of the baseline. Normal. |
| `WAIT` | Above the baseline; it has been cheaper. |
| `INSUFFICIENT_HISTORY` | Fewer than 3 prior observations **and** nothing else to compare with. The arithmetic is returned; the label is withheld. |
| `BASELINE` | The first observation for this itinerary, with nothing else to compare it with. |

`trend` (`FALLING` / `RISING` / `FLAT` / `UNKNOWN`) is reported separately, because "expensive but
falling" and "expensive and rising" are different decisions. It needs history, so it is `UNKNOWN`
until you have two observations.

### What it compares against, and how much that is worth

Every row names its `basis`. A verdict from a month of history is a stronger claim than one from a
spread of dates read in the same minute, and `basis` is how a pipeline tells them apart without
parsing English. `confidence` is capped by the basis, so a weaker yardstick can never report
`high`.

| `basis` | The question it answers | Max `confidence` | Costs |
|---|---|---|---|
| `history` | Is this cheap **compared with what you have been offered for this trip since you started watching**? | `high` | nothing extra |
| `route` | Of the departure dates you could fly, is this one cheap? | `medium` | one upstream search per extra date |
| `upstream_typical` | Is this below the upstream's own typical-price figure for the route? | `low` | nothing extra |
| `none` | Nothing was available. `BASELINE` or `INSUFFICIENT_HISTORY`. | `none` | nothing |

The default scraper, `memo23/google-flights-scraper`, does **not** publish a typical price (checked
against a live run). On it the ladder is `history` then `route` then `none`. The third tier only
fires if you point `upstreamActor` at a scraper that does publish one.

**`BOOK_NOW` is reachable only from `history`.** It asserts a floor across time, and a set of
prices all read in the same minute has no time axis to find a floor in. A cross-section can say
this date is the cheapest of the dates you looked at; it cannot say today is the cheapest day to
buy it.

### Your first run is not wasted

A history-based tool has a real cold start: on day one there is no history. Rather than hand you
`INSUFFICIENT_HISTORY` and an invitation to come back in a fortnight, a run whose itineraries have
too little history prices a spread of **other departure dates on the same route** and scores
today's fare against those instead.

**How often that succeeds, measured rather than promised.** The cross-section needs at least four
of those extra dates to return a price, so it depends on the upstream scraper answering.

**Measured 2026-09-07, against the default upstream:** 58 searches, 0 empty. Four first runs on
four fresh routes, each with no stored history:

| First run | Result, measured 2026-09-07 |
|---|---|
| Produced a price | **4 of 4** |
| Produced a route verdict, `basis: route` | **4 of 4** |
| At `medium` confidence, all six baseline dates priced | **4 of 4** |

An earlier default upstream returned empty on ~39% of searches, which put the same figure at about
34%. **That is why every row is labelled rather than assumed:** the numbers above move with the
upstream you point `upstreamActor` at, and a run tells you which case it got instead of leaving you
to infer it.

**This Actor never promises a verdict. It promises a labelled outcome.** Every row states the
`basis` it used and the `confidence` that basis supports, or says plainly that it had neither, so a
pipeline can branch on the label instead of trusting a number.

This is a real first run, not an illustration. Thanksgiving week out of LAX:

```
{ "origin": "LAX", "destination": "JFK", "departDates": ["2026-11-23"] }
```

```json
{
  "verdict": "WAIT",
  "basis": "route",
  "confidence": "low",
  "price": 254.0,
  "carrier": "JetBlue",
  "basisReference": 202.0,
  "basisCheapest": 199.0,
  "basisCount": 4,
  "pctVsBasis": 25.74,
  "reason": "254 is 25.7% above the 202 median of 4 other departure dates on this route (cheapest 199). This compares dates, not days: it places this date among the ones you could fly, and does not say whether today is a good day to book it.",
  "historyCount": 1
}
```

The series still starts building on that same run, and as soon as it reaches three observations
the basis switches to `history` on its own. Nothing to change, nothing to re-run.

**Note the `low`, and note that two of the six baseline dates are missing from that count.** The
upstream returned nothing for two of them, so the cross-section was built from four dates instead
of six and the confidence was lowered to match. Nothing here rounds up: a run reports the width of
the yardstick it actually got.

**`medium` requires all six baseline dates to return a price.** Against the default upstream that
is the ordinary result - four of four measured first runs reached it on 2026-09-07. Against a less
reliable scraper it is rare, and the row says `low` rather than rounding up. The example above,
recorded against the older default, is what a partial cross-section looks like.

**What it costs, plainly.** Each extra date is one more upstream search billed to your account,
exactly like a watched date. It is **not** charged this Actor's `fare-observation` event.

- `routeBaselineDates` defaults to **6** and is capped at 12. A one-date first run therefore
  costs **7 upstream searches** instead of one. Lower it to 4 for a cheaper first run, or to 0 to
  skip the cross-section entirely.
- **`medium` needs all six baseline dates to return a price**, which the default upstream managed
  on four of four measured first runs. A less reliable scraper yields `low`, which is reported
  rather than rounded up.
- Spending stops as soon as the run has priced **7 dates in total**, the width a date needs to be
  scored at `medium`, since a date is never counted in its own baseline. List seven `departDates`
  yourself and the cross-section is free.
- Nothing is bought when your history is already long enough to answer, when you passed a
  `datasetId`, or when `routeBaselineDates` is `0`.
- The extra dates come back as `rowType: "routeSample"` rows with their prices, so you can check
  the median by hand rather than take it on trust. They carry no verdict and are never charged.

Dates are chosen deterministically, in whole weeks either side of your date (`-7, +7, -14, +14…`),
so day of week is held constant: a Monday is compared with Mondays. Past dates are skipped, not
clamped.

### How it differs from the "typical price" badge

Google's own insight compares a fare with an aggregate across all travellers on that route. It is
used here only as the last-resort `upstream_typical` basis, with confidence capped at `low`,
because we cannot see how it was computed and it cannot establish a floor. The default scraper does
not expose it at all, so in practice this Actor never re-serves it. The `history` basis
compares a fare with **your** observed series for one exact itinerary shape: same cabin, same
passenger count, same stop and carrier filters. Those diverge exactly when it matters: a route can
be expensive in general and at its own floor today.

### What it costs

| Event | Price |
|---|---|
| Actor start | $0.01, charged only after your input validates |
| Fare observation stored and scored | $0.005 per itinerary |

**An itinerary that produced nothing is not charged.** A failed upstream search, an empty result,
or rows with no readable price all appear in the dataset with the reason and cost you nothing
beyond the start event. Route-baseline samples are not charged at all.

#### What a run actually costs you, both halves

Most of the bill is not ours. These are worked examples at the default `maxItineraries` of 20, where
one upstream search costs about $0.025 on `memo23/google-flights-scraper`. Your own numbers will move
with the scraper you name and the results a route returns.

| Run | Upstream searches | This Actor | Upstream, to you | All-in |
|---|---|---|---|---|
| First run, one date (buys a 6-date route baseline) | 7 | $0.015 | ~$0.175 | **~$0.19** |
| First run, seven dates (baseline comes free) | 7 | $0.045 | ~$0.175 | **~$0.22** |
| Repeat run, one date, once your history can answer | 1 | $0.015 | ~$0.025 | **~$0.04** |

Two things worth reading off that table. **The first run of a route is the expensive one**, because
it buys the cross-section that lets it answer at all; from the third run on, that route costs about
four cents. And **listing seven dates instead of one costs you nothing extra upstream** while
producing seven series instead of one, which is the cheapest way to use this Actor.

**The upstream search is billed separately, to you.** This Actor does not scrape Google. It runs
`automation-lab/google-flights-scraper` (or whichever scraper you name) as a sub-run on your own
account, at that Actor's price. `memo23/google-flights-scraper` is also supported directly; the two
use different input field names and the translation is done for you. Only memo23 filters by
airline, so `airlines` requires it - the run is refused rather than storing an observation labelled
with a carrier filter that was never applied. `maxStops` works with either, applied by this Actor
when the upstream cannot. Watching 30 dates means 30 upstream searches, which is why
`dateWindowDays` is capped at 60 and a run over 60 itineraries is refused rather than billed.
Route-baseline dates are searches too, capped separately by `routeBaselineDates`.

Already ran a scraper yourself? Pass `datasetId` and nothing is fetched at all.

### Your history is yours

Series are stored in a named key-value store **in your account** (`fare-history` by default), one
JSON record per itinerary. Read it, export it, point another tool at it, or delete it. None of
that needs this Actor. Records are append-only and capped at 400 observations, dropping the oldest.

**A series never mixes currencies.** If the upstream returns a different currency from the stored
series, today's price is reported and the history is left untouched, because a median computed
across USD and EUR describes nothing. The same rule applies to the cross-date baseline: dates
priced in another currency are left out of it rather than converted.

**Unreadable history restarts rather than failing.** If a stored record cannot be parsed, the run
tells you so on the row and starts a fresh series. It will not fail your run, and it will not
pretend the history was fine.

**Route-baseline dates are not stored.** They are priced to answer today's question and discarded.
Only the dates you asked to watch become series, so a probe can never quietly inflate your history
or your bill.

### Limits, stated plainly

- **The cross-date baseline answers a different question from the history one.** It tells you
  where a date sits among the dates you could fly. It cannot tell you whether the fare is
  falling, and it will never say `BOOK_NOW`. Read `basis` before acting on `verdict`.
- **A cross-section needs at least 4 other priced dates.** Below that it is a couple of anecdotes,
  and the run falls back to the upstream figure or to `BASELINE` rather than dressing them up.
- **Three prior observations before a `history` verdict, five before `BOOK_NOW`.**
- **One observation per run per itinerary**, and observations closer together than an hour are not
  appended. Fares do not move on a timescale you can act on faster than that.
- **The series follows the cheapest qualifying itinerary**, not Google's "best" tag, whose
  definition mixes price with duration and can change without notice. Reproducibility beats a
  weighting we cannot see. On a live LAX-JFK search the `best` fares were $219 and $244 while the
  cheapest was $205: the gap is the point.
- **It depends on an upstream scraper.** If that Actor breaks or gets blocked, this one reports the
  failure per itinerary and charges nothing for it, but it cannot invent a price.

# Actor input Schema

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

3-letter IATA code, e.g. LAX. Case does not matter.

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

3-letter IATA code, e.g. JFK.

## `departDates` (type: `array`):

YYYY-MM-DD dates. Each one becomes its own price series, because a fare is only comparable with itself. Leave this empty and the Actor watches a single date 60 days out, computed at run time, so a saved input never goes stale. Set dateWindowDays instead to watch a rolling window. Past dates are rejected rather than skipped.

## `dateWindowDays` (type: `integer`):

Instead of listing dates, watch every departure date from tomorrow to N days out. Each day is a separate upstream search billed to your account, so start small. Maximum 60.

## `returnAfterDays` (type: `integer`):

Leave empty for one-way. Set to 7 for a week-long round trip from each departure date.

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

Part of the series identity: economy and business are different products and are never compared with each other.

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

Passenger count. Also part of the series identity.

## `maxStops` (type: `integer`):

0 for nonstop only. Leave empty for any. This filters the candidate set, so it changes the minimum price and therefore starts its own series.

## `airlines` (type: `array`):

2-character carrier codes, e.g. \["DL"]. Leave empty for all carriers. Requires upstreamActor to be a scraper that filters by carrier natively (memo23/google-flights-scraper); the run is refused rather than storing an observation labelled with a filter that was never applied.

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

3-letter code. A series never mixes currencies: if the upstream returns a different one, today's price is reported and the history is left untouched rather than corrupted.

## `historyStoreName` (type: `string`):

The named key-value store IN YOUR ACCOUNT where the fare series are kept, one record per itinerary. Keep the default to build one history across runs. Change it to keep separate histories side by side. The data is yours: you can read, export or delete it without this Actor.

## `maxItineraries` (type: `integer`):

How many results to read from the upstream search before taking the cheapest. The upstream bills you per result returned, so this is the biggest lever on what a run actually costs. Raising it rarely changes the minimum: on a measured LAX-JFK search, results 10-29 contained nothing cheaper than the fare already found in the first ten.

## `routeBaselineDates` (type: `integer`):

Solves the cold start. On a run whose itineraries have too little stored history to be scored, this many EXTRA departure dates on the same route are priced so today's fare can be placed among the dates you could fly, instead of coming back as INSUFFICIENT\_HISTORY. They are spaced in whole weeks around your date, so day of week is held constant. Each one is a separate upstream search billed to your account, exactly like a watched date, and they are NOT charged this Actor's fare-observation event. Spending stops as soon as the run has priced 7 dates in total, which is what a date needs to be scored at medium confidence (a date is never counted in its own baseline). List 7 departDates yourself and this costs nothing; lower it to 4 for a cheaper first run at low confidence. Set to 0 to switch it off and accept INSUFFICIENT\_HISTORY until your history fills in.

## `upstreamActor` (type: `string`):

The Actor run to fetch today's itineraries. Its runs are billed to YOUR Apify account at its own price, separately from this Actor's events. Two are supported directly: automation-lab/google-flights-scraper (the default) and memo23/google-flights-scraper. Field names differ between them and are translated for you. Note that only memo23 can filter by airline, so `airlines` requires it; `maxStops` works with either, applied by this Actor when the upstream cannot. Naming any other scraper sends the memo23 field shape, which you can override with upstreamInput.

## `datasetId` (type: `string`):

Already have a flight scraper dataset? Point at it and nothing is fetched, so no upstream cost. The rows are scored against your history exactly as a fresh search would be. Route-baseline dates are never bought in this case: you chose to do the fetching, so this Actor spends nothing behind that choice.

## `upstreamInput` (type: `object`):

Passed through to the upstream Actor, e.g. proxy settings. Route, dates, cabin and currency are always set by this Actor and cannot be overridden here: a run that searched one itinerary and stored it under another would poison your history.

## Actor input object example

```json
{
  "origin": "LAX",
  "destination": "JFK",
  "cabinClass": "economy",
  "adults": 1,
  "currency": "USD",
  "historyStoreName": "fare-history",
  "maxItineraries": 20,
  "routeBaselineDates": 6,
  "upstreamActor": "automation-lab/google-flights-scraper"
}
```

# Actor output Schema

## `verdicts` (type: `string`):

One record per itinerary you asked to watch, including the ones that produced no price. Successful rows carry the fare, carrier, stops, the verdict (BOOK\_NOW / GOOD\_PRICE / TYPICAL / WAIT, or the honest BASELINE and INSUFFICIENT\_HISTORY), the basis it was computed from, the trend, and the arithmetic behind it. Rows that failed carry the same keys with a reason and are never charged. Route-baseline samples appear as rowType routeSample with their prices so a cross-date verdict can be checked by hand, and a single rowType summary row closes every run.

# 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": "LAX",
    "destination": "JFK"
};

// Run the Actor and wait for it to finish
const run = await client.actor("frameprobe/flight-fare-history-api").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": "LAX",
    "destination": "JFK",
}

# Run the Actor and wait for it to finish
run = client.actor("frameprobe/flight-fare-history-api").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": "LAX",
  "destination": "JFK"
}' |
apify call frameprobe/flight-fare-history-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,frameprobe/flight-fare-history-api"
        }
    }
}

```

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/G0DDyAJqOHgI8yhgh/builds/v2HTY5UqF6iNs8rsC/openapi.json
