# Amazon Price Tracker: Watch a List of ASINs Over Time (`amazonscrapers/amazon-price-tracker`) Actor

Track what a fixed list of Amazon products costs, run after run. Price, list price, discount, coupon, deal badge and stock, with a timestamp on every row so a schedule becomes a price history. 19 marketplaces. You only pay for rows that arrive with a price.

- **URL**: https://apify.com/amazonscrapers/amazon-price-tracker.md
- **Developed by:** [Amazon Scrapers](https://apify.com/amazonscrapers) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 products with a prices

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Amazon price tracker: watch a fixed list of ASINs over time

Give it the products you care about. Get back what each one costs right now, what
it used to cost, what discount is running, and a timestamp on every row, so a
schedule turns into a price history you own.

There is a price tracker on this Store already and it does not track prices. It
takes a keyword and a country and hands back search results. No fixed list, no
history, no change over time. This one is the other thing: you give it the ASINs,
it reads the same ASINs tomorrow, and the dataset grows into a record.

Eighteen fields per product, nineteen marketplaces, and a price that is never
guessed.

### Read this before your first run

Amazon does not hand out prices everywhere. The price this Actor reports comes
from the buy box, and on several marketplaces Amazon leaves the buy box out of
the page entirely when it cannot work out where to deliver.

Measured on 2026-08-24, share of products that came back with a price:

| Marketplace | Products priced |
| --- | --- |
| amazon.com | 57 of 60 |
| amazon.ca | 5 of 5 |
| amazon.co.uk | 5 of 6 |
| amazon.fr | 4 of 5 |
| amazon.it | 1 of 5 |
| amazon.es | 1 of 5 |
| amazon.de | 0 of 8 |
| amazon.com.au | 0 of 5 |

If your marketplace is at the bottom of that table this Actor has little to tell
you, and it will not pretend otherwise: those rows arrive without a price and
without a charge. Five approaches were tried to get German prices out, including
the shop's own language, matching country addresses, locale cookies and the
mobile page. None of them worked, and one of them made everything else worse.

Run twenty products on your own marketplace first. It costs nothing when the
prices are not there.

### It will not guess a price

Amazon puts up to nine price-shaped numbers on a product page: the subscribe
option, a used offer, a struck-through list price, a per-unit price, a bundle,
a Prime upsell that shows two different amounts in the same block.

Reading the markup and picking one gets it wrong more often than right. Measured
on this engine: prices derived from the markup were correct zero times out of
six. Prices taken from the buy box data Amazon itself uses to render the page
were correct nine times out of nine.

One concrete case from that measurement: a product selling for $16.99 came back
as $11.70 from the markup, because the cheapest visible number belonged to a used
offer three blocks down. On a price tracker that is not a small error. You would
have repriced against a number nobody was charging.

So the buy box data is the only source, `priceSource` records it on every row,
and when that data is absent you get no price rather than a wrong one. The row
still arrives with the stock status, the title and the timestamp.

**You are not charged for a row without a price.**

### Every field it returns

Eighteen per product, and eleven of them are about the price. That is the point:
this Actor goes narrow and deep rather than wide.

#### Price and offer

| Field | What it holds |
| --- | --- |
| `price` | Current price from Amazon's own buy box data **(this is what you are charged for)** |
| `listPrice` | The struck-through price, when Amazon shows one |
| `savingsAmount` | How much is off in money |
| `discountPercent` | How much is off as a percentage |
| `currency` | Three-letter code, from the page itself |
| `pricePerUnit` | Price per count, ounce, litre, whatever Amazon uses |
| `priceSource` | Where the price came from, so you can audit it |
| `isDiscounted` | Whether it is currently below list price |
| `coupon` | The clip-coupon text, when there is one |
| `dealBadge` | Lightning deal, limited time deal, and similar |
| `hasSubscribeAndSave` | Whether a subscription price exists |

#### Stock and delivery

| Field | What it holds |
| --- | --- |
| `inStock` | True, false, or null when Amazon does not say |
| `availability` | Amazon's own availability sentence |

#### Identity and catalogue

| Field | What it holds |
| --- | --- |
| `asin` | The product id |
| `title` | Full product title, shortened only past 1000 characters |
| `marketplace` | Which shop this row came from |
| `url` | The page this was read from |
| `scrapedAt` | Timestamp of the read |

### A real row

```
asin                 B0DXXYS4BJ
title                Roku Streaming Stick HD with Voice Remote
price                29.99
currency             USD
priceSource          buybox-data
listPrice            39.99
savingsAmount        10.00
discountPercent      25
isDiscounted         true
coupon               null
dealBadge            Limited time deal
pricePerUnit         null
hasSubscribeAndSave  false
inStock              true
availability         In Stock
marketplace          com
scrapedAt            2026-08-26T09:14:22.031Z
```

Twenty-five percent off with a limited time deal running, and `priceSource` says
the number came from Amazon's own buy box data rather than from anywhere else on
the page. That last field is what lets you audit a surprising price instead of
wondering about it.

### Turning it into a history

Run it once and you have today's prices. Set it to run daily from the Schedules
tab, point every run at the same named dataset, and the rows accumulate. Sorting
by `asin` and `scrapedAt` gives you the price line per product.

Nothing here computes a delta for you, and that is deliberate. Which comparison
matters depends on your question: yesterday, last week, the lowest ever, the
price before the coupon. A dataset with honest timestamps answers all of them. A
"price changed" flag whose rules you cannot inspect answers one, badly.

For the same reason there is no alerting built in. Put a webhook on the run and
compare the last two rows for an ASIN in your own code. Keeping the comparison
outside means you can change the rule without redeploying anything.

### What to put in

**ASINs or product URLs.** This is the main way in. Paste them in any shape: a
bare `B08N5WRWNW`, a full URL with tracking parameters, a spreadsheet cell that
still says `ASIN: B08N5WRWNW`, quotes or a trailing full stop around it.
Duplicates are removed across everything you give it, so a product listed twice
is fetched once and billed once.

**A search term or a category link**, if you want to establish which products to
track before you start tracking them. Most people run a category once, take the
ASINs, and track those from then on.

The list is yours and it stays yours. This Actor never expands it on your behalf,
never follows a variant family, never adds the products Amazon suggests. Every
one of those would quietly multiply what you are billed.

### Settings worth knowing about

| Setting | What it does |
| --- | --- |
| `marketplace` | One of nineteen, from `com` to `co.jp`. The currency follows the shop. |
| `proxyCountry` | Leave empty and it follows the marketplace. Worth setting by hand only when you want the price as a shopper in a specific country sees it. |
| `maxItems` | Hard ceiling on delivered products. Zero means no ceiling. |
| `maxItemsPerList` | How many products to take from each search term or category link. |
| `maxListPages` | How deep to walk each list. |
| `lanes` | How many products are read at once, each on its own exit address. Eight by default. Sixteen roughly halves the run time and costs a few prices, because Amazon strips the buy box sooner when you push. On a price tracker that trade is usually not worth it. |
| `maxAttempts` | Attempts per product, each from a different address. Twelve by default. |
| `residentialFallback` | Off by default. Retries through residential addresses after half the attempts have failed. Measured 2026-08-26: on a sixty product run it cost roughly six times as much and returned two more prices. Leave it off unless a marketplace is refusing everything. |
| `economical` | Hangs up on each page once the price block has arrived. Off by default; measured across three pairs of runs it saves nothing, because hanging up mid-page produces aborted connections that come back as retries. |

### How it works, in plain terms

Amazon refuses roughly six requests in ten to an address it has not seen before.
For a tracker that is worse than for a one-off report: a refused product is not a
missing row, it is a hole in a time series that you cannot fill in later.

So every product gets up to twelve attempts, each from a different exit address,
and the same address is never used twice in a row. Addresses that work are
remembered for the rest of the run and tried first. Addresses that keep getting
refused are dropped.

The pace adapts. When refusals go up the runner slows down instead of pushing
harder, because pushing harder is how an entire run gets locked out for an hour.
Anything still refused at the end goes into a second pass at half speed using
only the addresses that already proved themselves.

There is one more rule specific to prices. A page that came back complete but
without a price gets retried too, up to a point, because a missing buy box is
often a page variant rather than a fact. After five complete pages in a row
without a price it stops asking: that is Amazon telling you something, not
Amazon being difficult.

### Reading the run log

Every run ends with a summary:

```
Done: 16 charged (+4 delivered without a price, free), 0 refused by Amazon, 0 other. 864 KB per charged result, 167 attempts in total.
Exit addresses: 123 used, 2 proven good, 12 burned.
Attempts by outcome: {"refused by Amazon":108,"page did not carry the field this Actor needs":38,"http_500":3,"ok":16,"delivered, product unavailable":1,"network error":1}
```

`charged` is what you pay for. The bracketed number arrived complete but without
a price and cost you nothing. On amazon.com the second number is usually small;
if it is most of your run, check the marketplace table at the top of this page.

If a run ever delivers rows and charges for none of them, the log says so in
capitals at the bottom.

### Reliability, measured

Three consecutive runs of twelve products on 26 August 2026, at the default
settings:

```
run 1   11 of 12 delivered, 9 with a price
run 2   12 of 12 delivered, 10 with a price
run 3   11 of 12 delivered, 10 with a price
```

And one run of sixty products the same day: 60 delivered,
53 with a price, in 10.0 minutes.

Worth reading out of that: the longer run has the better delivery rate,
100 percent against 94, and that is not luck. The engine spends the first
products of a run working out which exit addresses Amazon is tolerating today, so
a short run pays the whole warm-up over fewer products. If delivery matters more
to you than turnaround, batch your work into larger runs.

The second is the residential switch, and the honest version is less dramatic
than it sounds. Measured 26 August on the same sixty products, twice: with the
switch off, fifty-one came back with the field this Actor charges on, for $0.0205
of platform usage. With it on, fifty-three came back, for $0.1192. Two more
products for roughly six times the cost.

That is why it is off by default. It is in your input form, so if a marketplace
is refusing almost everything you can turn it on and pay for the difference.

### What you can build with it

**A repricing input.** Daily prices on the products you compete with, with
`listPrice` and `discountPercent` so you can tell a real cut from a struck-through
number that never moves.

**A promotion calendar.** `dealBadge` and `coupon` over a month show when a
competitor runs promotions, and how deep.

**A price floor alarm.** Compare today against the lowest `price` you have on
record for that ASIN. The record is your dataset, so the rule is yours.

**A supplier check.** If you buy to resell, tracking the source listing tells you
when your margin is about to change.

**A subscription-price watch.** `hasSubscribeAndSave` flags listings where a
lower recurring price sits next to the one you are reading, which is a different
competitive position from a straight discount.

### Working it into your stack

Every run writes to a dataset you can download as CSV, JSON, Excel or XML, or
pull through the API. For a tracker the API matters more than the download: point
repeated runs at one named dataset and query it as a table.

Schedules take a cron expression. Webhooks fire on completion. The output is
flat, so it loads into a spreadsheet or a database without unpacking anything.

### Marketplaces

Nineteen, from amazon.com to amazon.co.jp, read in their own language. A German
rating saying "4,5 von 5 Sternen" and a Japanese one putting the number last both
come back as 4.5, and a German "Derzeit nicht verfügbar" comes back as
`inStock: false` rather than as an empty field.

Prices are the one thing that does not travel everywhere, and the table at the
top of this page is the honest picture.

One more thing worth knowing: `currency` reports what Amazon actually quoted, not
what the marketplace usually uses. amazon.ca quotes US dollars to a visitor it
places in the United States. A tracker that silently relabelled that as Canadian
dollars would give you a price history with a thirty percent step in it that
never happened.

### Pricing

Four dollars per thousand products with a price, plus three cents to start a run.

Watching fifty products daily costs twenty-three cents a day: fifty times four
tenths of a cent, plus the three cent start fee. You are not
charged for a row without a price or for a product that never arrived, and
platform usage is included, so there is no separate compute or proxy bill.

Because you are only billed for prices, a run against a marketplace that does not
give them costs you three cents and tells you so.

### The rest of this family

Eleven Actors on one engine. For a tracker the useful neighbours are:

- **Amazon Stock Checker** when the question is availability rather than price,
  and you want to be billed on the stock answer instead.
- **Amazon Offer Checker** when you need to know how many sellers are on the
  listing and whether a used offer is dragging the price picture around.
- **Amazon BSR Tracker** for rank over time, which moves days before price does.
- **Amazon Product Scraper** when you want all sixty-four fields rather than the
  eighteen here.

### Questions people ask

**How often should I run it.** Daily is the usual cadence. Amazon changes prices
more often than that, but for most decisions a daily point is enough, and hourly
runs mostly buy you noise plus a bigger bill.

**Why is listPrice often empty.** Because Amazon only shows a struck-through
price when there is a discount to show. An empty `listPrice` with a filled
`price` means the product is at its normal price.

**What is priceSource for.** Auditing. When a price surprises you, that field
tells you it came from Amazon's own data rather than from somewhere on the page
that might have belonged to a different offer.

**Can it tell me the price history before today.** No. Nobody can: Amazon does
not publish it. The history starts the first day you run this.

**Can I track products across several marketplaces at once.** One marketplace per
run, but you can schedule one run per marketplace into the same dataset. Every
row carries `marketplace`, so they stay apart.

**How many products can it track.** No built-in ceiling. Sixty took 10.0 minutes on
the measured run above, so fifty is around 8.

### What it does not collect

No review text, no reviewer names, no customer questions, no seller names. Those
are personal data, and scraping them out of a public website is almost always a
GDPR problem in Europe regardless of how public the page is.

### Legal note

This Actor reads public pages and takes product facts: prices, discounts,
availability. Business information about an item, not information about a person.

You are responsible for what you do with the output. Check Amazon's terms for
your own use case.

# Actor input Schema

## `productUrls` (type: `array`):

Amazon product URLs or bare ASINs, one per line. Leave empty if you are using Search instead. Duplicates are removed so you are never charged twice.

## `search` (type: `array`):

Anything that lists products: a plain search term ("tower fan"), an Amazon search URL, a category link, or a bestsellers page. Each one is crawled and every product found is scraped. Use this when you do not know the ASINs up front.

## `marketplace` (type: `string`):

Which Amazon site to read. The currency of the price follows the marketplace and the proxy country.

## `proxyCountry` (type: `string`):

Which country the requests should appear to come from. Leave empty and it follows the marketplace: amazon.de is read from Germany, amazon.co.uk from the UK. This matters more than it looks -- Amazon shows different prices, currencies and even availability depending on where you appear to be.

## `maxItems` (type: `integer`):

Hard ceiling on delivered products. 0 means no ceiling: the Actor keeps going until the lists run out or your limits stop it.

## `maxItemsPerList` (type: `integer`):

Products to take from each search term or category link. Up to 10000.

## `proxyConfiguration` (type: `object`):

Datacenter proxies were enough in testing: 30 out of 30 products returned with zero blocks. They are the cheaper option and the default. Switch to residential if you start seeing blocked pages at higher volume. Running without any proxy stops after roughly 85-120 products, because Amazon closes the door on a single IP address for over an hour.

## `lanes` (type: `integer`):

How many products to read at the same time. Each lane uses its own exit address, so more lanes never means asking one address more often. Eight is the tested default and the most accurate. Sixteen finishes the same sixty products in about half the time, but measured on 2026-08-24 it returned six more products without a price, because Amazon strips the buy box sooner under pressure. Raise it when speed matters more than the last few prices.

## `maxAttempts` (type: `integer`):

How often to retry a product Amazon refuses, each time from a different exit IP. A refusal is a property of one address, not of the product, and a refused attempt costs about 4 KB. Twelve tries is cheap insurance against a run that comes back short.

## `residentialFallback` (type: `boolean`):

After half the attempts have failed, retry through residential addresses instead of datacenter ones. Measured 2026-08-26: residential transfer was 90 percent of the bill on a sixty product run ($0.150 of $0.167) while a run that never touched it delivered all sixty anyway. Leave it off unless you are losing products; turn it on when a marketplace refuses everything.

## `economical` (type: `boolean`):

Hang up on each product page once the price is in, instead of reading it to the end. Roughly three times less traffic and about three times faster. Measured cost: over 30 products this lost 2 ratings, 4 review counts and 1 brand, because Amazon sometimes puts the rating below the price. Prices were identical either way. Leave this off unless you only need prices and are paying per gigabyte.

## `maxListPages` (type: `integer`):

Result pages to walk per list. Up to 100. Amazon itself stops earlier on most searches.

## `minDelayMs` (type: `integer`):

Pause between products. Measured 2026-08-23: at 1-2 seconds this returned 10 of 15 products; at 8-15 seconds it returned 12 of 15 on the same list. Pace turned out to matter more than anything else. The market leader waits about 14 seconds per product.

## `maxDelayMs` (type: `integer`):

Upper bound for the pause between products. The actual pause is random between the minimum and this value, which looks less mechanical than a fixed interval.

## `retryPauseMs` (type: `integer`):

How long to wait before trying a refused product again. Kept short because the retry already switches to a different exit IP, and waiting longer only makes the run more expensive.

## Actor input object example

```json
{
  "productUrls": [
    "B09B8V1LZ3",
    "https://www.amazon.com/dp/B0DXXYS4BJ"
  ],
  "search": [],
  "marketplace": "com",
  "maxItems": 0,
  "maxItemsPerList": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "lanes": 8,
  "maxAttempts": 12,
  "residentialFallback": false,
  "economical": false,
  "maxListPages": 5,
  "minDelayMs": 8000,
  "maxDelayMs": 15000,
  "retryPauseMs": 300
}
```

# Actor output Schema

## `products` (type: `string`):

One row per product per run, stamped with the moment it was read. Repeated runs into one dataset become a price history.

## `summary` (type: `string`):

Counts per outcome, exit addresses used, bytes per charged result.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "productUrls": [
        "B09B8V1LZ3",
        "https://www.amazon.com/dp/B0DXXYS4BJ"
    ],
    "search": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("amazonscrapers/amazon-price-tracker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "productUrls": [
        "B09B8V1LZ3",
        "https://www.amazon.com/dp/B0DXXYS4BJ",
    ],
    "search": [],
}

# Run the Actor and wait for it to finish
run = client.actor("amazonscrapers/amazon-price-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "productUrls": [
    "B09B8V1LZ3",
    "https://www.amazon.com/dp/B0DXXYS4BJ"
  ],
  "search": []
}' |
apify call amazonscrapers/amazon-price-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,amazonscrapers/amazon-price-tracker"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/09MAtGFI8jWM5JUki/builds/Pf9XloJpNe3H5vdPe/openapi.json
