Amazon Product Scraper By Bestseller & Demand Filter
Pricing
$19.99/month + usage
Amazon Product Scraper By Bestseller & Demand Filter
The Amazon Product Scraper Apify actor extracts detailed product data from Amazon, including titles, prices, reviews, ratings, images, and ASINs. Ideal for eCommerce analytics, price monitoring, and competitor research, it delivers structured JSON or CSV outputs ready for automation workflows.
Pricing
$19.99/month + usage
Rating
0.0
(0)
Developer
Scraper Engine
Maintained by CommunityActor stats
1
Bookmarked
19
Total users
0
Monthly active users
4 days ago
Last modified
Categories
Share
Amazon Product Scraper — Prices, Bestseller Rank, Demand Data
Amazon Product Scraper By Bestseller & Demand Filter turns Amazon category, search and product URLs into structured product rows, then scores each one for demand: monthlyPurchasesNumber parsed from Amazon's "bought in past month" badge, mainBestsellerRank from the product-details table, plus price, stars, reviewsCount and 50-odd more keys as typed JSON. Two thresholds — minMonthlyPurchases and maxBestsellerRank — mark every row as passing or failing. Paste a URL and press Start.
⚠️ Read this before your first run. The demand thresholds are not an Amazon-side search filter. Every product is fetched and parsed first, then scored, then pushed — rows that fail are still written to the dataset (flagged passedDemandFilter: false) but are not charged as a row_result event. The requests were already made, so a strict threshold lowers your usable row count without lowering the crawling work behind it. Both thresholds also read fields that only exist on a product page, so setting scrapeProductDetails: false leaves them with nothing to compare and every row passes.
What is Amazon Product Scraper By Bestseller & Demand Filter?
Amazon Product Scraper By Bestseller & Demand Filter is an Apify Actor that crawls Amazon category and search pages, opens each product page, and returns one JSON row per product with a demand-analytics block attached. It reads Amazon over plain HTTP with a browser-matched TLS fingerprint through Apify Residential proxies — no headless browser is launched.
No Amazon account, login or cookie is required. Everything it returns is visible to an anonymous shopper on the page.
It is built for sourcing and private-label teams screening products by demand signals, pricing analysts tracking buy-box movement, and developers piping Amazon catalogue data into their own tools or an LLM pipeline.
What Amazon product data is publicly available to scrape?
Amazon renders the full product page — title, brand, buy-box price, list price, rating histogram, review count, Best Sellers Rank, feature bullets, A+ content, delivery estimate and the featured seller — to any visitor with no login. What sits behind a gate is the seller's own performance reporting, not the listing itself.
| Data Category | Publicly Available | Behind an Amazon account / seller account / approved API |
|---|---|---|
| ASIN, title, brand, breadcrumbs, images | ✅ Public | — |
| Buy-box price, list price, price range, shipping line | ✅ Public — varies by delivery location | — |
| Star rating, review count, star histogram percentages | ✅ Public | — |
| Best Sellers Rank per category | ✅ Public in the product-details table — absent on many listings | — |
| "N bought in past month" badge | ✅ Public where Amazon chooses to render it — not on every listing | — |
| Other offers, condition, Prime flag, third-party seller names | ✅ Public | — |
| Exact unit sales, sessions, conversion rate | ❌ | Seller Central, for your own listings only |
| Individual review text and reviewer names | ❌ Not returned by this Actor | — |
| Bulk catalogue feed under a licence | ❌ | Product Advertising API, approved Associates account |
Amazon Product Scraper By Bestseller & Demand Filter only returns publicly visible data — what any visitor sees on a search result or product page. Nothing behind a login wall.
What data can I extract with Amazon Product Scraper By Bestseller & Demand Filter?
Every product row carries five groups: the demand block that gives this Actor its name, product identity, commercial data, page content, and nested seller and offer objects.
A full product row has 62 keys, plus priceVariants when variant-price scraping is switched on. Absent values come back as null, [] or {} rather than as a missing key, so downstream code never needs a presence check.
| Field Name | Description |
|---|---|
passedDemandFilter | true when the row satisfies both demand thresholds. true for every row when both are left at 0 |
demandFilterReasons | Array of strings explaining each threshold that failed, e.g. "bestsellerRank 8421 > maxBestsellerRank 5000". null when the row passed |
monthlyPurchasesNumber | Integer floor parsed from the purchase badge: "3K+" becomes 3000. null when Amazon did not render the badge |
monthlyPurchaseVolume | The badge text as rendered, e.g. "3K+ bought in past month". null when absent |
mainBestsellerRank | Rank number from the first entry in bestsellerRanks. null when the page shows no rank |
mainBestsellerCategory | Category name from that same first rank entry |
estimatedMonthlySalesRange | Object, or null. The Actor's own wide estimate derived from mainBestsellerRank — not Amazon data. See the table below |
bestsellerRanks | Array of objects: rank, category — every rank the product-details table lists. null when none found |
asin | 10-character Amazon ASIN, parsed from the URL |
originalAsin | Same as asin on a normal row; on a separately expanded variant row it holds the parent product's ASIN |
title | Product title from the page's title element |
brand | Byline brand, with Visit the, Store and Brand: stripped |
url | The Amazon product URL the Actor fetched |
input | The first entry in your categoryOrProductUrls list — see the note below |
unNormalizedProductUrl | Also the first entry in categoryOrProductUrls |
price | Object: value (float), currency (symbol). null when no price element is readable |
listPrice | Object: value, currency — the struck-through was-price |
priceRange | Object: min / max, each { value, currency } — only for listings that show a range |
shippingPrice | Object: value, currency — the first currency amount found in the delivery block |
inStock | Boolean. true when the availability text reads as in stock or an add-to-cart button exists |
inStockText | Cleaned availability line, e.g. "In Stock" |
stars | Rating out of 5 as a float |
starsBreakdown | Object keyed 5star … 1star, values as fractions (0.86 = 86%). {} when the histogram is not readable |
reviewsCount | Total ratings as an integer |
hasReviews | Boolean, derived from reviewsCount being non-zero |
reviewsLink | Link to the review page; falls back to a constructed /product-reviews/<asin>?reviewerType=all_reviews path |
answeredQuestions | Number from the answered-questions link |
breadCrumbs | Amazon's category trail joined with " > " |
thumbnailImage | Main product image URL |
galleryThumbnails | Array of gallery thumbnail URLs, GIFs and play-icon sprites removed |
highResolutionImages | Array of the same images rewritten to _SL1500_ — only entries whose URL could be rewritten appear |
description | Description paragraphs joined. Falls back to the first 800 characters of aPlusContent when Amazon shows no description block |
features | Array of feature-bullet strings longer than five characters |
productOverview | Array of objects: key, value — merged from the overview block, technical-spec tables and detail bullets, deduplicated |
aPlusContent | A+ content text, truncated to 5,000 characters |
brandStory | Brand-story text, truncated to 3,000 characters |
importantInformation | Important-information block, truncated to 2,000 characters |
sustainabilityFeatures | Climate Pledge / sustainability block, truncated to 1,500 characters |
productComparison | Array of arrays — Amazon's comparison table as rows of cell strings. null when the widget is absent |
returnPolicy | Return-policy text, truncated to 500 characters |
support | Product-support text, truncated to 500 characters |
delivery | First weekday delivery date found in the delivery block, e.g. "Tuesday, July 28" |
fastestDelivery | Second weekday date found in the same block |
isAmazonChoice | Boolean — an Amazon's Choice badge is present |
amazonChoiceText | Badge text, truncated to 200 characters. null when no badge |
visitStoreLink | Object: text, url — the brand-store link |
variantAsins | Array of sibling variant ASINs found on the page |
seller | Object — always present. Six keys by default, two more with scrapeSellers. See the table below |
offers | Array of offer objects. [] unless maxOffers is above 0. See the table below |
locationText | The delivery location Amazon displayed, read from the page's location strip |
loadedCountryCode | An echo of your countryCode input, or "US" when you left it empty. Not read from the page |
categoryPageData | Object: pageNumber, saleSummary, isSponsored, bestsellerBadge, productPosition |
author | Always null in this build |
videosCount | Always 0 in this build |
attributes | Always [] in this build |
variantAttributes | Always [] in this build |
manufacturerAttributes | Always [] in this build |
variantDetails | Always [] in this build |
bookDescription | Always null in this build |
aiReviewsSummary | Always null in this build |
productPageReviews | Always [] in this build |
productPageReviewsFromOtherCountries | Always [] in this build |
priceVariants | Only present when scrapeProductVariantPrices is true and the product has variants. Array of objects: asin, url, price |
inputandunNormalizedProductUrlboth reportcategoryOrProductUrls[0], not the start URL that actually produced the row. In a multi-URL run every row names the first URL in your list. Useurlfor the product, and treat those two keys as run-level rather than row-level.
Demand and rank fields
estimatedMonthlySalesRange is the one field in the row that Amazon does not publish in any form. It is computed by this Actor from mainBestsellerRank alone, using a power-law curve with a category-size adjustment, and it labels itself as such:
estimatedMonthlySalesRange key | Description |
|---|---|
min | Lower bound of the estimated monthly unit band |
max | Upper bound of the same band |
category | The category the rank came from |
basisRank | The rank the estimate was calculated from |
categoryTier | "large" when the category name matches one of Amazon's big top-level departments, otherwise "standard" |
estimated | Always true |
method | The literal string "BSR power-law heuristic (uncalibrated; wide band, not Amazon data)" |
The band is deliberately wide — the lower bound is 40% and the upper bound 220% of the curve's central value. Treat it as a relative sorting signal between products in the same category, never as a sales figure. monthlyPurchasesNumber is the field with an actual Amazon origin, and even that is a floor parsed from a rounded badge.
Nested seller and offer objects
seller is always an object, never null. With scrapeSellers off it carries whatever the product page exposed:
seller key | Description |
|---|---|
id | Amazon merchant ID parsed from the seller link |
name | Seller display name — five fallback selectors are tried in order |
url | Absolute seller-profile URL |
businessName | null unless scrapeSellers is true |
phone | null unless scrapeSellers is true |
address | null unless scrapeSellers is true |
averageRating | Key only added when scrapeSellers is true and the seller page was readable |
reviewsCount | Key only added under the same condition |
With scrapeSellers: true the Actor fetches the seller profile page for the featured seller and for the seller of every offer it collected, and merges in businessName, address, phone, averageRating and reviewsCount — only where the seller page produced a non-empty value.
Each entry in offers has the same shape:
offers[] key | Description |
|---|---|
price | Object: value, currency |
shippingPrice | Object: value, currency |
condition | Offer heading text, e.g. "New", "Used - Very Good" |
isPrime | Boolean — a Prime icon was present on the offer |
seller | Object: id, name, url, enriched with the seller fields above when scrapeSellers is on |
The lighter row shape from scrapeProductDetails: false
Setting scrapeProductDetails to false skips product pages entirely and builds rows from search-result tiles. That row has 15 keys, not 62: asin, title, url, thumbnailImage, price, stars, reviewsCount, isSponsored, categoryPageData (only productPosition and isSponsored inside it), plus the six demand keys. Note that isSponsored appears as a top-level key only on these lighter rows; on full rows it lives inside categoryPageData.
Because neither the purchase badge nor the Best Sellers Rank exists on a search tile, monthlyPurchasesNumber, mainBestsellerRank, mainBestsellerCategory and estimatedMonthlySalesRange are all null on a lighter row, and passedDemandFilter is true on every one of them regardless of what you set the thresholds to.
How prices are assembled
On product pages price is built by concatenating Amazon's whole and fraction price spans, so it depends on the whole-price span carrying the decimal separator; the search-tile parser instead joins the two with an explicit decimal point. When the whole/fraction path yields nothing, the Actor falls back to parsing Amazon's offscreen $129.99 string — which is also the only source used for listPrice, shippingPrice, offer prices and priceVariants. If a price.value ever looks off by a factor of one hundred, compare it against listPrice and the offers[].price values first.
🤖 Add-on: Need additional Amazon data?
Three Scraper-Engine Actors cover the Amazon jobs this one does not. Amazon ASIN Scraper By Country & Marketplace takes an ASIN list and compares the same products across marketplaces with USD conversion and price-gap ranking. Amazon Today's Deals Scraper With Star Ratings & Deal Badges reads the deals feed with badge labels, savings amounts and star histograms. Amazon Top Rated Products Scraper filters keyword searches by rating and review count and adds review insights.
How the demand filters work
Both thresholds are evaluated in the same place — after a product has been fetched and parsed, immediately before it is pushed — and both are permissive by design: a product is only dropped when known data violates a threshold.
minMonthlyPurchases compares against monthlyPurchasesNumber. Amazon does not publish a monthly sales figure; it renders a rounded social-proof badge such as "1K+ bought in past month" on some listings. The Actor reads that string, multiplies a K suffix by 1,000 and an M suffix by 1,000,000, discards the +, and floors the result to an integer. "1K+" becomes 1000, "500+" becomes 500, "2.5K+" becomes 2500. A product reading monthlyPurchasesNumber: 1000 may have sold anywhere from 1,000 units upward — the badge's own granularity is the limit, not the parser's. When the badge is absent the value is null and the product is kept, so a run with minMonthlyPurchases: 500 returns both products that demonstrably cleared 500 and products where Amazon showed nothing at all. Filter on monthlyPurchasesNumber being non-null if you need only the confirmed set.
maxBestsellerRank compares against mainBestsellerRank, which is the rank of the first entry in bestsellerRanks — the first "#N in Category" pair found in the product-details table, which on a normal Amazon page is the broad top-level department rank rather than a narrow sub-category rank. Products with no rank on the page are kept. If you need a specific sub-category rank, read the full bestsellerRanks array and filter it yourself.
Neither threshold reduces the requests made. Filtering happens after the fetch, so maxItemsPerStartUrl counts filtered-out products too, and a category where few products carry a rank or a badge will produce a dataset that is mostly passedDemandFilter: true — permissiveness, not accuracy.
A failing row is still written, just not charged. Rows that pass are pushed with the row_result charged event; rows that fail are pushed with no event at all. Filter your dataset on passedDemandFilter == true to get the charged set, and read demandFilterReasons on the rest to see which threshold rejected each one.
Why not build this yourself?
Amazon's own programmatic route is the Product Advertising API, and it is not self-serve: access is granted through an approved Amazon Associates account, and Amazon requires qualifying sales through your Associates links to grant and keep it (Product Advertising API 5.0 documentation). Without that approval, reading the public pages is the alternative — and the cost of doing it yourself sits in four places.
Anti-bot. Amazon does not just return a 403. It serves a Robot Check page, and — more awkwardly — a stripped-down version of a product page that returns HTTP 200 with no product-title element in it. A naive scraper counts those as successes and writes empty rows. This Actor treats a product page with no title element as a block, recycles the session that received it, and retries.
Fingerprint coherence. An HTTP client's TLS fingerprint has to match the User-Agent string it sends. This Actor ships three paired profiles — Chrome 131 on Windows, Chrome 131 on macOS, Chrome 124 on Windows — where the impersonation target, UA string, sec-ch-ua header and platform hint all agree. Mixing a modern UA with a mismatched handshake is a fast way to get Robot Checked.
Session lifecycle. Amazon issues cookies on the home page and expects them on every subsequent request. That means warming each proxy IP on the home page first, keeping the session sticky to that IP, and retiring it before it goes stale — this Actor retires a session after 200 requests, after nine minutes, or once its block rate exceeds 40% over at least five requests.
Parser churn. Amazon runs several markup generations at once. Every field here has fallbacks: the rating histogram is read from the modern histogram list, then from meter percentage attributes, then from a legacy table; the seller name has five selectors; the ranks table has a sales-rank element fallback; offers try the ajax offer panel first and the older offer-listing page second. Each of those fallbacks exists because the primary selector fails on some subset of listings.
How to use Amazon Product Scraper By Bestseller & Demand Filter
The Actor runs on Apify. Start it from the Apify Console or call it through the Apify API — no other account is needed, unless you want captcha solving.
- Open the Actor on Apify and click Try for free
- Paste one or more Amazon URLs into Amazon category, search, or product URLs (
categoryOrProductUrls) — this is the only required input. Category, search and browse-node pages are crawled for products;/dp/and/gp/product/URLs are scraped directly - Set Max products per start URL (
maxItemsPerStartUrl) explicitly rather than relying on a default - Under Sales demand filters, set Minimum monthly purchases and Maximum Best Sellers Rank. Leave both at
0to keep everything and just read the demand fields - Leave Scrape product details on — the demand filters need product pages to read
- Leave Proxy configuration on Apify Residential; Amazon blocks datacenter traffic hard
- Click Start, then export as JSON, CSV, Excel, XML or HTML
A run with an empty categoryOrProductUrls fails immediately with categoryOrProductUrls is required and must be a non-empty array. An entry that is an empty or blank string fails the same way, naming its index.
How to scale to bulk product extraction
categoryOrProductUrls is a list, so one run can mix category pages, search pages and individual product URLs. Plain strings and { "url": "..." } objects are both accepted, and — although the field is titled for URLs — a bare keyword also works: anything that does not start with http is turned into an amazon.com search for that phrase.
Three things to plan around. maxItemsPerStartUrl applies per start URL, so ten URLs at 50 is a 500-product target, not 50. Separately expanded variant rows are pushed on top of that cap, so maxProductVariantsAsSeparateResults can push your real row count above it. And ASINs are deduplicated across the whole run: a product that appears under two of your start URLs is scraped and charged once.
What can you do with Amazon product data?
- 🔎 A private-label sourcer screening a category sets
minMonthlyPurchasesandmaxBestsellerRank, then ranks the passing rows bymainBestsellerRankagainstpriceandreviewsCountto find demand that is not yet saturated with reviews. - 💰 A pricing analyst tracking a competitor re-runs the same product URLs daily and diffs
price.valueagainstlistPrice.valueperasinto measure discount depth, usingoffers[].priceto see whether a third-party seller is undercutting the buy box. - 📦 A reseller assessing margin turns on
maxOffersandscrapeSellersand readsoffers[].price,offers[].condition,offers[].isPrimeandseller.averageRatingto judge how crowded the offer page already is. - ⭐ A product manager auditing quality complaints reads
starsBreakdownandreviewsCounttogether, since a 4.3 built on 12% one-star reviews behaves differently from a 4.3 with almost none. - 🤖 An AI engineer building a shopping assistant indexes
title,description,features,productOverviewandaPlusContentinto a vector store keyed onasin, so an agent can answer "which keyboards under $150 show a thousand-plus monthly purchases and rank inside the top 5,000" against live data instead of a stale snapshot. - 📊 A market researcher sizing a niche exports
mainBestsellerCategory,mainBestsellerRankandmonthlyPurchasesNumberacross a category crawl and usesestimatedMonthlySalesRangeonly to sort products relative to one another, never as an absolute figure.
Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run.
How does Amazon Product Scraper By Bestseller & Demand Filter handle rate limits and blocking?
Requests go out through Apify Residential proxies from a pool of sticky sessions, each warmed on the Amazon home page before its first real request. Session pool size is max(min(maxConcurrentRequests, 12), 4), and warmups run at most four at a time, with a second attempt on a fresh IP when the first is blocked. Each session carries one of three coherent browser profiles, and a session that has served 200 requests, has lived nine minutes, or is blocking on more than 40% of at least five requests is retired and replaced.
A response counts as blocked if it contains a Robot Check marker or, on a product page, if the product-title element is missing — which is how Amazon's stripped-down pages are caught rather than saved as empty rows. Blocked responses recycle the session and retry: up to three fetch attempts per URL with exponential backoff from 1.5 seconds, capped at 15 seconds plus jitter, wrapped in an outer loop of up to three passes per product URL. Every request carries a small random delay that grows with the run's rolling block rate, and if that rate passes 25% the Actor adds a cooldown of up to 60 seconds between retries. HTTP 4xx responses are not retried. A proxy authentication failure aborts that URL and logs a proxy-configuration error.
When a URL is exhausted, nothing is pushed for it. There are no error rows in the dataset — failures are recorded in the run log and in the OUTPUT key-value record instead.
⚠️ Captcha solving requires your own CapSolver key
useCaptchaSolver does nothing on its own. When it is true and Amazon returns a Robot Check page, the Actor looks for a CapSolver API key in the CAPSOLVER_API_KEY environment variable. There is no input field for the key — you set it as an environment variable on the Actor, and the account at capsolver.com is one you create and fund yourself.
If the toggle is on and no key is found, nothing happens and no error is raised: the response is treated as a normal block and goes through the usual retry and session-recycle path. That is a silent no-op, so a run that looks like it has captcha solving enabled may not have it at all.
When a key is present, the Actor downloads the challenge image, submits it to CapSolver as an image-to-text task, polls up to twenty times at three-second intervals, and replays the solution against Amazon's captcha-validation form. Only image captchas on the standard Robot Check form are handled. Per the Actor's own input description, this works well only on the .com domain, and Amazon withholds some product fields after a solved captcha — including bestseller ranks, the field maxBestsellerRank filters on. Leaving useCaptchaSolver off is the right default for demand-filtered runs.
⬇️ Input
One parameter is required: categoryOrProductUrls. All nineteen parameters below match the Actor's input schema exactly.
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
categoryOrProductUrls | Yes | array | Amazon category, search, browse-node or product URLs. Category and search pages are crawled for products; /dp/ and /gp/product/ URLs are scraped directly. Plain strings and { "url": "..." } objects both accepted. No default; a run without it fails. | ["https://www.amazon.com/s?k=mechanical+keyboard"] |
maxItemsPerStartUrl | No | integer | Maximum products scraped from each start URL. Applies to category and search crawling only — a direct product URL is scraped regardless. Filtered-out products count against it. Minimum 0. Schema default 10; the Actor's internal fallback is 100, so set it explicitly. | 50 |
minMonthlyPurchases | No | integer | Keep products whose parsed purchase badge is at or above this number. Products with no badge are kept. Minimum 0. Default 0 (no minimum). | 1000 |
maxBestsellerRank | No | integer | Keep products whose first Best Sellers Rank is at or below this number. Products with no rank are kept. Minimum 0. Default 0 (no cap). | 5000 |
language | No | string | Sets the Accept-Language header and the language cookie. 24 options: cs, da, de, en, es, fr, nl, pl, pt, sv, tr, he, ar, mrta, hi, bn, ta, te, kn, ml, ko, zh_CN, zh_TW, ja. A domain that does not support your choice serves its own default. No default — the header falls back to en-US,en;q=0.9. | "en" |
proxyCountry | No | string | Apify proxy exit country, 252 options. AUTO_SELECT_PROXY_COUNTRY maps the first start URL's Amazon domain to a country (21 domains are mapped). NO_PROXY disables proxying entirely. Default "AUTO_SELECT_PROXY_COUNTRY". | "US" |
maxSearchPagesPerStartUrl | No | integer | Maximum search result pages per start URL. Whichever of this and maxItemsPerStartUrl is reached first stops the crawl. Minimum 1. Default 9999. | 5 |
maxProductVariantsAsSeparateResults | No | integer | Maximum variants per product to fetch and push as their own rows, each carrying the parent ASIN in originalAsin. These rows are pushed on top of maxItemsPerStartUrl. Minimum 0. No schema default; the Actor uses 0. | 3 |
maxOffers | No | integer | Maximum offers per product to collect from the other-sellers panel. 0 collects none and leaves offers as []. Minimum 0. Default 0. | 5 |
scrapeSellers | No | boolean | Fetch each seller's profile page and merge businessName, address, phone, averageRating and reviewsCount into seller and into every offers[].seller. One extra request per distinct seller. Default false. | false |
ensureLoadedProductDescriptionFields | No | boolean | Accepted by the schema but not read anywhere in this build — switching it on changes nothing. Default false. | false |
useCaptchaSolver | No | boolean | Attempt to solve Amazon image captchas. Requires a CapSolver key in the CAPSOLVER_API_KEY environment variable; without one it is a silent no-op. Default false. | false |
scrapeProductVariantPrices | No | boolean | Fetch one page per sibling variant to collect its price, adding a priceVariants array to the row. Default false. | false |
scrapeProductDetails | No | boolean | true opens every product page and returns the full 62-key row. false builds 15-key rows from search tiles instead — faster and lighter, but the demand filters have nothing to read and every row passes. Default true. | true |
maxConcurrentRequests | No | integer | Concurrent product-page requests for the whole run. Also sets the session pool size to max(min(value, 12), 4). Search-page, offer and seller requests are not covered by this limit. Minimum 1, maximum 20. Default 10. | 10 |
countryCode | No | string | Delivery country set on each session at warmup, 240 options. Echoed back in loadedCountryCode. No default. | "US" |
zipCode | No | string | Delivery postcode set on each session at warmup, for finer localisation than country alone. No default. | "10001" |
locationDeliverableRoutes | No | array | Page types the delivery-location settings should apply to: PRODUCT, SEARCH, OFFERS. Has no practical effect in this build — see the note below. Default ["PRODUCT", "SEARCH", "OFFERS"]. | ["PRODUCT", "SEARCH", "OFFERS"] |
proxyConfiguration | No | object | Apify proxy settings. RESIDENTIAL is used when no group is given. Setting useApifyProxy: false runs the Actor with no proxy at all. Prefilled {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}. | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} |
Inputs that do not behave the way their names suggest. Worth knowing before you debug a run:
- Pagination past page 1 rebuilds the URL when it contains
k=. Page 1 is fetched exactly as you supplied it, so in-URL filters and sort order apply there. From page 2 onward, if the start URL has ak=keyword parameter, the Actor constructs a freshhttps://www.amazon.com/s?k=<keyword>&page=N— dropping the original domain and the category, price, brand, rating and sort parameters. Browse-node and category URLs withoutk=keep every parameter and simply get&page=Nappended, so those are the safer input shape for a filtered multi-page crawl. - Non-
.commarketplaces work for direct product URLs only. A/dp/URL onamazon.deis fetched onamazon.de. But product links harvested from a category or search page are rebuilt ashttps://www.amazon.com/dp/<asin>, and session warmup, the delivery-location call, the offers endpoint, seller pages and variant lookups are all hardcoded toamazon.com. For a non-US marketplace, supply product URLs directly, or usescrapeProductDetails: false— the lighter path keeps the domain from the page's own links. locationDeliverableRoutesis inert. An empty list is silently replaced with all three values, and any non-empty list containing any of the three enables the location call, so no combination turns it off or narrows it. The delivery location is applied once per session at warmup, before any page type is known, and therefore applies to every request that session makes.countryCodeandzipCodeare set best-effort and silently. They are posted to Amazon's address-change endpoint during warmup and the response is ignored, so a rejected address produces no error. ChecklocationTextin the output — that is read from the page — rather thanloadedCountryCode, which only echoes your input.ensureLoadedProductDescriptionFieldsis accepted and ignored. No code path reads it.variantDetailsis always empty, despite themaxProductVariantsAsSeparateResultsdescription pointing you to it as the default-output alternative. If you need per-variant data, either expand variants as separate rows or usescrapeProductVariantPrices.maxItemsPerStartUrl: 0yields no products from category or search URLs, while direct product URLs are still scraped.maxConcurrentRequestsdoes not cap every request. It covers product pages. Search pages are fetched five at a time in the full-detail path, and offer, seller and variant-price requests happen outside that limit.- Pagination can over-fetch by up to four pages. In the full-detail path, search pages are requested in concurrent batches of five, so the batch that satisfies your product target may fetch pages you did not need.
- Retry and timeout values are not exposed. Three fetch attempts per URL, three passes per product URL, a 35-second request timeout, a 1.5-second base backoff capped at 15 seconds — all fixed internally, with no input to change them.
Example input
{"categoryOrProductUrls": ["https://www.amazon.com/s?k=mechanical+keyboard","https://www.amazon.com/dp/B0BSHF7WHW"],"maxItemsPerStartUrl": 50,"minMonthlyPurchases": 1000,"maxBestsellerRank": 5000,"language": "en","proxyCountry": "AUTO_SELECT_PROXY_COUNTRY","maxSearchPagesPerStartUrl": 5,"maxProductVariantsAsSeparateResults": 0,"maxOffers": 3,"scrapeSellers": true,"ensureLoadedProductDescriptionFields": false,"useCaptchaSolver": false,"scrapeProductVariantPrices": false,"scrapeProductDetails": true,"maxConcurrentRequests": 10,"countryCode": "US","zipCode": "10001","locationDeliverableRoutes": ["PRODUCT", "SEARCH", "OFFERS"],"proxyConfiguration": {"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"]}}
⬆️ Output
Every product is one dataset row of typed, normalized JSON with the same keys run after run — 62 for a full product row, 15 for a search-tile row, plus priceVariants when variant prices are on. Rows are pushed as each product finishes, so the dataset fills while the run is still going. Nested values arrive as plain objects and arrays; absent values come back as null, [] or {} rather than as missing keys. Export as JSON, CSV, Excel, XML or HTML, or read the dataset through the Apify API. The default dataset view surfaces fourteen columns — asin, title, passedDemandFilter, monthlyPurchasesNumber, mainBestsellerRank, estimatedMonthlySalesRange, price, stars, reviewsCount, brand, url, thumbnailImage, breadCrumbs, inStock — while the export contains every key.
There are no error rows and no accounting rows. No errorReason, error, isError or status field exists on any row — a product that could not be fetched or parsed is simply absent. Failures are collected in the run log and in the OUTPUT record of the run's key-value store, which holds status, scrapedAt, duration, a stats object (totalProducts, chargedProducts, filteredOutProducts, totalErrors, urlsProcessed plus request metrics) and an errors array of { url, error, kind } entries. Error kind values are blocked, lite_page, http_error, server_error, timeout, network, proxy_auth, parse_error, fatal and unknown.
The one row distinction that matters for cost is the demand flag: rows with passedDemandFilter: true are pushed with the row_result charged event, rows with passedDemandFilter: false are pushed with no event and cost nothing per row. Filter your dataset on passedDemandFilter == true to get exactly the set you were charged for.
Example output
{"title": "Keychron K8 Pro Wireless Mechanical Keyboard, 87-Key TKL Hot-Swappable QMK/VIA Programmable RGB Backlit Bluetooth Keyboard for Mac Windows","url": "https://www.amazon.com/dp/B0BSHF7WHW","asin": "B0BSHF7WHW","originalAsin": "B0BSHF7WHW","price": { "value": 114.99, "currency": "$" },"inStock": true,"inStockText": "In Stock","listPrice": { "value": 139.99, "currency": "$" },"brand": "Keychron","author": null,"shippingPrice": null,"stars": 4.5,"starsBreakdown": {"5star": 0.71,"4star": 0.16,"3star": 0.06,"2star": 0.03,"1star": 0.04},"reviewsCount": 3184,"answeredQuestions": 62,"breadCrumbs": "Electronics > Computers & Accessories > Computer Accessories & Peripherals > Keyboards, Mice & Accessories > Keyboards","videosCount": 0,"visitStoreLink": {"text": "Visit the Keychron Store","url": "https://www.amazon.com/stores/Keychron/page/8A5B2E71-4C3D-4E19-9F0A-2D6C1B7E5A34"},"thumbnailImage": "https://m.media-amazon.com/images/I/71qJ9pQKS2L._AC_SX679_.jpg","galleryThumbnails": ["https://m.media-amazon.com/images/I/71qJ9pQKS2L._AC_US40_.jpg","https://m.media-amazon.com/images/I/61bT4hVQxeL._AC_US40_.jpg"],"highResolutionImages": ["https://m.media-amazon.com/images/I/71qJ9pQKS2L._AC_SL1500_.jpg","https://m.media-amazon.com/images/I/61bT4hVQxeL._AC_SL1500_.jpg"],"importantInformation": null,"sustainabilityFeatures": null,"description": "The K8 Pro is a tenkeyless hot-swappable mechanical keyboard with QMK and VIA firmware support, south-facing RGB, a double-gasket design and Bluetooth 5.1 for up to three devices.","features": ["QMK/VIA fully programmable — remap every key and record macros without installing extra software","Hot-swappable sockets accept most 3-pin and 5-pin MX-style mechanical switches","Connect over Bluetooth 5.1 to three devices or use the detachable USB-C cable","Double-gasket design with sound-absorbing foam for a lower, softer typing sound"],"attributes": [],"productOverview": [{ "key": "Brand", "value": "Keychron" },{ "key": "Connectivity Technology", "value": "Bluetooth, USB" },{ "key": "Keyboard Description", "value": "Mechanical" },{ "key": "Number of Keys", "value": "87" },{ "key": "Item Weight", "value": "2.03 pounds" }],"variantAsins": ["B0BSHGQ4X1", "B0BSHJ2M7T"],"variantDetails": [],"reviewsLink": "/Keychron-K8-Pro-Mechanical-Keyboard/product-reviews/B0BSHF7WHW?reviewerType=all_reviews","hasReviews": true,"delivery": "Tuesday, July 28","fastestDelivery": "Sunday, July 26","returnPolicy": "Eligible for Return, Refund or Replacement within 30 days of receipt","support": "Free Amazon tech support included","variantAttributes": [],"manufacturerAttributes": [],"seller": {"id": "A2R4J8KDYQ7NLM","url": "https://www.amazon.com/sp?seller=A2R4J8KDYQ7NLM","name": "Keychron Official","businessName": "Keychron Technology Co., Limited","phone": "+852 3555 1234","address": "Unit 1204, 12/F, Tower A\nNew Mandarin Plaza, Kowloon\nHong Kong","averageRating": 4.8,"reviewsCount": 5417},"bestsellerRanks": [{ "rank": 312, "category": "Electronics" },{ "rank": 7, "category": "Computer Keyboards" }],"isAmazonChoice": true,"amazonChoiceText": "Amazon's Choice for mechanical keyboard","bookDescription": null,"priceRange": null,"aPlusContent": "Built for creators and developers. The K8 Pro ships with QMK firmware pre-flashed and VIA support enabled, so remapping happens in the browser.","brandStory": "Keychron builds keyboards for people who type for a living.","productComparison": [["", "Keychron K8 Pro", "Keychron K10 Pro", "Keychron Q1 Pro"],["Layout", "TKL 87-key", "Full-size 108-key", "75% 82-key"],["Hot-swappable", "Yes", "Yes", "Yes"]],"aiReviewsSummary": null,"monthlyPurchaseVolume": "3K+ bought in past month","productPageReviews": [],"productPageReviewsFromOtherCountries": [],"locationText": "New York 10001","loadedCountryCode": "US","offers": [{"price": { "value": 114.99, "currency": "$" },"shippingPrice": null,"condition": "New","isPrime": true,"seller": {"id": "A2R4J8KDYQ7NLM","name": "Keychron Official","url": "https://www.amazon.com/sp?seller=A2R4J8KDYQ7NLM","businessName": "Keychron Technology Co., Limited","averageRating": 4.8,"reviewsCount": 5417}},{"price": { "value": 103.45, "currency": "$" },"shippingPrice": { "value": 5.99, "currency": "$" },"condition": "Used - Like New","isPrime": false,"seller": {"id": "A18KQD2T3XZ9BF","name": "PeriphOutlet","url": "https://www.amazon.com/sp?seller=A18KQD2T3XZ9BF","averageRating": 4.6,"reviewsCount": 2038}}],"unNormalizedProductUrl": "https://www.amazon.com/s?k=mechanical+keyboard","categoryPageData": {"pageNumber": 1,"saleSummary": null,"isSponsored": false,"bestsellerBadge": null,"productPosition": 4},"input": "https://www.amazon.com/s?k=mechanical+keyboard","monthlyPurchasesNumber": 3000,"mainBestsellerRank": 312,"mainBestsellerCategory": "Electronics","estimatedMonthlySalesRange": {"min": 769,"max": 4231,"category": "Electronics","basisRank": 312,"categoryTier": "large","estimated": true,"method": "BSR power-law heuristic (uncalibrated; wide band, not Amazon data)"},"passedDemandFilter": true,"demandFilterReasons": null}
How does it work?
The Actor sends plain HTTP requests with a browser-matched TLS fingerprint rather than driving a headless browser — lighter and quicker on Amazon, which serves most of its product data in server-rendered HTML.
A pool of sticky sessions is created first, each bound to one Apify Residential proxy IP and warmed on the Amazon home page so it holds real cookies; your delivery country and postcode, if set, are posted at that moment. Category and search pages are then fetched to collect ASINs, and each product page is fetched, parsed and pushed to the dataset as it completes. Every field falls back through several selectors — modern markup first, legacy markup second — so an Amazon front-end change usually degrades one field rather than failing the row, and your key names and types stay put.
Only publicly visible data is read: no login, no cookie you supply, no access to any account. After parsing, each row is scored against your demand thresholds, tagged with passedDemandFilter, and pushed — charged when it passes, uncharged when it does not.
Integrations
The Actor is a standard Apify Actor, so it works with anything that can call the Apify API or read a dataset.
Calling the Actor from Python
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("<YOUR_USERNAME>/amazon-product-scraper-by-bestseller-demand-filter").call(run_input={"categoryOrProductUrls": ["https://www.amazon.com/s?k=mechanical+keyboard"],"maxItemsPerStartUrl": 50,"minMonthlyPurchases": 1000,"maxBestsellerRank": 5000,"proxyConfiguration": {"useApifyProxy": True, "apifyProxyGroups": ["RESIDENTIAL"]},})rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())kept = [r for r in rows if r.get("passedDemandFilter")]for p in sorted(kept, key=lambda r: r.get("mainBestsellerRank") or 10**9):print(p["asin"], p.get("mainBestsellerRank"), p.get("monthlyPurchasesNumber"), p["title"][:60])
Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request.
Scheduling and webhooks
Apify Schedules re-run the same URL list on a cron expression, which is how you build a price or rank history: each run writes its own dataset, and rows join cleanly on asin. Attach a webhook on run success to push the finished dataset ID to your own endpoint, or route it to Google Sheets, Slack or a database from the Actor's integrations tab.
No-code tools (n8n, Make, Zapier)
In n8n, use the Apify node — or an HTTP Request node pointed at the Apify run endpoint with your token — and pass the same JSON input shown above; a Filter node on passedDemandFilter then keeps only the charged rows, and a Split Out node on offers gives one item per offer. In Make, use the Apify module's run-and-wait action and map the dataset into a Google Sheets or Airtable step. Both handle scheduling, so a weekly demand sweep needs no code.
Is it legal to scrape Amazon products?
Scraping publicly listed product data is broadly treated as permissible where the data is accessible without authentication, and this Actor collects only what an anonymous shopper already sees. No login is used and no access control is bypassed — with one caveat below.
The output is business and product data: ASINs, titles, prices, ranks, ratings, review counts, offers and delivery estimates. These are commercial records rather than personal data, so GDPR and equivalent personal-data regimes do not attach to this output the way they would to a profile scraper. The governing considerations here are contractual and database-related instead: Amazon's conditions of use restrict automated access, which is a civil and contractual matter, and database or unfair-competition rules can apply if you systematically reproduce a substantial part of Amazon's catalogue as a competing listing.
One narrow exception. With scrapeSellers enabled, seller.name, seller.businessName, seller.phone and seller.address — and the same fields inside offers[].seller — can identify a natural person when the merchant is a sole trader. Amazon publishes those details under trader-transparency obligations, which does not exempt you from handling them as personal data. Leave scrapeSellers off if you do not need them.
Separately, if you enable useCaptchaSolver, note that a captcha is a deliberate access control rather than an incidental one, and solving it is a factor worth raising with counsel. Consult legal counsel before republishing extracted catalogue data as a competing inventory, or if your use case involves bulk storage of seller contact details.
❓ Frequently asked questions
What Amazon product fields does this Actor return?
The five most used are asin, title, price, mainBestsellerRank and monthlyPurchasesNumber. A full product row has 62 keys, including nested objects for price, seller, offers, estimatedMonthlySalesRange and categoryPageData — see the data fields table above for every one.
Does it require an Amazon account or login?
No. No Amazon account, no cookie, no session, and no Product Advertising API credentials. The only credential you need is your Apify token. The one optional external account is CapSolver, and only if you switch on useCaptchaSolver.
How many products can I extract in one run?
maxItemsPerStartUrl sets a per-start-URL target with a minimum of 0 and no upper bound in the schema, and maxSearchPagesPerStartUrl caps the pages crawled to reach it. The real ceiling is Amazon's own pagination depth for the query, plus the crawler's own stop condition: it abandons pagination after three consecutive empty or failed pages. Separately expanded variant rows are pushed above the per-URL cap, and ASINs are deduplicated across the run.
What happens if a category URL yields no products that pass the demand filters?
You still get rows — all flagged passedDemandFilter: false, and none of them charged as a row_result. Each carries a demandFilterReasons array naming the threshold that rejected it, so you can see whether your rank cap or your purchase floor was the binding constraint. Because products with no badge and no rank are always kept, an entirely empty pass set usually means the products did have ranks and those ranks were above maxBestsellerRank — loosen it and re-run. The run's OUTPUT record logs chargedProducts and filteredOutProducts so you can see the split at a glance.
What happens if a product is delisted or blocked mid-run?
Nothing is pushed for it. A 404 or other 4xx is recorded and not retried; a Robot Check page or a stripped-down product page is retried across fresh sessions, and if all attempts fail the URL is abandoned. There are no error rows in the dataset and no placeholder values, so a delisted product lowers your row count rather than corrupting the data. Check the run log, or the errors array in the OUTPUT key-value record, where each failure appears as { url, error, kind }.
Does monthlyPurchasesNumber come from Amazon's own sales data?
No. Amazon renders a rounded badge — "1K+ bought in past month" — on some listings, and this Actor parses that string into an integer floor. 1000 means "the badge said 1K+", not "1,000 units sold". Listings without the badge return null. estimatedMonthlySalesRange is a separate field calculated by the Actor from the Best Sellers Rank, not reported by Amazon; it flags itself with estimated: true and states its own method inline.
Which Amazon marketplaces does it work with?
Direct /dp/ and /gp/product/ URLs are fetched on whatever domain you supply, and proxyCountry on AUTO_SELECT_PROXY_COUNTRY matches the exit country to that domain across 21 mapped Amazon TLDs. Category and search crawling in full-detail mode is different: harvested product links are rebuilt on amazon.com, and session warmup, delivery-location setting, offers and seller lookups are hardcoded to amazon.com too. For a non-US marketplace, either pass product URLs directly or run with scrapeProductDetails: false, which keeps the domain from the page's own links.
Can I scrape multiple Amazon URLs at once?
Yes. categoryOrProductUrls is a list and accepts any mix of category, search, browse-node and product URLs, as plain strings or { "url": "..." } objects. A bare keyword also works and becomes an amazon.com search. maxItemsPerStartUrl applies to each entry separately, so the run target is that number multiplied by your URL count.
How does it compare to other Amazon product scrapers?
Checked on the Apify Store on 25 July 2026: junglee/Amazon-crawler is the most-used Amazon Actor, documents essentially the same product-row shape as this Actor, and its README covers Amazon's roughly seven-page search restriction plus an error-item convention where failed inputs are pushed into the dataset with an error field. junglee/free-amazon-product-scraper documents the same row shape and the same error-item table for category-URL input. curious_coder/amazon-scraper covers product-listing pages including bestseller pages, documents a flatter row whose bestSellersRank entries carry category links, and states pricing of $0.1 per 1,000 results on its listing. None of the three documents a demand threshold, a parsed monthly-purchase number, or per-row pass/fail flagging. That is what this Actor adds: monthlyPurchasesNumber, mainBestsellerRank, passedDemandFilter and demandFilterReasons on every row, with failing rows kept for audit and left uncharged. What it does not do is push error items — failures live in the log and the OUTPUT record, which the two junglee Actors handle in the dataset instead.
Does it return data in a format LLMs can use directly?
Yes. Every row is typed, normalized JSON with consistent field names across runs — no HTML parsing, no selectors. Pass a row straight into an LLM context window, index it into a vector store, or hand it to an agent tool. The Actor is callable as a standard HTTP endpoint through the Apify API, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and consume the result directly.
Which Amazon product fields work best for AI training data and RAG indexing?
For RAG: title, description, features, productOverview and aPlusContent carry the most descriptive text and chunk cleanly by asin, with breadCrumbs giving category context for filtered retrieval. For training data: price.value, stars, reviewsCount, mainBestsellerRank, monthlyPurchasesNumber and the five starsBreakdown fractions are the most structurally consistent fields across records. All values return as typed primitives, arrays or plain nested objects, so no normalization pass is needed — just remember that monthlyPurchasesNumber and mainBestsellerRank are null on a meaningful share of listings and should be treated as missing rather than zero.
🔗 Related scrapers
| Scraper Name | What it extracts |
|---|---|
| Amazon ASIN Scraper By Country & Marketplace | One ASIN list priced across marketplaces — USD conversion, price rank, cheapest-marketplace gap |
| Amazon Today's Deals Scraper With Star Ratings & Deal Badges | The deals feed with badge labels, savings amounts, deal windows and star histograms |
| Amazon Top Rated Products Scraper | Keyword searches filtered by rating and review count, with rating breakdown and review insights |
| Alibaba Scraper | B2B product listings with price tiers, minimum order quantity and supplier details |
| Google Play Store Reviews Scraper | App review text and ratings for the mobile side of a product catalogue |
| Fragrantica Scraper With Dupe Similarity Scoring | Fragrance records with similarity scoring, for dupe and substitute research |
💬 Your feedback
Found a bug, or need a field that is on the Amazon page but not in the output — variantDetails, attributes and aiReviewsSummary are all placeholders in this build? Open an issue on the Actor's Issues tab and it will be looked at. Reports that include the exact input JSON and the Amazon URL you ran are the fastest to reproduce and fix.