Flight Fare History API: book-now / wait from your own history
Pricing
from $5.00 / 1,000 fare observation stored and scoreds
Flight Fare History API: book-now / wait from your own history
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.
Pricing
from $5.00 / 1,000 fare observation stored and scoreds
Rating
0.0
(0)
Developer
FrameProbe
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
17 hours ago
Last modified
Categories
Share
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" }
{"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.
originanddestination, 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"] }
{"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.
routeBaselineDatesdefaults 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.mediumneeds all six baseline dates to return a price, which the default upstream managed on four of four measured first runs. A less reliable scraper yieldslow, 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 sevendepartDatesyourself and the cross-section is free. - Nothing is bought when your history is already long enough to answer, when you passed a
datasetId, or whenrouteBaselineDatesis0. - 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. Readbasisbefore acting onverdict. - 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
BASELINErather than dressing them up. - Three prior observations before a
historyverdict, five beforeBOOK_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
bestfares 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.