What they return, what a request costs, and how to turn the points into a normal selling price.
Published
A price history API returns dated prices for a product at each retailer that sells it: the price an offer had, and when that price was seen. It is the data behind a price chart, available to your own code.
Teams use it to answer questions a current price cannot:
In the ShopSavvy Data API, history comes from the offers endpoint with a date window. Send one or more identifiers (up to 100 per request, comma-separated in ids) and start and end dates as YYYY-MM-DD. A window can span at most 366 days; ask for longer histories in successive calls.
90 days of history for one product by its UPC:
curl "https://api.shopsavvy.com/v1/products/offers/history?ids=611247373064&start=2026-07-14&end=2026-10-11" \
-H "Authorization: Bearer $SHOPSAVVY_API_KEY"The response has one entry per product found, with the same product fields as a lookup and an offers array. Each offer carries a history array of points, newest first:
| Field | What it holds |
|---|---|
| timestamp | When ShopSavvy saw the change, as an ISO 8601 date and time. |
| price | The offer’s price at that time, always a number. |
| currency | The ISO 4217 currency of that price; null on the rare point with no recorded currency, so never assume US dollars from a missing value. |
| availability | in or out, only when it was confirmed at the time. |
Points are quality-filtered: you get the series ShopSavvy’s own price chart draws, from verified retailers or listings validation confirmed are the same product, inside each retailer’s outlier band. eBay listings never carry history.
History costs the offer price per product (3 credits at every retailer, 2 at one retailer) plus 1 credit per day of the window. The per-day part is charged once per request, not per product, and nothing is charged when no offers are found.
| Request | Credits |
|---|---|
| 1 product, 90 days | 1 × 3 + 90 = 93 |
| 10 products in one request, 90 days | 10 × 3 + 90 = 120 |
| 10 products in ten separate requests, 90 days | 10 × 93 = 930 |
So batch your identifiers: the same history for 10 products costs 120 credits in one request and 930 in ten.
History points are changes: a price holds until the next point. To get a product’s usual price, rebuild each offer’s price for every day of the window, take the lowest new-condition price each day, and use the median of those daily lows. The median is the right summary here because it ignores a single odd day, where a minimum would not.
Works on the offers array of one product from the response above:
type HistoryPoint = { timestamp: string; price: number; currency: string | null }
type Offer = { condition?: string; currency: string; price: number; history: HistoryPoint[] }
// An offer's price on a day: its last history point on or before that day.
// History points are changes, so a price holds until the next point.
function priceOn(history: HistoryPoint[], day: string): number | null {
let price: number | null = null
for (const point of [...history].sort((a, b) => a.timestamp.localeCompare(b.timestamp))) {
if (point.timestamp.slice(0, 10) > day) break
if (point.currency === 'USD') price = point.price
}
return price
}
// The median, over every day of the window, of that day's lowest new-condition price.
function normalSellingPrice(offers: Offer[], start: string, end: string): number | null {
const usable = offers.filter((offer) => !offer.condition || offer.condition === 'new')
const lows: number[] = []
for (let date = new Date(start + 'T00:00:00Z'); date <= new Date(end + 'T00:00:00Z'); date.setUTCDate(date.getUTCDate() + 1)) {
const day = date.toISOString().slice(0, 10)
const prices = usable.map((offer) => priceOn(offer.history, day)).filter((price): price is number => price !== null)
if (prices.length > 0) lows.push(Math.min(...prices))
}
if (lows.length === 0) return null
lows.sort((a, b) => a - b)
const middle = Math.floor(lows.length / 2)
return lows.length % 2 === 1 ? lows[middle] : (lows[middle - 1] + lows[middle]) / 2
}
// Is today's lowest new price at least 10% under the usual price?
function isDeal(offers: Offer[], start: string, end: string): boolean {
const usual = normalSellingPrice(offers, start, end)
const current = offers.filter((offer) => (!offer.condition || offer.condition === 'new') && offer.currency === 'USD').map((offer) => offer.price)
return usual !== null && current.length > 0 && Math.min(...current) <= usual * 0.9
}A 10% threshold is a starting point; tune it by category. Electronics often move in larger steps than groceries.
ShopSavvy’s archive holds price history back to 2011, dense from 2019; density still varies by product and retailer, and history only contains prices that were actually observed.
ShopSavvy’s archive holds price history back to 2011, dense from 2019. One request can cover up to 366 days; ask for older windows in successive requests.
Yes. Send up to 100 identifiers in one request. The per-day charge applies once per request, so 10 products over 90 days cost 120 credits in one request instead of 930 in ten.
eBay listings never carry history, a listing may have no points that passed the quality filter, and a product ShopSavvy only started seeing recently has little to show yet.
A price tracker watches products for you and alerts you when prices change. A price history API gives your own software the dated prices, so you can build charts, deal checks, valuations or alerts yourself.