Home / Price History API
Price History API
Append-only observations, time-weighted percentiles, and the one asset in price data that money cannot buy.
Price history is the record of what a product has cost, per seller, over time. It is the hardest thing in commerce data to acquire because it only accumulates forward — nobody can sell you the history of a product they were not watching last year. That property makes it the one genuinely defensible asset in the category.
Store observations, not prices
A price is a mutable current-state field. An observation is an immutable fact: this source reported this value at this instant. Store observations and current price becomes a view over them. Store prices and you have thrown away the thing you were trying to build.
Each observation should carry two timestamps — when the source said it, and when you learned it. Those differ, sometimes materially, and anything time-sensitive needs both.
The series is a change log
An unchanged reading should still count as a poll — it informs how often to look again — but should write nothing. Done properly, three rows over ninety days does not mean you looked three times. It means the price changed twice.
{ "series": [
{ "observed_at": "2026-06-09T13:21:15Z", "price_minor": 39800 },
{ "observed_at": "2026-08-21T02:10:44Z", "price_minor": 31999 },
{ "observed_at": "2026-09-06T14:21:59Z", "price_minor": 19800 } ] }
// $398.00 held 73 days. $319.99 held 16. $198.00 is one day old.Time-weight everything, or your statistics lie
This is the subtlety that breaks most implementations. Because unchanged readings write no row, a naive average over rows weights a one-day clearance exactly the same as a quarter of stability. Every statistic has to be weighted by how long each price actually held.
Ties need handling too. A listing that has never moved sits at both its own low and its own high — midrank puts it at the fiftieth percentile, which is the honest answer. Without it, a flat price reads as either the best deal ever recorded or the worst, depending on which way the comparison happens to fall.
Never pool sellers
Compare a seller's price against that seller's own history. Pooling every retailer's series into one distribution makes the cheapest seller look merely average, because the pool contains the expensive sellers' prices too. The percentile that means something is the one scoped to where you would actually buy.
What history is actually for
It converts a reading into a judgement. “$198.00” is a fact any scraper returns. “$198.00, the fourth percentile of eighty-nine days at this retailer, lowest since March” is a decision — and it is what a shopper, or an agent acting for one, actually needs.
It is also the honest basis for a discount claim. Percent-off depends on a reference price the seller chooses; percentile depends on what the product has actually cost, which it does not.
The strategic consequence
Every other component of a price product can be built in a quarter — collection, matching, alerting, an API. History cannot be built at all; it can only be started. A competitor who decides to enter tomorrow has ninety days of history in ninety days, and no amount of funding compresses that.
Which means the correct time to start collecting is before you need it. If you are evaluating vendors, start a candidate collecting on your priority products in parallel now, so the data exists when the decision arrives.
Segments, not rows: reconstructing what a price actually was
The mechanics matter because the naive implementation is both obvious and wrong. You do not store one row per hour — you store a row when something changes. So a price that held sixty days is one row and a price that flickered for an afternoon is three, and any statistic counting rows has concluded the afternoon mattered twenty times more than the two months.
# observations -> [(price, start, end)] segments over the window segments = to_segments(observations, window_start, window_end) total = sum(s.end - s.start for s in segments) below = sum(s.end - s.start for s in segments if s.price < current) equal = sum(s.end - s.start for s in segments if s.price == current) # midrank: half the time spent AT the current price counts as below. # Without this term a flat price reports as both 0th and 100th percentile. percentile = 100 * (below + 0.5 * equal) / total
The midrank term is the part that gets left out, and leaving it out produces the most confusing output a price tool can give. A product whose price has never moved is simultaneously at its lowest ever and its highest ever, and which one you report depends entirely on whether you wrote a strict or non-strict comparison. With midrank it correctly reports as the middle of its own distribution — and the honest output there is not a percentile at all but a verdict saying the price has not moved.
Windows change the answer, so state the window
A percentile is meaningless without the window it was computed over, and the same price can sit at the 5th percentile over 30 days and the 60th over a year. Neither is wrong. A system reporting a percentile without its window, or silently picking one, is hiding the parameter that most determines the answer.
| Window | Answers | Misleads when |
|---|---|---|
| 30 days | Is this good right now? | The product is seasonal |
| 90 days | Is this good this quarter? | A generation change happened mid-window |
| 365 days | Is this good historically? | The product is only months old |
| Since first seen | Everything we know | Coverage started recently — say so |
The last row requires honesty. A percentile computed over three weeks of observation is not a percentile against the product's behaviour, and presenting it as one is the most common overstatement in this category. Return the observation count and the window start alongside every verdict so a consumer can judge whether to trust it.
Questions
Common questions
Can price history be backfilled?
No. It accumulates forward only. A provider offering deep history on a product they began tracking recently cannot have it, and no archive substitutes — nobody can verify observations they did not make.
How far back should price history go to be useful?
Ninety days is enough for percentile judgements on most products. A full year captures seasonality, which matters in categories with an annual cycle like outdoor and toys.
Why are there so few rows in a long price history?
Because unchanged readings are deduplicated. The series records changes, not polls — which is why any statistic computed from it must be weighted by duration rather than by row count.
Is percent-off a good measure of a discount?
No, because the reference price is chosen by the seller and is frequently inflated. Percentile against the product's own history at that seller is defensible and reproducible.
Related
Amazon Price Monitoring & Price History
How Amazon pricing data works in 2026: PA-API's deprecation, what replaced it, why scraping fails at scale, and how to get price history through an API.
Price Monitoring Software
A technical guide to competitor price monitoring: how matching fails silently, why cadence should be earned, and how to evaluate vendors on what they can prove.
Product Data API
Measured field coverage across 1,993 products and 11,265 variants in nine industries: where GTINs exist, and which fields you cannot rely on.