# Amazon Bestseller Rank Scraper - Prices, ASIN, Product Data (`snow_leo_data/amazon-product-scraper`) Actor

ASIN lookup and Amazon product scraper past the 306-product search ceiling: 691 unique products where the same search returns 61, measured. Prices, Best Sellers Rank, duplicates removed by ASIN after a measured reshuffle between identical queries. Amazon best sellers list.

- **URL**: https://apify.com/snow\_leo\_data/amazon-product-scraper.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** E-commerce, MCP servers
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.50 / 1,000 products

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Amazon Scraper - Product Data, Prices, ASIN Lookup, Bestsellers

Collect product data from **amazon.com** — search results, product pages and
Best Sellers lists — without the Amazon API and without a browser. Prices, list
prices and discounts, ratings and rating counts, stock, seller, bullet points,
specifications, images, badges, Best Sellers Rank and ASINs.

**A residential proxy is required and is on by default.** Measured on
18 September 2026: from Apify's cloud addresses Amazon answered 503 to every
request and no page could be read; through a residential address the same
search returned a full page of products. The request fingerprint matters as
much as the address — on the same home connection a Chrome fingerprint got 503
while a Firefox one returned 1 017 218 bytes with 28 product cards.

Every number on this page was measured, and the command that reproduces it is
named next to it. Nothing here is an estimate.

***

### What it does that a plain search cannot

**One Amazon search stops at 306 products.** Measured on `wireless earbuds`:
16 organic products per page, pages 1, 2, 3, 5, 7, 8, 10, 15 each returned 16,
page 20 returned 2 and there was no page 21 — while Amazon's own header said
"over 20,000 results". 306 is the whole window, not a page limit.

**Deep scan walks 11 price bands instead of one search**, and each band is its
own window with its own ceiling. Measured: six price bands returned 96 products
of which **96 were unique — zero overlap**. Four sort orders returned 64
products of which 64 were unique. Bands and sort orders together returned 160
rows with **155 unique products from first pages alone**, against 16 from a
plain first page.

**Two identical requests do not return the same products.** Measured: the same
first page fetched twice shared only 13 of 16 products — Amazon reshuffles. The
Actor de-duplicates by ASIN across the whole run and counts the repeats in the
REPORT record, so you are never charged twice for one product.

**An unknown department is refused instead of being quietly ignored.**
Measured: `https://www.amazon.com/s?k=earbuds&i=zzqqnotadept` answers **HTTP 200
with ordinary unfiltered results — 14 of its 16 products were identical to the
search with no department at all**. A scraper that passes your typo through
hands you "products from department X" that are not from department X. The
`department` field is a closed list of Amazon's 58 departments, and a department
hidden inside a search URL you paste (`&i=...`) is checked against the same list
and stops the run before the first request with a named reason.

***

### Input

| Field | What it does |
|---|---|
| `searchTerms` | Keywords to search on amazon.com |
| `startUrls` | Product pages, search URLs, Best Sellers URLs, or bare ASINs |
| `bestsellerCategories` | Best Sellers nodes, e.g. `electronics`, `toys-and-games` |
| `department` | One of Amazon's 58 departments; an unknown one in a pasted URL stops the run |
| `minPrice`, `maxPrice` | Sent to Amazon as a price filter, not applied afterwards |
| `sortBy` | Amazon's own order: price up, price down, rating, newest, best sellers |
| `deepScan` | Walk 11 price bands to break the 306-product ceiling |
| `maxPagesPerSearch` | 1–20; 16 organic products per page |
| `maxItems` | Limits what is **written**, not what is crawled |
| `fetchProductDetails` | Open every product page: brand, seller, stock, bullets, specs, images, Best Sellers Rank |
| `minRating`, `minReviews` | Products with no rating at all are kept |
| `onlyDiscounted`, `onlyInStock`, `titleContains`, `brandIs` | Filters |
| `onlyChanged`, `emitUnchanged` | Monitor mode |
| `compactOutput`, `dropEmptyFields` | 15 columns instead of 40; drop empty keys |
| `requestDelaySeconds`, `requestTimeoutSeconds` | Politeness and patience |

`maxItems` limits the **output**. If a filter drops rows, the crawl keeps going
until that many rows have actually been delivered — it does not stop at that
many rows *collected* and then hand you an empty dataset.

### Output

40 columns. Measured fill rate on one live search page (16 products):

| field | filled |
|---|---|
| `asin`, `title`, `url`, `image`, `rating`, `reviewsCount`, `deliveryText` | 16/16 |
| `price`, `priceCurrency`, `listPrice`, `discountPercent` | 15/16 |
| `boughtInPastMonth` | 14/16 |
| `badge` (Amazon's Choice / Best Seller) | 3/16 |

Measured fill rate on six live product pages:

| field | filled |
|---|---|
| `title`, `brand`, `rating`, `reviewsCount`, `availability`, `features`, `images`, `starsBreakdown` | 6/6 |
| `price`, `specifications`, `deliveryText` | 5/6 — the sixth is `Currently unavailable`, and that is what the row says |
| `seller`, `bestSellersRank`, `breadcrumbs`, `variantAsins` | 3/6 — Amazon does not print them on every page |
| `boughtInPastMonth` | 2/6 |
| `description` | 1/6 — the long description block is empty on five of the six |

Best Sellers lists give 30 positions per page and 2 pages per category, so
**60 ranked products** with `bestsellerRank` 1..60.

Empty numeric fields are `null`, never `""`, so the CSV loads into Excel,
BigQuery and pandas without a type error.

### Monitor mode

`onlyChanged` compares every product against what earlier runs delivered and
writes only those whose **price, list price, stock, rating, rating count or best
seller rank** moved. Each row is labelled `NEW`, `UPDATED` or `UNCHANGED`.

A field Amazon simply stopped printing is **not** counted as a change — that
would bill you for the whole catalogue every morning. Memory lives in a named
key-value store, so it survives between runs.

### When the dataset is empty, the Actor says why

A green run with no rows is the worst thing a scraper can do to you. Every empty
result here carries an `empty_reason` in the REPORT record and a warning in the
log:

`nothing-asked`, `department-not-recognised`, `price-range-empty`,
`rating-filter-impossible`, `charge-limit-reached`, `source-returned-nothing`,
`filtered-out`, `nothing-changed`, `nothing-collected`, `source-blocked`.

The first 5 are settled **before a single request is sent**, so you are not
charged for downloading pages that the next line of code would have thrown away.
`source-blocked` exists because a block page arrives with code 200 and no
products, and calling that "nothing found" would be a lie.

### Honest limitations

- **amazon.com only.** Measured from the Apify cloud on two separate attempts:
  `amazon.co.uk`, `amazon.de`, `amazon.ca` and `amazon.in` answer **503** to
  every request. Competitors that list many marketplaces reach them another way;
  this Actor does not pretend to.
- **No customer reviews.** `https://www.amazon.com/product-reviews/<ASIN>/`
  returns 221 KB with not one review body in it — Amazon puts reviews behind a
  sign-in. If you need review text, use a review scraper.
- **No offer listings.** `/gp/offer-listing/` returns a page with no offers in
  the markup.
- **Only organic results.** Sponsored placements are skipped on purpose, so no
  ad slots are billed to you as products.
- **`badge` is filled on 3 of 16 products** because Amazon shows it on 3 of 16,
  not because parsing misses it. On the product page that block arrives empty
  on all six pages measured, so the badge is taken from the search row, and
  there is no separate always-false "is Amazon's Choice" column.
- **40 fields against 59 in the largest competitor's schema.** What is missing
  is what needs a proxy, a captcha solver or a signed-in session: other-seller
  offers, review text, per-variant prices, delivery to a chosen ZIP code and
  non-US marketplaces.

### How it is checked

```
python3 tests/run_all.py     # logic, lifecycle, live source
python3 tools/prove.py       # every check is broken on purpose and must fail
python3 tools/verify_readme.py   # every number on this page is compared to the code
```

`tests/test_logic.py` runs against **real amazon.com pages** saved in `recon/`,
not against hand-written markup. `tests/test_lifecycle.py` runs the whole Actor
against a double that is meaner than the real source: hard page ceiling,
reshuffled results, block pages, a withdrawn product, a broken push in the
middle of a batch and a charge limit that does not move.

`tests/test_live.py` exits with code 2 on a home connection, on purpose:
measured, amazon.com answers 503 to everything that is not the Apify cloud, and
a check that cannot run must not report success.

#### What else is this called?

Buyers look for this Actor under several names, and all of them describe what
it actually does: Amazon product scraper, Amazon bestseller tracker, ASIN
lookup API, product catalog scraper, Amazon price tracker, stock availability
monitor. It is one Actor; the names differ because the job does.

More field names: list price label with the strikethrough price (`listPriceLabel`), SEO canonical URL (`seoUrl`), bought-in-past-month figure used as a sales proxy (`boughtInPastMonth`), coupon detection (`hasCoupon`).

# Actor input Schema

## `searchTerms` (type: `array`):

Keywords to search on amazon.com, one per line. Amazon stops a single search at 306 organic products (measured); switch on Deep scan to go past that.

## `startUrls` (type: `array`):

Product pages (/dp/ASIN), search result URLs and Best Sellers URLs. Bare ASINs work too. Anything that is not an amazon.com URL or an ASIN is listed in the REPORT record as skipped instead of being silently ignored.

## `bestsellerCategories` (type: `array`):

Best Sellers category nodes, for example electronics, kitchen, toys-and-games or electronics/12097478011. Each list gives 60 ranked products over two pages.

## `department` (type: `string`):

Limit the search to one Amazon department. An unknown name stops the run with a named reason on purpose: Amazon answers an unknown department with HTTP 200 and ordinary unfiltered results (measured: 14 of 16 products identical to the unfiltered search).

## `minPrice` (type: `integer`):

Sent to Amazon as a price filter, not applied afterwards.

## `maxPrice` (type: `integer`):

Sent to Amazon as a price filter, not applied afterwards. Leave at 0 for no upper limit.

## `sortBy` (type: `string`):

Amazon's own sort order. Each order is a different window into the same catalogue: measured, four orders returned 64 products of which 64 were unique.

## `deepScan` (type: `boolean`):

Walks the same keyword through 11 separate price bands instead of one plain search. Measured on amazon.com: six price bands returned 96 products with zero overlap, so each band is its own window with its own ceiling. Costs more requests.

## `maxPagesPerSearch` (type: `integer`):

16 organic products per page. Amazon's own limit is 20 pages per window (measured: page 20 returned 2 products and there was no page 21).

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

Limits what is WRITTEN to the dataset, not what is crawled. Filters are applied before this count, so a filtered run still fills up to this number. 0 means no limit.

## `fetchProductDetails` (type: `boolean`):

Open every product page for brand, seller, availability, bullet points, specifications, high-resolution images and Best Sellers Rank. One extra request per product.

## `minRating` (type: `integer`):

Products with no rating at all are kept: missing data is not a mismatch.

## `minReviews` (type: `integer`):

Products with no rating count at all are kept: missing data is not a mismatch.

## `onlyDiscounted` (type: `boolean`):

Keeps products that show a struck-through list price. Works without product details.

## `onlyInStock` (type: `boolean`):

Stock is only stated on the product page, so switching this on switches product details on as well, and the log says so.

## `titleContains` (type: `string`):

Case-insensitive. Products with no title are kept.

## `brandIs` (type: `string`):

Brand is only stated on the product page, so this switches product details on as well.

## `onlyChanged` (type: `boolean`):

Compares against what earlier runs delivered and writes only products whose price, list price, stock, rating, rating count or best seller rank moved. Every row is labelled NEW, UPDATED or UNCHANGED.

## `emitUnchanged` (type: `boolean`):

Monitor mode with the labels but without skipping anything.

## `compactOutput` (type: `boolean`):

15 columns instead of 35, for CSV exports and AI agents.

## `dropEmptyFields` (type: `boolean`):

Leave out keys whose value is empty.

## `requestDelaySeconds` (type: `integer`):

Measured from the Apify cloud: 30 requests back to back with no delay returned 30 answers with code 200 and no rate limiting. The default is polite rather than necessary.

## `requestTimeoutSeconds` (type: `integer`):

How long to wait for one Amazon page before giving up and retrying.

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

Where the requests go out from. Measured on 18 Sep 2026 from Apify's own cloud: amazon.com answered 503 to every direct request and no product could be read, so a residential address is the default. Turn it off to run without a proxy.

## Actor input object example

```json
{
  "searchTerms": [
    "wireless earbuds"
  ],
  "startUrls": [],
  "bestsellerCategories": [],
  "department": "",
  "minPrice": 0,
  "maxPrice": 0,
  "sortBy": "",
  "deepScan": false,
  "maxPagesPerSearch": 3,
  "maxItems": 50,
  "fetchProductDetails": false,
  "minRating": 0,
  "minReviews": 0,
  "onlyDiscounted": false,
  "onlyInStock": false,
  "titleContains": "",
  "brandIs": "",
  "onlyChanged": false,
  "emitUnchanged": false,
  "compactOutput": false,
  "dropEmptyFields": false,
  "requestDelaySeconds": 0,
  "requestTimeoutSeconds": 45,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All collected rows

# 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 = {
    "searchTerms": [
        "wireless earbuds"
    ],
    "maxPagesPerSearch": 3,
    "maxItems": 50,
    "requestDelaySeconds": 0,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/amazon-product-scraper").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 = {
    "searchTerms": ["wireless earbuds"],
    "maxPagesPerSearch": 3,
    "maxItems": 50,
    "requestDelaySeconds": 0,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("snow_leo_data/amazon-product-scraper").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 '{
  "searchTerms": [
    "wireless earbuds"
  ],
  "maxPagesPerSearch": 3,
  "maxItems": 50,
  "requestDelaySeconds": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call snow_leo_data/amazon-product-scraper --silent --output-dataset

```

## MCP server setup

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

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/Pmphx5bRRVBTuaPNO/builds/55WrWQV6TqpVFIkx5/openapi.json
