An Amazon scraping API turns Amazon pages into JSON you can store: keyword search results, full product details by ASIN, best-seller rankings, deals and seller storefronts. This guide covers every Amazon endpoint ScrapingBot offers, with real responses, and finishes with a complete price and buy box monitor in Python and Node.js.
It is written for the jobs people actually build: competitor price tracking, assortment research, deal alerts and seller monitoring. Along the way you will see which fields to trust, how to page through results, what each call costs, and the things the API does not do, so you can rule it in or out quickly.
Key takeaways
- One endpoint,
POST /api/v1/amazon, with ten routes:/search,/product-details,/products,/autocomplete,/product-category-list,/best-sellers,/new-releases,/deals-v2,/seller-profileand/seller-products. - Every call costs 10 credits, except the batch route
/products, which costs 10 credits per product returned. Failed calls are refunded, and the 100 free credits cover 10 real calls. - The usual flow is search, shortlist, then
/product-detailsper ASIN. Lists such as best sellers often have no price, so enrich them by ASIN. - Price history is something you build: snapshot product details on a schedule and diff them. The script below does exactly that.
- Data comes from amazon.com only. There is no reviews endpoint and no country parameter.
Why use an API instead of scraping Amazon pages yourself
Fetching an Amazon product page is easy once. Keeping a scraper running against thousands of ASINs is where the time goes:
- Bot checks. Repeated requests from one address start getting a "Robot Check" CAPTCHA page instead of the product.
- Many layouts. Books, electronics, groceries and renewed products all lay out the price, availability and seller differently, and Amazon runs layout experiments constantly.
- Prices live in several places. The headline price, the buy box, strike-through list prices, coupons and other offers are separate elements. Picking the wrong one silently corrupts your data.
- Variations. One listing can cover dozens of child ASINs for colors and sizes, each with its own price and stock.
The general version of these problems, and how proxies and browsers help, is covered in the web scraping challenges guide and the proxy types comparison. An API moves that work off your plate: you send an ASIN and get named fields back. If you do want raw pages from other retailers, the general Website API handles rendering and proxies for any URL.
How the Amazon API works: one endpoint, ten routes
Every call is a POST to the same URL with an endpoint and a params object. Authenticate with an x-api-key header (a Bearer token or an api_key query parameter also work).
curl -X POST https://scrapingbot.io/api/v1/amazon \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endpoint": "/search", "params": {"query": "wireless earbuds"}}'
- Endpoint
POST /api/v1/amazon- Cost
- 10 credits per call (
/products: 10 per product) - Timeout
- 30 seconds
- Marketplace
- amazon.com (US)
| Route | Use it to | Parameters | Pages |
|---|---|---|---|
/search | Find products for a keyword | query (required), page, department | 1 to 100 |
/product-details | Get one product page by ASIN | asin (required) | None |
/products | Get up to 20 product pages in one call | asins (required) | None |
/autocomplete | Get Amazon's search-box suggestions | query (required), department, limit | None |
/product-category-list | Get subcategory names and links from Amazon's Best Sellers navigation | None | None |
/best-sellers | Get a department's best-seller ranking | category (required), page | 1 to 100 |
/new-releases | Get a department's new-release ranking | category (required), page | 1 to 100 |
/deals-v2 | Browse current deals | page | 1 to 100 |
/seller-profile | Look up a seller | seller_id (required), optional asin | None |
/seller-products | List a seller's storefront | seller_id (required), page | 1 to 100 |
Successful responses share one envelope: success, status, request_id, parameters (what was looked up, including "country": "US"), data, duration, statusCode and creditsUsed. Only data changes shape per route. Errors return success: false with an error message; a missing parameter or an unknown route is rejected with a 400 before anything is charged. The full parameter reference is in the Amazon API docs.
Here is the same request in Python and Node.js, fetching one product:
import requests
res = requests.post(
"https://scrapingbot.io/api/v1/amazon",
headers={"x-api-key": "YOUR_API_KEY"},
json={"endpoint": "/product-details", "params": {"asin": "B07ZPKN6YR"}},
timeout=45,
)
body = res.json()
if body.get("success"):
product = body["data"]
print(product["product_title"], product["product_price"], (product.get("main_buy_box") or {}).get("seller"))
else:
print(res.status_code, body.get("error"))
const res = await fetch('https://scrapingbot.io/api/v1/amazon', {
method: 'POST',
headers: { 'x-api-key': 'YOUR_API_KEY', 'content-type': 'application/json' },
body: JSON.stringify({ endpoint: '/product-details', params: { asin: 'B07ZPKN6YR' } }),
});
const body = await res.json();
if (body.success) {
const { product_title, product_price, main_buy_box } = body.data;
console.log(product_title, product_price, main_buy_box?.seller);
} else {
console.log(res.status, body.error);
}
Step 1: Search Amazon products by keyword
/search is the discovery call. It returns the products Amazon shows for a keyword, in order, with a light record per product: ASIN, title, price, list price, rating, review count, badge, and Prime and sponsored flags. This is a real response, trimmed:
{
"success": true,
"status": "OK",
"request_id": "f44ec798-fdd8-4b68-aad8-4955e04c3a02",
"parameters": { "query": "wireless earbuds", "country": "US", "department": "aps", "page": 1, "sort": "featured" },
"data": {
"page": 1,
"total_products": 16,
"has_next_page": true,
"next_page": 2,
"products": [
{
"position": 1,
"asin": "B0HDB41H28",
"product_title": "Wireless Earbuds, Bluetooth 5.3 Headphones HiFi Stereo 50H Playback…",
"product_price": "19.98",
"product_original_price": "249.99",
"currency": "USD",
"product_star_rating": "4.7",
"product_num_ratings": 128,
"is_prime": false,
"is_sponsored": false,
"badge": "Overall Pick",
"product_url": "https://www.amazon.com/…/dp/B0HDB41H28/…",
"product_photo": "https://m.media-amazon.com/images/I/618n7fFdFXL._AC_UY218_.jpg"
},
…
]
},
"duration": "2.55",
"statusCode": 200,
"creditsUsed": 10
}
A few things worth knowing before you build on it:
- Prices are strings.
"19.98", not19.98. Convert them before comparing, and expectnullwhen Amazon shows no price. - Sponsored results are flagged. Filter on
is_sponsoredif you want organic placement only, or keep them if you are studying who pays for the keyword. - Page size varies. This page returned 16 products. Use
has_next_pageandnext_pagerather than assuming a fixed count. departmentnarrows the search. The default isaps(All Departments), which you can see echoed inparameters. There is no sort parameter; results come in Amazon's default "featured" order.
Step 2: Build a shortlist of ASINs
Search results are noisy. Before spending a product-details call on every ASIN, filter the list down: drop repeats and sponsored placements, and keep products with enough ratings to matter. This helper walks up to three result pages and stops early when there are no more:
import requests
API_URL = "https://scrapingbot.io/api/v1/amazon"
HEADERS = {"x-api-key": "YOUR_API_KEY"}
def amazon(endpoint, **params):
r = requests.post(API_URL, headers=HEADERS, timeout=45,
json={"endpoint": endpoint, "params": params})
r.raise_for_status()
return r.json()["data"]
def shortlist(query, max_pages=3, min_ratings=100, min_stars=4.0):
picks, seen = [], set()
for page in range(1, max_pages + 1):
data = amazon("/search", query=query, page=page)
for p in data.get("products", []):
if p["asin"] in seen or p.get("is_sponsored"):
continue # skip repeats and paid placements
seen.add(p["asin"])
stars = float(p.get("product_star_rating") or 0)
ratings = p.get("product_num_ratings") or 0
if stars >= min_stars and ratings >= min_ratings:
picks.append((p["asin"], p.get("product_price"), stars, ratings, p["product_title"]))
if not data.get("has_next_page"):
break
return picks
for asin, price, stars, ratings, title in shortlist("wireless earbuds"):
print(asin, price, stars, ratings, title[:60])
const API_URL = 'https://scrapingbot.io/api/v1/amazon';
async function amazon(endpoint, params) {
const res = await fetch(API_URL, {
method: 'POST',
headers: { 'x-api-key': 'YOUR_API_KEY', 'content-type': 'application/json' },
body: JSON.stringify({ endpoint, params }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
return (await res.json()).data;
}
async function shortlist(query, { maxPages = 3, minRatings = 100, minStars = 4.0 } = {}) {
const picks = [];
const seen = new Set();
for (let page = 1; page <= maxPages; page++) {
const data = await amazon('/search', { query, page });
for (const p of data.products ?? []) {
if (seen.has(p.asin) || p.is_sponsored) continue; // skip repeats and paid placements
seen.add(p.asin);
const stars = Number(p.product_star_rating ?? 0);
const ratings = p.product_num_ratings ?? 0;
if (stars >= minStars && ratings >= minRatings) {
picks.push({ asin: p.asin, price: p.product_price, stars, ratings, title: p.product_title });
}
}
if (!data.has_next_page) break;
}
return picks;
}
for (const p of await shortlist('wireless earbuds')) {
console.log(p.asin, p.price, p.stars, p.ratings, p.title.slice(0, 60));
}
Three pages cost 30 credits and return up to about 50 products before filtering. Save the shortlist; it is the input to everything that follows.
Step 3: Get product details by ASIN
/product-details is the workhorse. One call returns the full product page as named fields: price, buy box, offer count, availability, rating, sales volume, category path, specifications, variations and photos. A real response for a renewed iPhone, trimmed:
{
"success": true,
"status": "OK",
"parameters": { "asin": "B07ZPKN6YR", "country": "US" },
"data": {
"asin": "B07ZPKN6YR",
"parent_asin": "B0GR1S2JM2",
"product_title": "Apple iPhone 11, 64GB, Black - Unlocked (Renewed)",
"product_price": "184.99",
"product_original_price": null,
"currency": "USD",
"product_availability": "In Stock",
"product_num_offers": 58,
"main_buy_box": {
"price": "$184.99",
"seller": "Woot",
"seller_id": "A1KWJVS57NX03I",
"return_policy": "FREE Returns",
"seller_link": "https://www.amazon.com/gp/help/seller/at-a-glance.html/…"
},
"buy_boxes": [],
"product_star_rating": "4.2",
"product_num_ratings": 60093,
"sales_volume": "1K+ bought in past month",
"is_best_seller": false,
"is_amazon_choice": false,
"is_prime": false,
"delivery": "FREE delivery Thursday, October 8",
"category": { "id": "2335752011", "name": "Cell Phones & Accessories" },
"category_path": [
{ "id": "2335752011", "name": "Cell Phones & Accessories", "link": "https://www.amazon.com/…" },
{ "id": "7072561011", "name": "Cell Phones", "link": "https://www.amazon.com/…" }
],
"product_details": { "Brand": "Apple", "Model Name": "iPhone 11", "Screen Size": "6.1 inches", … },
"product_variations": {
"color_name": [ { "asin": "B084GXXP31", "value": "Black", "is_available": true }, … ],
…
},
"about_product": [ "This phone is unlocked and compatible with any carrier of choice…", … ],
"product_photos": [ "https://m.media-amazon.com/images/I/61MG3m5FhIL._AC_SL1500_.jpg" ],
"top_reviews": [],
"rating_distribution": {},
…
},
"duration": "2.75",
"statusCode": 200,
"creditsUsed": 10
}
Notice the two prices. product_price is a bare number string ("184.99") while main_buy_box.price includes the currency symbol ("$184.99"). The buy box also tells you who is winning the sale (seller, seller_id) and how many competing offers exist (product_num_offers: 58 here). For a price monitor, those three fields matter most.
Variations come back as a map of dimension to options, each with its own ASIN and availability. To track every color of a product, collect the child ASINs from product_variations and fetch each one.
Product details field reference
These are the fields you are most likely to store. All of them appeared in our live test; fields marked "may be empty" are often null, [] or {} depending on the product.
| Field | Type | Example | Notes |
|---|---|---|---|
asin, parent_asin | string | B07ZPKN6YR | The parent groups all variations |
product_title | string | Apple iPhone 11, 64GB… | |
product_price | string or null | "184.99" | Number as text; no currency symbol |
product_original_price | string or null | "249.99" | The strike-through price, when shown |
currency | string | USD | |
main_buy_box | object | { price, seller, seller_id, … } | Price includes $; seller_id feeds the seller routes |
buy_boxes | array | [] | May be empty |
product_num_offers | integer | 58 | A count only; there is no offers list |
product_availability | string | In Stock | Free text, as Amazon shows it |
product_star_rating | string | "4.2" | Number as text |
product_num_ratings | integer | 60093 | |
sales_volume | string or null | 1K+ bought in past month | Amazon's rounded demand badge |
is_best_seller, is_amazon_choice, is_prime | boolean | false | |
delivery, delivery_price | string | FREE delivery Thursday, October 8 | For a default US address |
category, category_path | object, array | { id: "2335752011", name } | The breadcrumb, root first |
product_details | object | { "Brand": "Apple", … } | Spec table; keys vary by category |
product_variations | object | { color_name: [ … ] } | Child ASINs per dimension |
about_product | array | Bullet points | |
product_photos, product_videos | array | Image URLs | Videos may be empty |
top_reviews, rating_distribution | array, object | [], {} | May be empty; both were empty in our test |
To read a full payload comfortably, paste it into the JSON formatter. To turn the curl example into code for another language, use the curl converter.
Best-seller rankings by department
Best-seller lists show what is selling now, ranked, and they move daily. That makes them a strong input for trend spotting and assortment research. The ranking call needs a category: an Amazon department slug such as electronics, books or toys-and-games. Twenty departments are supported, and the docs list them all. /product-category-list takes no parameters and returns the subcategory names and links from Amazon's Best Sellers navigation:
{ "endpoint": "/product-category-list", "params": {} }
{
"success": true,
"parameters": { "category": "electronics", "country": "US", "list_type": "best_sellers" },
"data": {
"current_category": "electronics",
"list_type": "best_sellers",
"total_categories": 19,
"categories": [
{
"id": "electronics",
"name": "Headphones",
"selected": true,
"url": "https://www.amazon.com/Best-Sellers-Electronics-Headphones-Earbuds/zgbs/electronics/172541/…"
},
{
"id": "electronics",
"name": "Wearable Technology",
"selected": true,
"url": "https://www.amazon.com/Best-Sellers-Electronics-Wearable-Technology/zgbs/electronics/10048700011/…"
},
…
]
},
"duration": "1.27",
"statusCode": 200,
"creditsUsed": 10
}
The subcategory entries carry their names and Amazon's own best-seller URLs, but every entry has the same id, so use the list for names and links, not as category values for the rankings.
Use a department slug
In our tests on October 3, 2026, department slugs such as "electronics" returned a ranking, while a bare browse node number ("172282") returned an error. Errors are refunded, so a wrong value costs nothing.
{ "endpoint": "/best-sellers", "params": { "category": "electronics", "page": 1 } }
{
"success": true,
"parameters": { "category": "electronics", "country": "US", "list_type": "best_sellers", "page": 1 },
"data": {
"category": "electronics",
"list_type": "best_sellers",
"page": 1,
"total_products": 30,
"products": [
{
"rank": 2,
"position": 2,
"asin": "B0DCH8VDXF",
"product_title": "Apple EarPods Headphones with USB-C Plug, Wired Ear Buds with Built-in Remote…",
"product_price": null,
"product_star_rating": "4.5",
"product_num_ratings": 455,
"is_prime": false,
"is_sponsored": false,
"product_url": "https://www.amazon.com/Apple-EarPods-Headphones-Built-Control/dp/B0DCH8VDXF/…"
},
…
]
},
"duration": "0.99",
"statusCode": 200,
"creditsUsed": 10
}
Each product carries rank, rating and review count, and this page held 30 products. In our test product_price was null across the list, so treat the ranking as a list of ASINs and fetch prices with /product-details. Recording the ranking once a day and comparing day over day shows which products are climbing.
Amazon deals
/deals-v2 returns Amazon's current deals across the store, one page at a time. Unlike best sellers, deals carry prices: the deal price, the original price, the discount percentage and the deal badge.
{ "endpoint": "/deals-v2", "params": { "page": 1 } }
{
"success": true,
"parameters": { "country": "US", "page": 1 },
"data": {
"page": 1,
"total_products": 42,
"products": [
{
"position": 3,
"asin": "B00005UP2K",
"product_title": "KitchenAid Artisan, 5-Qt Tilt Head Stand Mixer, KSM150PS…",
"product_price": "379.95",
"product_original_price": "499.99",
"discount_percentage": 24,
"deal_badge": "Limited time deal",
"currency": "USD",
"coupon": null,
"product_url": "https://www.amazon.com/…/dp/B00005UP2K/…"
},
…
]
},
"duration": "1.66",
"statusCode": 200,
"creditsUsed": 10
}
The deals feed is not filtered by category, so filter on your side: by keywords in the title, by a minimum discount_percentage, or by matching ASINs you already track. The deals response has no has_next_page flag; page through until a page comes back empty or you have what you need. Deals change through the day, so poll more often around big sale events and less often otherwise.
Seller profile and storefront
Seller research usually starts from a product. The buy box in /product-details gives you main_buy_box.seller_id, and that ID feeds both seller routes:
{ "endpoint": "/seller-profile", "params": { "seller_id": "A1KWJVS57NX03I" } }
{ "endpoint": "/seller-products", "params": { "seller_id": "A2L77EE7U53NWQ", "page": 1 } }
/seller-profilereturns the seller's profile: name, ratings and storefront details. Pass an optionalasinfor a product they offer./seller-productslists the storefront, page by page, with the same product record as search.
{
"success": true,
"parameters": { "seller_id": "A2L77EE7U53NWQ", "country": "US", "page": 1, "sort": "featured" },
"data": {
"page": 1,
"total_products": 16,
"has_next_page": true,
"next_page": 2,
"products": [
{
"position": 4,
"asin": "B07CMS5Q6P",
"product_title": "Logitech G305 Lightspeed Wireless Gaming Mouse - Black",
"product_price": null,
"product_star_rating": "4.6",
"product_num_ratings": 401,
…
},
…
]
},
"duration": "1.39",
"statusCode": 200,
"creditsUsed": 10
}
Storefront listings, like best sellers, may omit prices, so enrich the ASINs you care about with /product-details. Snapshot a competitor's storefront weekly and diff the ASIN sets to see what they add and drop.
Pagination across the Amazon endpoints
Five routes paginate with a page parameter from 1 to 100. They signal the end differently:
| Route | Page size we saw | When to stop |
|---|---|---|
/search | 16 | has_next_page is false |
/seller-products | 16 | has_next_page is false |
/best-sellers, /new-releases | 30 | A page with no products |
/deals-v2 | 42 | A page with no products |
Page sizes are what Amazon happened to return in our tests, not guarantees. Every page is a separate 10-credit call, so cap the depth (three to five pages covers most research) and de-duplicate on asin as you go.
A complete Amazon price monitoring script (Python and Node.js)
This is the piece most teams end up building: a job that checks a list of ASINs, stores every snapshot, and tells you when the price, the buy box seller or the availability changes. The approach:
- Choose the ASINs to track. Collect them from a keyword search, a best-seller list, a seller storefront or your own catalog, and keep the list in code or a table.
- Fetch product details for each ASIN. Call /product-details once per ASIN per run, retrying 429 and 5xx responses with a growing delay.
- Normalize and store a snapshot. Turn price strings into numbers, pull the buy box seller and availability into columns, and keep the raw payload next to them.
- Compare with the previous snapshot. Report price moves above a threshold, buy box seller changes and availability changes.
- Run it on a schedule. Trigger the script from cron or a job runner, and size any parallelism to your plan's concurrency limit.
The Python version uses requests and the standard library's sqlite3. Set SCRAPINGBOT_API_KEY in the environment or replace the placeholder.
"""Amazon price and buy box monitor.
Run it on a schedule. Each run fetches every tracked ASIN, stores a
snapshot in SQLite and prints what changed since the previous snapshot.
"""
import json
import os
import sqlite3
import sys
import time
from datetime import datetime, timezone
import requests
API_URL = "https://scrapingbot.io/api/v1/amazon"
API_KEY = os.environ.get("SCRAPINGBOT_API_KEY", "YOUR_API_KEY")
ASINS = ["B07ZPKN6YR", "B0F3GWXLTS"] # the products you track
DB_PATH = "amazon_prices.db"
PRICE_ALERT_PCT = 2.0 # report price moves of at least this many percent
COLUMNS = ["asin", "taken_at", "title", "price", "buy_box_price", "buy_box_seller",
"buy_box_seller_id", "availability", "num_offers", "rating", "num_ratings", "raw"]
def fetch_product(asin, attempts=4):
"""Return the product's `data` object, or None if every attempt failed."""
for attempt in range(attempts):
try:
r = requests.post(
API_URL,
headers={"x-api-key": API_KEY},
json={"endpoint": "/product-details", "params": {"asin": asin}},
timeout=45,
)
if r.status_code == 200 and r.json().get("success"):
return r.json()["data"]
if r.status_code in (400, 401, 402):
print(f"{asin}: {r.status_code} {r.text[:200]}", file=sys.stderr)
return None # retrying will not help
except requests.RequestException:
pass
time.sleep(2 ** attempt) # 429, 5xx and timeouts are refunded, so retry
return None
def to_number(value):
"""'184.99' or '$1,184.99' -> float. None or '' -> None."""
if value in (None, ""):
return None
try:
return float(str(value).replace("$", "").replace(",", "").strip())
except ValueError:
return None
def snapshot(asin, data):
box = data.get("main_buy_box") or {}
return {
"asin": asin,
"taken_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"title": data.get("product_title"),
"price": to_number(data.get("product_price")),
"buy_box_price": to_number(box.get("price")),
"buy_box_seller": box.get("seller"),
"buy_box_seller_id": box.get("seller_id"),
"availability": data.get("product_availability"),
"num_offers": data.get("product_num_offers"),
"rating": to_number(data.get("product_star_rating")),
"num_ratings": data.get("product_num_ratings"),
"raw": json.dumps(data),
}
def diff(old, new):
found = []
if old["price"] and new["price"] and old["price"] != new["price"]:
pct = (new["price"] - old["price"]) / old["price"] * 100
if abs(pct) >= PRICE_ALERT_PCT:
found.append(f"price {old['price']:.2f} -> {new['price']:.2f} ({pct:+.1f}%)")
if old["buy_box_seller_id"] != new["buy_box_seller_id"]:
found.append(f"buy box {old['buy_box_seller']} -> {new['buy_box_seller']}")
if old["availability"] != new["availability"]:
found.append(f"availability {old['availability']!r} -> {new['availability']!r}")
return found
def main():
db = sqlite3.connect(DB_PATH)
db.row_factory = sqlite3.Row
db.execute(f"CREATE TABLE IF NOT EXISTS snapshots ({', '.join(COLUMNS)})")
db.execute("CREATE INDEX IF NOT EXISTS by_asin ON snapshots (asin, taken_at)")
for asin in ASINS:
data = fetch_product(asin)
if data is None:
print(f"{asin}: no data this run")
continue
new = snapshot(asin, data)
old = db.execute(
"SELECT * FROM snapshots WHERE asin = ? ORDER BY taken_at DESC LIMIT 1", (asin,)
).fetchone()
db.execute(f"INSERT INTO snapshots VALUES ({', '.join('?' * len(COLUMNS))})",
[new[c] for c in COLUMNS])
db.commit()
if old is None:
print(f"{asin}: first snapshot at {new['price']} (buy box: {new['buy_box_seller']})")
for change in diff(dict(old), new) if old else []:
print(f"{asin}: {change}")
if __name__ == "__main__":
main()
The Node.js version has no dependencies. It needs Node.js 22.13 or later for the built-in node:sqlite module, which still prints an "experimental" warning on Node.js 24; the warning is harmless.
// Amazon price and buy box monitor. Node.js 22.13 or later (built-in fetch and node:sqlite).
// Run it on a schedule. Each run stores a snapshot per ASIN and prints what changed.
import { DatabaseSync } from 'node:sqlite';
const API_URL = 'https://scrapingbot.io/api/v1/amazon';
const API_KEY = process.env.SCRAPINGBOT_API_KEY ?? 'YOUR_API_KEY';
const ASINS = ['B07ZPKN6YR', 'B0F3GWXLTS']; // the products you track
const PRICE_ALERT_PCT = 2; // report price moves of at least this many percent
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function fetchProduct(asin, attempts = 4) {
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const res = await fetch(API_URL, {
method: 'POST',
headers: { 'x-api-key': API_KEY, 'content-type': 'application/json' },
body: JSON.stringify({ endpoint: '/product-details', params: { asin } }),
signal: AbortSignal.timeout(45_000),
});
const body = await res.json().catch(() => ({}));
if (res.ok && body.success) return body.data;
if ([400, 401, 402].includes(res.status)) {
console.error(`${asin}: ${res.status} ${body.error ?? ''}`);
return null; // retrying will not help
}
} catch {
// network error or timeout: fall through and retry
}
await sleep(1000 * 2 ** attempt); // 429, 5xx and timeouts are refunded, so retry
}
return null;
}
// '184.99' or '$1,184.99' -> 1184.99. null or '' -> null.
const toNumber = (v) => {
if (v === null || v === undefined || v === '') return null;
const n = Number(String(v).replace(/[$,]/g, '').trim());
return Number.isFinite(n) ? n : null;
};
function snapshot(asin, d) {
const box = d.main_buy_box ?? {};
return {
asin,
taken_at: new Date().toISOString(),
title: d.product_title ?? null,
price: toNumber(d.product_price),
buy_box_price: toNumber(box.price),
buy_box_seller: box.seller ?? null,
buy_box_seller_id: box.seller_id ?? null,
availability: d.product_availability ?? null,
num_offers: d.product_num_offers ?? null,
rating: toNumber(d.product_star_rating),
num_ratings: d.product_num_ratings ?? null,
raw: JSON.stringify(d),
};
}
function diff(prev, next) {
const found = [];
if (prev.price && next.price && prev.price !== next.price) {
const pct = ((next.price - prev.price) / prev.price) * 100;
if (Math.abs(pct) >= PRICE_ALERT_PCT) {
found.push(`price ${prev.price.toFixed(2)} -> ${next.price.toFixed(2)} (${pct > 0 ? '+' : ''}${pct.toFixed(1)}%)`);
}
}
if (prev.buy_box_seller_id !== next.buy_box_seller_id) found.push(`buy box ${prev.buy_box_seller} -> ${next.buy_box_seller}`);
if (prev.availability !== next.availability) found.push(`availability "${prev.availability}" -> "${next.availability}"`);
return found;
}
const COLUMNS = ['asin', 'taken_at', 'title', 'price', 'buy_box_price', 'buy_box_seller',
'buy_box_seller_id', 'availability', 'num_offers', 'rating', 'num_ratings', 'raw'];
const db = new DatabaseSync('amazon_prices.db');
db.exec(`CREATE TABLE IF NOT EXISTS snapshots (${COLUMNS.join(', ')});
CREATE INDEX IF NOT EXISTS by_asin ON snapshots (asin, taken_at);`);
const latest = db.prepare('SELECT * FROM snapshots WHERE asin = ? ORDER BY taken_at DESC LIMIT 1');
const insert = db.prepare(`INSERT INTO snapshots VALUES (${COLUMNS.map(() => '?').join(', ')})`);
for (const asin of ASINS) {
const data = await fetchProduct(asin);
if (!data) {
console.log(`${asin}: no data this run`);
continue;
}
const next = snapshot(asin, data);
const prev = latest.get(asin);
insert.run(...COLUMNS.map((c) => next[c]));
if (!prev) console.log(`${asin}: first snapshot at ${next.price} (buy box: ${next.buy_box_seller})`);
for (const change of prev ? diff(prev, next) : []) console.log(`${asin}: ${change}`);
}
The first run stores a baseline. Later runs print only what changed, like this:
B07ZPKN6YR: price 184.99 -> 169.00 (-8.6%)
B07ZPKN6YR: buy box Woot -> OtherSeller
Why the script is built this way
- It stores the raw payload. When you later want
sales_volumeorproduct_num_offersas a trend, the history is already there. - It compares the buy box by
seller_id. Seller display names can change; the ID does not. - It uses a threshold. Small price wobbles are common; a 2% floor keeps alerts meaningful. Tune it per category.
- It retries only what is worth retrying.
429,5xxand timeouts are refunded, so a retry never pays twice.400,401and402need a fix, not a retry. - It skips a failed ASIN instead of recording a gap as a change. A missing snapshot is not a price drop.
To send alerts somewhere useful, replace the print or console.log calls with a Slack webhook, an email or a row in your own database.
Cost math and scheduling
Every Amazon call costs 10 credits, whatever the route. A useful way to plan is calls per month:
| Plan | Price / month | Credits | Amazon calls | Per 1,000 calls | Concurrency |
|---|---|---|---|---|---|
| Free | $0 | 100 on sign-up | 10 | n/a | 1 |
| Starter | $49.99 | 275K | 27,500 | $1.82 | 10 |
| Startup | $99.99 | 1.1M | 110,000 | $0.91 | 50 |
| Pro | $249.99 | 3.3M | 330,000 | $0.76 | 100 |
| Business | $599.99 | 9M | 900,000 | $0.67 | 200 |
| Business+ | $999.99 | 15.5M | 1,550,000 | $0.65 | 200 |
Some worked examples, using 30-day months:
- 200 ASINs every 6 hours: 200 × 4 × 30 = 24,000 calls, or 240,000 credits. Fits Starter.
- 1,000 ASINs once a day: 30,000 calls, or 300,000 credits. Just over Starter, so Startup.
- 5,000 ASINs twice a day: 300,000 calls, or 3,000,000 credits. Fits Pro.
- Weekly discovery on top: 50 keywords × 3 search pages × 4 weeks = 600 calls, or 6,000 credits.
Scheduling advice that saves both credits and headaches:
- Tier your ASINs. Check your closest competitors every few hours and the long tail daily or weekly.
- Match parallelism to your plan. Limits are on requests in flight, not per minute. Going over returns an immediate, free
429; see concurrency and plans. - Allow for slow calls. Amazon calls can take several seconds and time out at 30. Set your client timeout a little higher.
- Spread runs out. Stagger large batches across the hour instead of starting everything at :00.
What the Amazon API does not do
Better to know these now than after you have built on the wrong assumption:
- No reviews endpoint. Requests for review routes return
400 Unsupported Amazon endpoint.top_reviewsandrating_distributionexist on product details but are often empty. - US store only. Data comes from amazon.com. There is no country, marketplace or language parameter, so Amazon UK, Germany, Japan and the rest are not covered.
- No offers list. You get the winning buy box and a count of offers (
product_num_offers), not every seller's price. - No price history. Each call is a live snapshot. History is what you store.
- No sort or filter parameters on search beyond
query,pageanddepartment. - List prices can be missing. Best sellers and storefront listings may return
product_price: null; use product details for prices.
If you also need search engine data around the same products, such as which retailers rank for a product name, the Google Search API uses the same key and credits, and the Google scraping guide walks through a rank tracker built the same way as the monitor above.
Next steps
- Get a free key. Sign up for 100 credits with no card, enough for 10 Amazon calls.
- Try your real ASINs in the playground. Check the fields you need actually come back for your categories.
- Run the monitor on five ASINs twice, a few hours apart, and confirm the diffs look right.
- Scale the list and schedule it, using the cost table to pick a plan.
Working with an AI agent? The ScrapingBot MCP server exposes amazonSearch and amazonProduct tools, plus providerRequest for any other route, so Claude, Cursor and other MCP clients can call the Amazon API with your key. For the product overview and more examples, see the Amazon API page.
Frequently asked questions
Does Amazon have an official API for product data?
Amazon has APIs for specific programs: the Product Advertising API for members of its affiliate program and the Selling Partner API for sellers working with their own listings. Both require an account in that program and follow its rules. If you need public catalog data such as prices, rankings and deals for products you do not sell, a scraping API is the usual route.
Is it legal to scrape Amazon?
Collecting publicly visible product data is common practice, but Amazon's Conditions of Use restrict automated data gathering, and the rules that apply to you depend on your jurisdiction and what you do with the data. Stick to public product data, avoid personal data, and ask a lawyer before building a business on it. This is not legal advice.
How much does an Amazon API call cost?
Every Amazon endpoint costs 10 credits per successful call. Failed calls are refunded. The Starter plan ($49.99 for 275,000 credits) covers 27,500 calls a month, which works out to about $1.82 per 1,000 calls. New accounts get 100 free credits, enough for 10 calls.
Can I get Amazon reviews through the API?
Not as a dedicated endpoint. There is no reviews endpoint, and asking for one returns a 400 error. Product details include top_reviews and rating_distribution fields, but they are often empty, so do not plan a review-analysis product around them. The star rating and rating count are always the reliable signals.
Can I get data from Amazon UK, Germany or other marketplaces?
No. The data comes from the US store, amazon.com, and there is no country or language parameter. Each response echoes "country": "US" in parameters.
Can I get historical prices for an ASIN?
No. Every call returns the product as it is right now. Price history comes from taking snapshots on a schedule and storing them, which is what the monitoring script in this guide does.
Which categories can I use for best sellers?
Pass an Amazon department slug as category, such as electronics, books, toys-and-games or home-garden. Twenty departments are supported; the docs list them all. Bare browse node numbers return an error, and the ids from /product-category-list can't be used either.
How often should I check prices?
Match the schedule to how fast the product moves. Daily is enough for most catalogs; every few hours suits competitive categories and deal events. At 10 credits per check, 1,000 ASINs checked daily is 300,000 credits a month.