E-commerce

Amazon Scraping API: The Complete Guide to Product Data and Price Tracking

Every Amazon endpoint with real responses: keyword search, ASIN details, best sellers, deals and sellers, plus a price and buy box monitor in Python and Node.js.

ScrapingBot 13 min read
Illustration for Amazon Scraping API: The Complete Guide to Product Data and Price Tracking
On this page

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-profile and /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-details per 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)
RouteUse it toParametersPages
/searchFind products for a keywordquery (required), page, department1 to 100
/product-detailsGet one product page by ASINasin (required)None
/productsGet up to 20 product pages in one callasins (required)None
/autocompleteGet Amazon's search-box suggestionsquery (required), department, limitNone
/product-category-listGet subcategory names and links from Amazon's Best Sellers navigationNoneNone
/best-sellersGet a department's best-seller rankingcategory (required), page1 to 100
/new-releasesGet a department's new-release rankingcategory (required), page1 to 100
/deals-v2Browse current dealspage1 to 100
/seller-profileLook up a sellerseller_id (required), optional asinNone
/seller-productsList a seller's storefrontseller_id (required), page1 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);
}

/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", not 19.98. Convert them before comparing, and expect null when Amazon shows no price.
  • Sponsored results are flagged. Filter on is_sponsored if 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_page and next_page rather than assuming a fixed count.
  • department narrows the search. The default is aps (All Departments), which you can see echoed in parameters. 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.

FieldTypeExampleNotes
asin, parent_asinstringB07ZPKN6YRThe parent groups all variations
product_titlestringApple iPhone 11, 64GB…
product_pricestring or null"184.99"Number as text; no currency symbol
product_original_pricestring or null"249.99"The strike-through price, when shown
currencystringUSD
main_buy_boxobject{ price, seller, seller_id, … }Price includes $; seller_id feeds the seller routes
buy_boxesarray[]May be empty
product_num_offersinteger58A count only; there is no offers list
product_availabilitystringIn StockFree text, as Amazon shows it
product_star_ratingstring"4.2"Number as text
product_num_ratingsinteger60093
sales_volumestring or null1K+ bought in past monthAmazon's rounded demand badge
is_best_seller, is_amazon_choice, is_primebooleanfalse
delivery, delivery_pricestringFREE delivery Thursday, October 8For a default US address
category, category_pathobject, array{ id: "2335752011", name }The breadcrumb, root first
product_detailsobject{ "Brand": "Apple", … }Spec table; keys vary by category
product_variationsobject{ color_name: [ … ] }Child ASINs per dimension
about_productarrayBullet points
product_photos, product_videosarrayImage URLsVideos may be empty
top_reviews, rating_distributionarray, 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-profile returns the seller's profile: name, ratings and storefront details. Pass an optional asin for a product they offer.
  • /seller-products lists 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:

RoutePage size we sawWhen to stop
/search16has_next_page is false
/seller-products16has_next_page is false
/best-sellers, /new-releases30A page with no products
/deals-v242A 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:

  1. 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.
  2. Fetch product details for each ASIN. Call /product-details once per ASIN per run, retrying 429 and 5xx responses with a growing delay.
  3. 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.
  4. Compare with the previous snapshot. Report price moves above a threshold, buy box seller changes and availability changes.
  5. 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_volume or product_num_offers as 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, 5xx and timeouts are refunded, so a retry never pays twice. 400, 401 and 402 need 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:

PlanPrice / monthCreditsAmazon callsPer 1,000 callsConcurrency
Free$0100 on sign-up10n/a1
Starter$49.99275K27,500$1.8210
Startup$99.991.1M110,000$0.9150
Pro$249.993.3M330,000$0.76100
Business$599.999M900,000$0.67200
Business+$999.9915.5M1,550,000$0.65200

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_reviews and rating_distribution exist 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, page and department.
  • 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

  1. Get a free key. Sign up for 100 credits with no card, enough for 10 Amazon calls.
  2. Try your real ASINs in the playground. Check the fields you need actually come back for your categories.
  3. Run the monitor on five ASINs twice, a few hours apart, and confirm the diffs look right.
  4. 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.

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.

Start scraping in the next five minutes.

100 free credits, no credit card. One API key works for websites, TikTok, Instagram, Google, Amazon and ChatGPT.