Amazon Product Scraper By Bestseller & Demand Filter avatar

Amazon Product Scraper By Bestseller & Demand Filter

Pricing

$19.99/month + usage

Go to Apify Store
Amazon Product Scraper By Bestseller & Demand Filter

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

Scraper Engine

Maintained by Community

Actor stats

1

Bookmarked

19

Total users

0

Monthly active users

4 days ago

Last modified

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 CategoryPublicly AvailableBehind 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 rateSeller Central, for your own listings only
Individual review text and reviewer names❌ Not returned by this Actor
Bulk catalogue feed under a licenceProduct 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 NameDescription
passedDemandFiltertrue when the row satisfies both demand thresholds. true for every row when both are left at 0
demandFilterReasonsArray of strings explaining each threshold that failed, e.g. "bestsellerRank 8421 > maxBestsellerRank 5000". null when the row passed
monthlyPurchasesNumberInteger floor parsed from the purchase badge: "3K+" becomes 3000. null when Amazon did not render the badge
monthlyPurchaseVolumeThe badge text as rendered, e.g. "3K+ bought in past month". null when absent
mainBestsellerRankRank number from the first entry in bestsellerRanks. null when the page shows no rank
mainBestsellerCategoryCategory name from that same first rank entry
estimatedMonthlySalesRangeObject, or null. The Actor's own wide estimate derived from mainBestsellerRanknot Amazon data. See the table below
bestsellerRanksArray of objects: rank, category — every rank the product-details table lists. null when none found
asin10-character Amazon ASIN, parsed from the URL
originalAsinSame as asin on a normal row; on a separately expanded variant row it holds the parent product's ASIN
titleProduct title from the page's title element
brandByline brand, with Visit the, Store and Brand: stripped
urlThe Amazon product URL the Actor fetched
inputThe first entry in your categoryOrProductUrls list — see the note below
unNormalizedProductUrlAlso the first entry in categoryOrProductUrls
priceObject: value (float), currency (symbol). null when no price element is readable
listPriceObject: value, currency — the struck-through was-price
priceRangeObject: min / max, each { value, currency } — only for listings that show a range
shippingPriceObject: value, currency — the first currency amount found in the delivery block
inStockBoolean. true when the availability text reads as in stock or an add-to-cart button exists
inStockTextCleaned availability line, e.g. "In Stock"
starsRating out of 5 as a float
starsBreakdownObject keyed 5star1star, values as fractions (0.86 = 86%). {} when the histogram is not readable
reviewsCountTotal ratings as an integer
hasReviewsBoolean, derived from reviewsCount being non-zero
reviewsLinkLink to the review page; falls back to a constructed /product-reviews/<asin>?reviewerType=all_reviews path
answeredQuestionsNumber from the answered-questions link
breadCrumbsAmazon's category trail joined with " > "
thumbnailImageMain product image URL
galleryThumbnailsArray of gallery thumbnail URLs, GIFs and play-icon sprites removed
highResolutionImagesArray of the same images rewritten to _SL1500_ — only entries whose URL could be rewritten appear
descriptionDescription paragraphs joined. Falls back to the first 800 characters of aPlusContent when Amazon shows no description block
featuresArray of feature-bullet strings longer than five characters
productOverviewArray of objects: key, value — merged from the overview block, technical-spec tables and detail bullets, deduplicated
aPlusContentA+ content text, truncated to 5,000 characters
brandStoryBrand-story text, truncated to 3,000 characters
importantInformationImportant-information block, truncated to 2,000 characters
sustainabilityFeaturesClimate Pledge / sustainability block, truncated to 1,500 characters
productComparisonArray of arrays — Amazon's comparison table as rows of cell strings. null when the widget is absent
returnPolicyReturn-policy text, truncated to 500 characters
supportProduct-support text, truncated to 500 characters
deliveryFirst weekday delivery date found in the delivery block, e.g. "Tuesday, July 28"
fastestDeliverySecond weekday date found in the same block
isAmazonChoiceBoolean — an Amazon's Choice badge is present
amazonChoiceTextBadge text, truncated to 200 characters. null when no badge
visitStoreLinkObject: text, url — the brand-store link
variantAsinsArray of sibling variant ASINs found on the page
sellerObject — always present. Six keys by default, two more with scrapeSellers. See the table below
offersArray of offer objects. [] unless maxOffers is above 0. See the table below
locationTextThe delivery location Amazon displayed, read from the page's location strip
loadedCountryCodeAn echo of your countryCode input, or "US" when you left it empty. Not read from the page
categoryPageDataObject: pageNumber, saleSummary, isSponsored, bestsellerBadge, productPosition
authorAlways null in this build
videosCountAlways 0 in this build
attributesAlways [] in this build
variantAttributesAlways [] in this build
manufacturerAttributesAlways [] in this build
variantDetailsAlways [] in this build
bookDescriptionAlways null in this build
aiReviewsSummaryAlways null in this build
productPageReviewsAlways [] in this build
productPageReviewsFromOtherCountriesAlways [] in this build
priceVariantsOnly present when scrapeProductVariantPrices is true and the product has variants. Array of objects: asin, url, price

input and unNormalizedProductUrl both report categoryOrProductUrls[0], not the start URL that actually produced the row. In a multi-URL run every row names the first URL in your list. Use url for 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 keyDescription
minLower bound of the estimated monthly unit band
maxUpper bound of the same band
categoryThe category the rank came from
basisRankThe rank the estimate was calculated from
categoryTier"large" when the category name matches one of Amazon's big top-level departments, otherwise "standard"
estimatedAlways true
methodThe 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 keyDescription
idAmazon merchant ID parsed from the seller link
nameSeller display name — five fallback selectors are tried in order
urlAbsolute seller-profile URL
businessNamenull unless scrapeSellers is true
phonenull unless scrapeSellers is true
addressnull unless scrapeSellers is true
averageRatingKey only added when scrapeSellers is true and the seller page was readable
reviewsCountKey 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[] keyDescription
priceObject: value, currency
shippingPriceObject: value, currency
conditionOffer heading text, e.g. "New", "Used - Very Good"
isPrimeBoolean — a Prime icon was present on the offer
sellerObject: 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.

  1. Open the Actor on Apify and click Try for free
  2. 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
  3. Set Max products per start URL (maxItemsPerStartUrl) explicitly rather than relying on a default
  4. Under Sales demand filters, set Minimum monthly purchases and Maximum Best Sellers Rank. Leave both at 0 to keep everything and just read the demand fields
  5. Leave Scrape product details on — the demand filters need product pages to read
  6. Leave Proxy configuration on Apify Residential; Amazon blocks datacenter traffic hard
  7. 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 minMonthlyPurchases and maxBestsellerRank, then ranks the passing rows by mainBestsellerRank against price and reviewsCount to 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.value against listPrice.value per asin to measure discount depth, using offers[].price to see whether a third-party seller is undercutting the buy box.
  • 📦 A reseller assessing margin turns on maxOffers and scrapeSellers and reads offers[].price, offers[].condition, offers[].isPrime and seller.averageRating to judge how crowded the offer page already is.
  • A product manager auditing quality complaints reads starsBreakdown and reviewsCount together, 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, productOverview and aPlusContent into a vector store keyed on asin, 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, mainBestsellerRank and monthlyPurchasesNumber across a category crawl and uses estimatedMonthlySalesRange only 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.

ParameterRequiredTypeDescriptionExample Value
categoryOrProductUrlsYesarrayAmazon 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"]
maxItemsPerStartUrlNointegerMaximum 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
minMonthlyPurchasesNointegerKeep products whose parsed purchase badge is at or above this number. Products with no badge are kept. Minimum 0. Default 0 (no minimum).1000
maxBestsellerRankNointegerKeep 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
languageNostringSets 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"
proxyCountryNostringApify 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"
maxSearchPagesPerStartUrlNointegerMaximum search result pages per start URL. Whichever of this and maxItemsPerStartUrl is reached first stops the crawl. Minimum 1. Default 9999.5
maxProductVariantsAsSeparateResultsNointegerMaximum 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
maxOffersNointegerMaximum offers per product to collect from the other-sellers panel. 0 collects none and leaves offers as []. Minimum 0. Default 0.5
scrapeSellersNobooleanFetch 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
ensureLoadedProductDescriptionFieldsNobooleanAccepted by the schema but not read anywhere in this build — switching it on changes nothing. Default false.false
useCaptchaSolverNobooleanAttempt 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
scrapeProductVariantPricesNobooleanFetch one page per sibling variant to collect its price, adding a priceVariants array to the row. Default false.false
scrapeProductDetailsNobooleantrue 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
maxConcurrentRequestsNointegerConcurrent 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
countryCodeNostringDelivery country set on each session at warmup, 240 options. Echoed back in loadedCountryCode. No default."US"
zipCodeNostringDelivery postcode set on each session at warmup, for finer localisation than country alone. No default."10001"
locationDeliverableRoutesNoarrayPage 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"]
proxyConfigurationNoobjectApify 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 a k= keyword parameter, the Actor constructs a fresh https://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 without k= keep every parameter and simply get &page=N appended, so those are the safer input shape for a filtered multi-page crawl.
  • Non-.com marketplaces work for direct product URLs only. A /dp/ URL on amazon.de is fetched on amazon.de. But product links harvested from a category or search page are rebuilt as https://www.amazon.com/dp/<asin>, and session warmup, the delivery-location call, the offers endpoint, seller pages and variant lookups are all hardcoded to amazon.com. For a non-US marketplace, supply product URLs directly, or use scrapeProductDetails: false — the lighter path keeps the domain from the page's own links.
  • locationDeliverableRoutes is 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.
  • countryCode and zipCode are 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. Check locationText in the output — that is read from the page — rather than loadedCountryCode, which only echoes your input.
  • ensureLoadedProductDescriptionFields is accepted and ignored. No code path reads it.
  • variantDetails is always empty, despite the maxProductVariantsAsSeparateResults description pointing you to it as the default-output alternative. If you need per-variant data, either expand variants as separate rows or use scrapeProductVariantPrices.
  • maxItemsPerStartUrl: 0 yields no products from category or search URLs, while direct product URLs are still scraped.
  • maxConcurrentRequests does 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 ApifyClient
client = 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.

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.

Scraper NameWhat it extracts
Amazon ASIN Scraper By Country & MarketplaceOne ASIN list priced across marketplaces — USD conversion, price rank, cheapest-marketplace gap
Amazon Today's Deals Scraper With Star Ratings & Deal BadgesThe deals feed with badge labels, savings amounts, deal windows and star histograms
Amazon Top Rated Products ScraperKeyword searches filtered by rating and review count, with rating breakdown and review insights
Alibaba ScraperB2B product listings with price tiers, minimum order quantity and supplier details
Google Play Store Reviews ScraperApp review text and ratings for the mobile side of a product catalogue
Fragrantica Scraper With Dupe Similarity ScoringFragrance 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.