How to Scrape Amazon with Python (2026 Guide)
Run a typical "scrape Amazon with Python" tutorial today and you'll probably get this:
Title: Anker USB C to USB C Cable, 60W Fast Charging Cable (2-Pack, 6 ft, Black) Price: None Rating: 4.7 out of 5 stars Reviews: (90,152) Ships to: Ukraine
The title and rating come through, but the price is missing. Amazon decides what to show based on your delivery location, and it guesses that location from your IP. We ran this from a non-US server, so Amazon showed "This item cannot be shipped to your selected delivery location", and no price at all.
That's one of six things we found when we tested Amazon scraping in October 2026. This guide gives you code that works with today's Amazon: product pages, search results, prices for a specific US ZIP code, top reviews, and handling for the blocks you'll hit.
Quick answer
A minimal product scraper with requests and BeautifulSoup:
import requests from bs4 import BeautifulSoup headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36", "Accept-Language": "en-US,en;q=0.9", } r = requests.get("https://www.amazon.com/dp/B088NRLMPV", headers=headers, timeout=30) soup = BeautifulSoup(r.text, "lxml") def text(selector): el = soup.select_one(selector) return el.get_text(" ", strip=True) if el else None print("Title: ", text("#productTitle")) print("Price: ", text("#corePrice_feature_div .a-offscreen")) print("Rating: ", text("#acrPopover span.a-icon-alt")) print("Reviews: ", text("#acrCustomerReviewText")) print("Ships to:", text("#glow-ingress-line2"))
If Price is None and Ships to isn't a US location, you need a US IP or a US ZIP code (see Step 3). For more than a handful of pages, you also need rotating residential proxies and block detection (see Step 5). The full script at the end does all of it.
What we found testing Amazon in October 2026
We tested on amazon.com with Python 3.13, requests 2.34, beautifulsoup4 4.15 and curl_cffi 0.16, sending a few dozen requests spaced several seconds apart.
Your location decides whether you see prices. From a non-US IP, product pages showed "Deliver to Ukraine" and no price. After setting ZIP code 10001 through Amazon's own "Deliver to" form, the same page showed $237.49 and "Only 1 left in stock".
The default python-requests User-Agent gets a 503 right away. With browser-like headers, the same URL returned 200.
Amazon sometimes returns a JavaScript challenge instead of HTML. The response is 200 OK but only ~2 KB of JavaScript (Akamai's bm-verify interstitial), with zero products. It appeared on some first requests and not on others. Status codes won't tell you: you have to check the content.
CAPTCHAs come fast from a single IP. The second product page we requested from the same IP within a few seconds got the "Enter the characters you see below" page. That's what rotating proxies are for.
A popular selector broke. h2 a span, used in several top-ranking tutorials for search result titles, now returns nothing. The title is in h2 span, and the link wraps the h2.
The "See all reviews" page now requires sign-in. /product-reviews/{ASIN} redirects to the login page. Without an account, you get only the top reviews on the product page, and sometimes not even those.
Is it legal to scrape Amazon?
This isn't legal advice, but here's the practical line most teams follow:
Public data only. Prices, titles, ratings and product details visible without logging in are generally considered public. Don't scrape behind a login: it's clearly against Amazon's terms, and your account can be banned.
No personal data. Review texts are fine for sentiment analysis, but don't collect reviewer names or profile links. Under GDPR and similar laws, that's personal data you probably don't need.
Be gentle. Keep request rates reasonable, cache what you already have, and don't hammer the same pages.
Amazon's Conditions of Use restrict automated access. Read them and decide with your legal team, especially for commercial use.
Setup
pip install requests beautifulsoup4 lxml
That's all the full script needs. We used Python 3.13; any Python 3.10+ should work.
Step 1: Fetch the page and detect blocks
Amazon rarely tells you it blocked you with a clear status code. Check the HTML for known block pages:
BLOCK_MARKERS = ( "validateCaptcha", # "Enter the characters you see below" "bm-verify", # JavaScript interstitial challenge "api-services-support@amazon.com", # "Sorry! Something went wrong" (503 page) )
def _get(self, url, **kwargs): time.sleep(random.uniform(*self.delay)) r = self.session.get(url, timeout=30, **kwargs) if r.status_code in (429, 503) or any(m in r.text for m in BLOCK_MARKERS): raise Blocked(f"{r.status_code} on {url}") r.raise_for_status() return r.text
Without this check, your scraper will happily parse a CAPTCHA page, find no products, and save empty rows without any error.
Step 2: Parse a product page
These selectors worked on amazon.com product pages in October 2026:
Field | Selector | Example value |
|---|---|---|
Title |
| Apple AirPods Pro 2 Wireless Earbuds… |
Price |
| $237.49 |
Rating |
| 4.6 out of 5 stars |
Review count |
| (47,013) |
Availability |
| In Stock |
Brand |
| Visit the Apple Store |
Bullet points |
| 10 items |
Main image (hi-res) |
| …AC_SL1500.jpg |
Delivery location |
| New York 10001 |
Selectors that returned nothing in our tests but still appear in older tutorials: #priceblock_ourprice and #SalesRank.
Always save the delivery location next to the price. If a price looks wrong, the first thing to check is which location Amazon priced it for.
def parse_product(html, asin): soup = BeautifulSoup(html, "lxml") image = soup.select_one("#landingImage") return { "asin": asin, "title": text(soup, "#productTitle"), "price": to_price(text(soup, "#corePrice_feature_div .a-offscreen") or text(soup, ".a-price .a-offscreen")), "rating": to_rating(text(soup, "#acrPopover span.a-icon-alt")), "reviews": to_count(text(soup, "#acrCustomerReviewText")), "availability": text(soup, "#availability"), "brand": text(soup, "#bylineInfo"), "bullets": [li.get_text(" ", strip=True) for li in soup.select("#feature-bullets li")], "image": image.get("data-old-hires") if image else None, "delivery_location": text(soup, "#glow-ingress-line2"), "top_reviews": [...], # see Step 6 }
Small helpers turn Amazon's text into numbers: "$1,234.56" → 1234.56, "4.6 out of 5 stars" → 4.6, "(90.1K)" → 90100. They're in the full script.
Step 3: Set the delivery ZIP code to get real prices
Prices, availability, delivery dates and even search rankings depend on the delivery address. You have two levers:
A US IP address. Amazon guesses your location from it. A US residential proxy gives you a realistic US location by default.
An explicit ZIP code. The "Deliver to" button lets any visitor pick a ZIP without signing in. Your scraper can do the same three requests the browser does:
def set_zip_code(self, zip_code): # 1. Any product page has the "Deliver to" button with a CSRF token. html = self._get(f"{BASE_URL}/dp/B0D1XD1ZV3") button = BeautifulSoup(html, "lxml").select_one("#nav-global-location-data-modal-action") if button is None: raise Blocked("location button not found") modal = json.loads(button["data-a-modal"]) # 2. The location popup returns a second token. popup = self._get(BASE_URL + modal["url"], headers=modal["ajaxHeaders"]) token = re.search(r'CSRF_TOKEN\s*:\s*"([^"]+)"', popup) if token is None: raise Blocked("location token not found") # 3. Submit the ZIP code, exactly like the "Apply" button does. r = self.session.post( f"{BASE_URL}/portal-migration/hz/glow/address-change", params={"actionSource": "glow"}, json={"locationType": "LOCATION_INPUT", "zipCode": zip_code, "deviceType": "web", "storeContext": "generic", "pageType": "Detail", "actionSource": "glow"}, headers={"anti-csrftoken-a2z": token.group(1), "Accept": "application/json"}, timeout=30, ) if not r.ok or not r.json().get("successful"): raise RuntimeError(f"Could not set ZIP code {zip_code}: {r.text[:200]}")
In our test, the response confirmed "countryCode":"US","zipCode":"10001","city":"NEW YORK", and every page in that session showed "Deliver to New York 10001" with US prices.
The ZIP code is an Amazon setting, not a proxy feature. You don't need a proxy provider with ZIP-level targeting: we set ZIP 10001 from a non-US IP without any proxy at all. The proxy's job is different: a US residential IP keeps Amazon from blocking you and makes the session look like a normal US shopper. The ZIP then pins the exact delivery address.
The ZIP lives in the session cookies. Two consequences:
Use one session per location. To compare prices in New York and Los Angeles, run two scrapers with zip_code="10001" and zip_code="90001".
Keep the same IP for the whole session. Cookies that suddenly arrive from a different IP look suspicious. Use sticky proxy sessions (the same IP for several minutes) rather than a new IP on every request. When you get blocked, start a new session: new IP, new cookies, ZIP set again.
Step 4: Scrape search results
Search pages are where you find ASINs, track rankings and watch competitors' prices.
def parse_search(html): soup = BeautifulSoup(html, "lxml") results, seen = [], set() positions = {True: 0, False: 0} # ads and organic results are ranked separately for item in soup.select('div[data-component-type="s-search-result"]'): asin = item.get("data-asin") sponsored = item.select_one(".puis-sponsored-label-text") is not None # The same product can appear twice: as an ad and as an organic result. Keep both. if not asin or (asin, sponsored) in seen: continue seen.add((asin, sponsored)) positions[sponsored] += 1 results.append({ "position": positions[sponsored], "asin": asin, "title": text(item, "h2 span"), "url": f"{BASE_URL}/dp/{asin}", "price": to_price(text(item, ".a-price .a-offscreen")), "rating": to_rating(text(item, ".a-icon-alt")), "reviews": to_count(text(item, "a[href*='customerReviews'] span")), "sponsored": sponsored, "image": (item.select_one("img.s-image") or {}).get("src"), }) return results
Three details that most tutorials miss:
Ads and organic results are ranked separately. In our test for "usb c cable", the same LISEN cable was sponsored #1 and organic #2. If you deduplicate by ASIN only, you lose one of those positions, and rank tracking breaks.
Build the URL from the ASIN. Links on sponsored results go through an ad redirect (/sspa/click?...). https://www.amazon.com/dp/{ASIN} is clean and stable.
Don't read "list price" from search cards. In our test, .a-text-price on search cards held the price per unit (like $4.49/count), not the old price.
Pagination: add &page=2, &page=3 and so on to the search URL, or follow a.s-pagination-next. A page returned 16 to 22 results in our tests, depending on the layout Amazon served.
Step 5: Handle blocks with retries and proxies
From a single IP, we got a CAPTCHA on the second product page. At any real volume you need:
Delays with jitter. The scraper waits 3–7 seconds between requests by default.
Block detection (Step 1).
A new identity on every block: new IP, new cookies, ZIP set again, then retry with exponential backoff.
def fetch(self, url, attempts=4): for attempt in range(attempts): try: return self._get(url) except (Blocked, requests.RequestException) as e: print(f" attempt {attempt + 1} failed: {e}") time.sleep(2 ** attempt * 5) try: self.new_session() except (Blocked, requests.RequestException): pass # the next attempt will try again raise RuntimeError(f"Giving up on {url}")
new_session() picks a random proxy from proxies.txt, one proxy URL per line:
http://USERNAME:PASSWORD@PROXY_HOST:PORT_1 http://USERNAME:PASSWORD@PROXY_HOST:PORT_2 http://USERNAME:PASSWORD@PROXY_HOST:PORT_3
Which proxies work for Amazon:
Proxy type | Result on Amazon |
|---|---|
Datacenter | Blocked or CAPTCHA-ed quickly. Amazon knows cloud IP ranges. |
Residential (US) | Best default. Real home ISP IPs, realistic US location, prices visible. |
Mobile (4G/5G) | Highest trust, useful for the toughest volumes, but more expensive per GB. |
With SimplyNode, set the location to United States (or a specific state or city), choose sticky rotation by time (10–30 minutes works well for a scraping session), and export the proxy list. Each line is a separate endpoint with its own IP, so new_session() switches to a different IP on every block. See proxies for Amazon for setup details.
Plain requests was enough in our tests. If you start seeing the JavaScript challenge (bm-verify) on most requests even through residential IPs, the next step is a real browser: see our Selenium proxy guide. A browser runs the challenge's JavaScript like any visitor would.
Step 6: Top reviews (without signing in)
Since the full reviews page now requires sign-in, the reviews you can get without an account are the ones shown on the product page itself, about a dozen. Some product pages don't include them in the initial HTML at all: in our tests, the same product had 13 reviews in one response and none in another.
"top_reviews": [ { # no reviewer names: that's personal data you don't need "rating": to_rating(text(r, "[data-hook=review-star-rating]")), "title": text(r, "[data-hook=reviewTitle]"), "body": text(r, "[data-hook=reviewRichContentContainer]"), "date": text(r, "[data-hook=review-date]"), "verified": r.select_one("[data-hook=avp-badge]") is not None, } for r in soup.select("[data-hook=review]") ],
Note the selectors: older tutorials use [data-hook=review-title] and [data-hook=review-body], which returned nothing in our tests. Today's are reviewTitle and reviewRichContentContainer.
Don't log in to scrape reviews. It's clearly against Amazon's terms, the account will likely be banned, and everything you collect is tied to your identity.
The full script
Save as amazon_scraper.py, optionally create proxies.txt, and run python amazon_scraper.py. It sets ZIP 10001, searches for "usb c cable", scrapes the top 3 organic products, and saves amazon_search.csv and amazon_products.json.
"""amazon_scraper.py: scrape Amazon search results and product pages with Python. Tested on amazon.com in October 2026. Amazon changes its HTML often: if a field comes back empty, check the selectors first. """ import csv import json import os import random import re import time import requests from bs4 import BeautifulSoup BASE_URL = "https://www.amazon.com" HEADERS = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", } BLOCK_MARKERS = ( "validateCaptcha", # "Enter the characters you see below" "bm-verify", # JavaScript interstitial challenge "api-services-support@amazon.com", # "Sorry! Something went wrong" (503 page) ) class Blocked(Exception): pass def load_proxies(path="proxies.txt"): """One proxy URL per line, e.g. http://user:pass@host:port. Empty list = no proxy.""" if not os.path.exists(path): return [] with open(path) as f: return [line.strip() for line in f if line.strip()] class AmazonScraper: def __init__(self, zip_code="10001", proxies=None, delay=(3, 7)): self.zip_code = zip_code self.proxies = proxies or [] self.delay = delay self.session = None self.new_session() # --- session and location ------------------------------------------------- def new_session(self): """Fresh cookies, next proxy (= new IP), and the delivery ZIP set again.""" self.session = requests.Session() self.session.headers.update(HEADERS) if self.proxies: proxy = random.choice(self.proxies) self.session.proxies.update({"http": proxy, "https": proxy}) if self.zip_code: self.set_zip_code(self.zip_code) def set_zip_code(self, zip_code): # 1. Any product page has the "Deliver to" button with a CSRF token. html = self._get(f"{BASE_URL}/dp/B0D1XD1ZV3") button = BeautifulSoup(html, "lxml").select_one("#nav-global-location-data-modal-action") if button is None: raise Blocked("location button not found") modal = json.loads(button["data-a-modal"]) # 2. The location popup returns a second token. popup = self._get(BASE_URL + modal["url"], headers=modal["ajaxHeaders"]) token = re.search(r'CSRF_TOKEN\s*:\s*"([^"]+)"', popup) if token is None: raise Blocked("location token not found") # 3. Submit the ZIP code, exactly like the "Apply" button does. r = self.session.post( f"{BASE_URL}/portal-migration/hz/glow/address-change", params={"actionSource": "glow"}, json={"locationType": "LOCATION_INPUT", "zipCode": zip_code, "deviceType": "web", "storeContext": "generic", "pageType": "Detail", "actionSource": "glow"}, headers={"anti-csrftoken-a2z": token.group(1), "Accept": "application/json"}, timeout=30, ) if not r.ok or not r.json().get("successful"): raise RuntimeError(f"Could not set ZIP code {zip_code}: {r.text[:200]}") # --- fetching --------------------------------------------------------------- def _get(self, url, **kwargs): time.sleep(random.uniform(*self.delay)) r = self.session.get(url, timeout=30, **kwargs) if r.status_code in (429, 503) or any(m in r.text for m in BLOCK_MARKERS): raise Blocked(f"{r.status_code} on {url}") r.raise_for_status() return r.text def fetch(self, url, attempts=4): for attempt in range(attempts): try: return self._get(url) except (Blocked, requests.RequestException) as e: print(f" attempt {attempt + 1} failed: {e}") time.sleep(2 ** attempt * 5) try: self.new_session() except (Blocked, requests.RequestException): pass # the next attempt will try again raise RuntimeError(f"Giving up on {url}") # --- public API -------------------------------------------------------------- def search(self, query, page=1): html = self.fetch(f"{BASE_URL}/s?k={requests.utils.quote(query)}&page={page}") return parse_search(html) def product(self, asin): html = self.fetch(f"{BASE_URL}/dp/{asin}") return parse_product(html, asin) # --- parsing ---------------------------------------------------------------------- def text(node, selector): el = node.select_one(selector) return el.get_text(" ", strip=True) if el else None def to_price(value): if not value: return None match = re.search(r"[\d,]+\.?\d*", value) return float(match.group().replace(",", "")) if match else None def to_rating(value): match = re.search(r"\d+(\.\d+)?", value or "") return float(match.group()) if match else None def to_count(value): """'(47,013)' -> 47013, '(90.1K)' -> 90100""" match = re.search(r"([\d,.]+)\s*([KM]?)", value or "") if not match: return None number = float(match.group(1).replace(",", "")) return int(number * {"": 1, "K": 1_000, "M": 1_000_000}[match.group(2)]) def parse_search(html): soup = BeautifulSoup(html, "lxml") results, seen = [], set() positions = {True: 0, False: 0} # ads and organic results are ranked separately for item in soup.select('div[data-component-type="s-search-result"]'): asin = item.get("data-asin") sponsored = item.select_one(".puis-sponsored-label-text") is not None # The same product can appear twice: as an ad and as an organic result. Keep both. if not asin or (asin, sponsored) in seen: continue seen.add((asin, sponsored)) positions[sponsored] += 1 results.append({ "position": positions[sponsored], "asin": asin, "title": text(item, "h2 span"), "url": f"{BASE_URL}/dp/{asin}", "price": to_price(text(item, ".a-price .a-offscreen")), "rating": to_rating(text(item, ".a-icon-alt")), "reviews": to_count(text(item, "a[href*='customerReviews'] span")), "sponsored": sponsored, "image": (item.select_one("img.s-image") or {}).get("src"), }) return results def parse_product(html, asin): soup = BeautifulSoup(html, "lxml") image = soup.select_one("#landingImage") return { "asin": asin, "title": text(soup, "#productTitle"), "price": to_price(text(soup, "#corePrice_feature_div .a-offscreen") or text(soup, ".a-price .a-offscreen")), "rating": to_rating(text(soup, "#acrPopover span.a-icon-alt")), "reviews": to_count(text(soup, "#acrCustomerReviewText")), "availability": text(soup, "#availability"), "brand": text(soup, "#bylineInfo"), "bullets": [li.get_text(" ", strip=True) for li in soup.select("#feature-bullets li")], "image": image.get("data-old-hires") if image else None, "delivery_location": text(soup, "#glow-ingress-line2"), "top_reviews": [ { # no reviewer names: that's personal data you don't need "rating": to_rating(text(r, "[data-hook=review-star-rating]")), "title": text(r, "[data-hook=reviewTitle]"), "body": text(r, "[data-hook=reviewRichContentContainer]"), "date": text(r, "[data-hook=review-date]"), "verified": r.select_one("[data-hook=avp-badge]") is not None, } for r in soup.select("[data-hook=review]") ], } if __name__ == "__main__": scraper = AmazonScraper(zip_code="10001", proxies=load_proxies()) results = scraper.search("usb c cable") print(f"Found {len(results)} products") products = [] for item in [r for r in results if not r["sponsored"]][:3]: p = scraper.product(item["asin"]) print(f'{p["asin"]} ${p["price"]} {p["rating"]}★ ({p["reviews"]}) {p["title"][:60]}') products.append(p) with open("amazon_search.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=results[0].keys()) writer.writeheader() writer.writerows(results) with open("amazon_products.json", "w") as f: json.dump(products, f, indent=2, ensure_ascii=False)
Output from our test run (October 21, 2026):
Found 16 products B088NRLMPV $9.99 4.7★ (90152) Anker USB C to USB C Cable, 60W Fast Charging Cable (2-Pack, B088NMR44C $9.99 4.7★ (90152) Anker USB C to USB C Cable, 60W Fast Charging Cable (2-Pack, B0HJBWVSSS $6.99 5.0★ (1) Mcbazel USB-C Charging Cable for Wii U Gamepad, Type-C Femal
And the CSV:
position,asin,title,url,price,rating,reviews,sponsored,image 1,B088NRLMPV,"Anker USB C to USB C Cable, 60W Fast Charging Cable (2-Pack, 6 ft, Black)",https://www.amazon.com/dp/B088NRLMPV,9.99,4.7,90100,False,https://m.media-amazon.com/images/I/71IE9dLBduL._AC_UY218_.jpg 2,B088NMR44C,"Anker USB C to USB C Cable, 60W Fast Charging Cable (2-Pack, 3.3 ft, Black)",https://www.amazon.com/dp/B088NMR44C,9.99,4.7,90100,False,https://m.media-amazon.com/images/I/71btI2mQBmL._AC_UY218_.jpg
Keep your scraper alive
Amazon changes its HTML often, and different visitors get different layouts. Some habits that save you from silently collecting empty data:
Monitor empty fields. If more than ~10% of products come back with title or price = None, stop and check the selectors. That's how we noticed h2 a span had broken.
Save the raw HTML of failed pages. You can't debug a CAPTCHA you didn't keep.
Cache. Prices change, but titles, images and bullet points don't change daily. Re-scrape those weekly, prices daily or hourly.
Scrape by ASIN. Once you know which products you track, go straight to /dp/{ASIN} instead of searching every time. It's fewer requests and less load.
Store the delivery location with every price. Without it, a price change and a location change look the same.
Estimate proxy traffic
Proxy traffic is measured on the wire, after compression. requests asks for gzip by default, so in our tests an amazon.com product page cost about 380 KB of traffic (1.4–1.9 MB once unpacked), and a search page about 160 KB. Plain requests downloads only the HTML, not images or scripts.
Product pages per day | Traffic per day | Traffic per month |
|---|---|---|
1,000 | ~0.4 GB | ~11 GB |
10,000 | ~3.8 GB | ~115 GB |
100,000 | ~38 GB | ~1.1 TB |
That's roughly 2,700 product pages per GB. Add 10–20% for retries and the ZIP setup on each new session. A headless browser loads images and scripts too, so expect several times more traffic per page with Selenium or Playwright. More: How much proxy bandwidth do you actually need?
Troubleshooting
Symptom | Cause | Fix |
|---|---|---|
Price is | Non-US location | Use a US residential IP and/or set a ZIP code (Step 3). |
| Default User-Agent or IP flagged | Send browser headers. Rotate to a new IP. |
| JavaScript challenge ( | Detect it (Step 1), retry from a new session and IP. If it persists, use a real browser. |
“Enter the characters you see below” | CAPTCHA after too many requests per IP | Slow down, rotate IPs, new session per block. |
Search titles empty | Old selector | Use |
| Full reviews page requires login since late 2024 | Use top reviews on the product page. Don’t log in. |
Same ASIN twice in search results | Ad + organic placement | Keep both, track ad and organic positions separately. |
Prices in the wrong currency | IP or ZIP for another country | Match the proxy country to the marketplace. |
FAQ
Can you scrape Amazon with Python?
Yes. requests with browser-like headers and BeautifulSoup can parse Amazon product and search pages. For more than a few pages, you'll also need block detection, delays and rotating residential proxies, because Amazon shows CAPTCHAs quickly to a single IP.
Why does my Amazon scraper return no price?
Most often because Amazon thinks you're outside the US and can't ship there, so it doesn't show a price. Use a US IP, or set a US ZIP code through the "Deliver to" form, as shown in Step 3. Also check that you're not parsing a CAPTCHA page.
What are the current CSS selectors for Amazon prices?
On product pages, #corePrice_feature_div .a-offscreen, with .a-price .a-offscreen as a fallback. On search pages, .a-price .a-offscreen inside each div[data-component-type="s-search-result"]. We tested these in October 2026.
How do I scrape Amazon prices for a specific ZIP code?
Submit the ZIP through the same endpoint the "Deliver to" popup uses: read the CSRF token from the location button, load the popup, then POST the ZIP to /portal-migration/hz/glow/address-change. The location then sticks to your session cookies.
Can I scrape all Amazon reviews?
Not without signing in. Since late 2024, the full reviews page requires a login, and logging in to scrape violates Amazon's terms. Without an account, you can collect the top reviews shown on each product page.
Do I need proxies to scrape Amazon?
For anything beyond a few pages, yes. In our test, the second request from the same IP got a CAPTCHA. US residential proxies with sticky sessions work best: they give you a realistic US location and keep the same IP for a whole session.
Is it legal to scrape Amazon?
Scraping publicly visible data is generally treated differently from accessing data behind a login, but Amazon's terms restrict automated access. Collect only public data, skip personal data like reviewer names, keep request rates low, and check with a lawyer for commercial use.
Should I use Selenium or requests for Amazon?
Start with requests: it's faster and uses far less proxy traffic. Switch to a browser (Selenium or Playwright) only if the JavaScript challenge appears on most requests, or if you need data that only loads with JavaScript.
Scrape Amazon without blocks
Everything in this guide works best with US residential IPs. SimplyNode gives you:
8M+ residential IPs, with US state and city targeting, so your scraper starts as a realistic US shopper (set the exact ZIP in code, as in Step 3)
Sticky sessions from 90 seconds to 6 hours, so a whole scraping session keeps one IP and its ZIP cookies
Unlimited concurrent sessions for parallel scrapers
Pay-as-you-go from $2.25/GB, with traffic that doesn't expire
Related reading
:format(webp))