Tutorial

Price history APIs: what they are and how to use them

What they return, what a request costs, and how to turn the points into a normal selling price.

Published

What a price history API returns

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:

  • Is today’s price actually low for this product, or is the “sale” its normal price?
  • What did an item cost on the date it was lost, for an insurance claim?
  • How often does a competitor change the price of a product, and by how much?
  • What should a trade-in offer be, given what the item has sold for new over the last few months?

Making a request

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:

FieldWhat it holds
timestampWhen ShopSavvy saw the change, as an ISO 8601 date and time.
priceThe offer’s price at that time, always a number.
currencyThe ISO 4217 currency of that price; null on the rare point with no recorded currency, so never assume US dollars from a missing value.
availabilityin 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.

What a history request costs

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.

RequestCredits
1 product, 90 days1 × 3 + 90 = 93
10 products in one request, 90 days10 × 3 + 90 = 120
10 products in ten separate requests, 90 days10 × 93 = 930

So batch your identifiers: the same history for 10 products costs 120 credits in one request and 930 in ten.

Computing a normal selling price

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.

Pitfalls to plan for

  • Gaps are not flat prices. A period with no points means nothing new was seen, and the further back you go the sparser the observations get.
  • Some pages show two prices, such as a subscribe-and-save price and the one-time price, and a history can flip between them. Medians over a window absorb this; a single minimum does not.
  • Compare like with like: new condition, the same pack size, and one currency. Marketplace sellers can price far from the retailer itself.
  • A very new product has little history; say so in your interface instead of showing a deal badge.

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.

Frequently asked questions

How far back does price history go?

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.

Can I get price history for many products at once?

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.

Why is the history empty for some offers?

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.

Is a price history API the same as a price tracker?

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.