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

> How a price history API works: a real request, the fields you get back, what it costs, and how to compute a product’s normal selling price to tell a real deal.

*Source: https://shopsavvy.com/learn/price-history-api · Published 2026-10-12*

---

## 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:

```bash
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.

## 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.

| 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.

## 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:

```typescript
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.

## Related

- [Product data API guide](https://shopsavvy.com/learn/product-data-api-guide)
- [Price history in the Data API documentation](https://shopsavvy.com/data/documentation)
- [Product data API cost calculator](https://shopsavvy.com/tools/product-data-api-cost-calculator)
- [Price-comparison and deal apps](https://shopsavvy.com/data/for-price-comparison-apps)
- [Insurance and claims valuation](https://shopsavvy.com/data/for-insurance)
- [Data API playground](https://shopsavvy.com/data/playground)
