How to scrape Amazon product data with Python

By John F.·Updated ·9 min read

Reader-supported. How we make money

Clay product parcel and sorting arm arranging structured records

Short answer

Use ScraperAPI's structured Amazon search endpoint when you need a list of products as JSON, and its product endpoint when you already have an ASIN. The managed endpoint avoids writing your own HTML parser for those responses. This guide shows a bounded Python-to-CSV workflow, with missing-field handling and no automatic product-by-product fan-out.

Top picks

Features checked November 2026

  1. For Amazon JSON pipelines

    ScraperAPI

    Python pipelines using documented Amazon JSON endpoints

    Hobby: $49/month; trial has separate limits

    Try ScraperAPI
On this page

Choose Amazon search or product data first

To scrape Amazon use the endpoint matching your input. A search term belongs in the search endpoint; an ASIN belongs in the product endpoint. They return different shapes, so one parsing loop won't work for both.

ScraperAPI's Amazon search documentation describes GET https://api.scraperapi.com/structured/amazon/search. Supply query and your api_key. Its JSON example has a results array of product objects, plus next_pages and explore_more_items arrays.

The product endpoint documentation describes GET https://api.scraperapi.com/structured/amazon/product with asin and api_key. It returns a product object, with fields such as name, product_information, brand, images and feature_bullets in the documented sample.

Input you haveEndpointRead these documented fields
A product search phrase/structured/amazon/searchresults[], then fields such as asin, name, url, optional price fields
A known ASIN/structured/amazon/productA product object; the sample places ASIN inside product_information

The product sample doesn't establish a guaranteed top-level price, rating or availability field. Don't invent one because it would make a convenient CSV column. Search results document price, price_string and price_symbol, but those can still be absent on individual rows.

Set up a small, bounded request

You'll need Python and the requests package. Set your own ScraperAPI key in an environment variable called SCRAPERAPI_KEY; don't paste it into a committed script, notebook output or CSV. Our ScraperAPI review explains the service and its wider use cases.

Bash
python -m pip install requests

The script below makes one search request and writes a local CSV. It doesn't loop over Amazon pages or fetch every returned ASIN. Those would be additional requests with additional credit costs.

ScraperAPI documents internal retries lasting up to 70 seconds. We use a 10-second connection timeout and 90-second read timeout so the normal read window exceeds that internal retry period. The tuple is our client configuration, not a vendor guarantee that every request finishes within 90 seconds. A read timeout also isn't a total runtime cap.

One search request, then a CSV export

This example follows the documented search response. It was checked offline with illustrative fixtures, not against a paid live API response. Treat the output as a collection snapshot, not a price guarantee.

Python
import csv
import os
from datetime import datetime, timezone
from pathlib import Path

import requests

SEARCH_ENDPOINT = "https://api.scraperapi.com/structured/amazon/search"
COLUMNS = ["retrieved_at", "market", "query", "page", "asin",
           "name", "url", "price", "price_symbol"]

def spreadsheet_text(value):
    text = "" if value is None else str(value)
    if text.lstrip().startswith(("=", "+", "-", "@")):
        return "'" + text
    return text

def search_amazon(query, market="com", page=1):
    key = os.environ.get("SCRAPERAPI_KEY")
    if not key:
        raise RuntimeError("Set SCRAPERAPI_KEY before running.")
    params = {"api_key": key, "query": query, "tld": market,
              "country_code": "us", "page": page,
              "output_format": "json"}
    try:
        response = requests.get(SEARCH_ENDPOINT, params=params,
                                timeout=(10, 90))
    except requests.RequestException:
        # Request exceptions may include the URL and its secret query key.
        raise RuntimeError("Request failed; check usage before retrying.") from None
    status = response.status_code
    if status == 404:
        raise RuntimeError("Target not found (404); this may consume credits.")
    if status != 200:
        raise RuntimeError(f"Request stopped with HTTP {status}; no automatic retry.")
    try:
        data = response.json()
    except ValueError:
        raise RuntimeError("Expected JSON; inspect a sanitized response.") from None
    if not isinstance(data, dict) or not isinstance(data.get("results"), list):
        raise RuntimeError("Unexpected search schema; results must be a list.")
    return data

def rows_from_search(data, query, market="com", page=1):
    if not isinstance(data, dict) or not isinstance(data.get("results"), list):
        raise ValueError("Expected a search object with results[].")
    stamp = datetime.now(timezone.utc).isoformat()
    seen = set()
    for item in data["results"]:
        if not isinstance(item, dict) or item.get("type") != "search_product":
            continue
        asin = item.get("asin")
        if not isinstance(asin, str) or not asin.strip():
            continue
        identity = (market, asin)
        if identity in seen:
            continue
        seen.add(identity)
        price = item.get("price")
        # Preserve a genuine zero; missing or unexpected price stays blank.
        if isinstance(price, bool) or not isinstance(price, (int, float)):
            price = ""
        row = {"retrieved_at": stamp, "market": market, "query": query,
               "page": page, "asin": asin, "name": item.get("name"),
               "url": item.get("url"), "price": price,
               "price_symbol": item.get("price_symbol")}
        yield {k: (v if k == "price" else spreadsheet_text(v))
               for k, v in row.items()}

def write_csv(rows, filename):
    with Path(filename).open("w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(handle, fieldnames=COLUMNS)
        writer.writeheader()
        writer.writerows(rows)

if __name__ == "__main__":
    term = "ceramic mug"
    payload = search_amazon(term)
    write_csv(rows_from_search(payload, term), "amazon-search.csv")

The script fails closed when it doesn't recognize the response shape. It also avoids printing request exceptions, because the documented endpoint puts the API key in query parameters. An exception containing the prepared URL can leak that key even when your source code uses an environment variable.

Our spreadsheet-text helper prefixes potentially executable text values. Quoting a CSV cell alone doesn't stop spreadsheet formula interpretation. Numeric prices stay numeric; absent prices stay blank. Some spreadsheet programs handle imports differently, so review the import settings before opening data you don't trust.

What the offline checks cover

We checked parsing with a documentation-shaped fixture containing duplicate ASINs, missing fields, a non-product entry and a text value beginning with a formula character. Separate checks cover an empty result list, unexpected schema and simulated HTTP or request failures. These are code checks, not evidence of scraping success on Amazon.

Deduplication here is limited to one exported batch and the pair of market plus ASIN. If you combine pages or collection dates later, decide whether you want one row per product or one row per observation. Deduplicating the whole dataset by ASIN would erase the price history you might be trying to collect.

Markets, country targeting and pagination

Amazon search data: one request, a validated export

  1. 1. One search request

    Choose marketplace, query and page; keep the API key out of logs.

  2. 2. Validate results[]

    Check shape, missing fields and market-plus-ASIN duplicates.

  3. 3. Write a bounded CSV

    Preserve blank prices and timestamps; protect text cells.

Illustrative data flow for the documented search endpoint, not a live API result or a product dashboard.

tld selects the Amazon marketplace, such as com or co.uk. country_code describes geographic targeting. They aren't interchangeable. The product documentation says ASINs are market-specific, so don't assume an identifier and fields collected in one market transfer unchanged to another.

The example is deliberately US-oriented: marketplace com and country code us. If you change the marketplace, review the targeting and your plan's supported locations rather than changing one string and assuming the rest remains correct.

The search endpoint documents a page parameter and returns next_pages in its sample. For a larger export, add an explicit maximum page count and a request budget before introducing a loop. Stop on empty results and log the page number. Don't blindly follow every URL forever.

A successful response can still contain a product without a price. It may be unavailable, presented differently or absent from the parsed fields; the response alone doesn't establish which explanation is correct. Keep the cell blank and retain the original JSON when you need to investigate.

Fetch a known product without assuming the search schema

For an ASIN lookup, call the product endpoint with asin, tld and the appropriate targeting, using the same timeout and sanitized-error approach. Read the returned object rather than looking for results[].

Start with fields actually shown in the product documentation: name, product_information.asin, brand, and images. Nested lists and dictionaries need their own handling; don't let Python stringify a large object into a CSV cell without deciding whether that format suits your downstream system.

The page's opening mentions public reviews, but its sample has an empty reviews list. We wouldn't promise a complete review archive based on that. If your task requires reviews, offers or a guaranteed field, verify the dedicated documentation and sample for that task before committing to a large collection.

Credits, errors and the cost of retries

The official credit-cost documentation lists Amazon at a base cost of five credits per request. Parameters and protection handling can change the actual cost. The response header sa-credit-cost reports the charge for a request; don't add rendering or premium options merely because another tutorial does.

The current pricing page lists Hobby at $49 per month, with 100,000 credits and 20 concurrent threads. Its $44.10 monthly equivalent is billed annually. The page separately describes a seven-day trial with 5,000 credits and no card, and a free-plan FAQ with different allowances. Those aren't one combined free balance.

The status-code documentation distinguishes these cases:

StatusDocumented meaningSensible next action
400Malformed requestFix parameters rather than retrying unchanged
401Invalid keyCheck configuration without printing the key
403Credits exhaustedStop and review account usage
404Target page missingRecord the missing target; the request can be billed
429Too much concurrencyReduce concurrency before a deliberate retry
500Request failed after internal retriesReview the target and configuration before retrying

ScraperAPI says successful 200 and 404 responses are charged, while failed 500 requests aren't. Its credit documentation also warns that cancelling your side before it has had 70 seconds can incur a charge. Therefore a client timeout must not trigger an unlimited immediate retry loop. Check usage first when completion is uncertain.

When a different approach makes sense

If you're learning HTML parsing, a local saved page and BeautifulSoup can teach the extraction step without a stream of live requests. A browser automation project may suit a permitted task requiring interaction, but it adds another system to maintain. Neither approach automatically solves access restrictions or grants permission to collect data.

Read the site's terms, assess privacy and intellectual-property obligations, and collect only the public data appropriate to your task. A scraping provider's technical capability isn't a legal opinion. This guide doesn't cover account-only content or bypassing access controls.

For Google search data rather than Amazon products, use our Google scraping guide. If search results are the whole workload, our SERP API comparison addresses that narrower selection.

Frequently asked questions

Do I need to parse Amazon HTML?

Not for the structured responses shown here. ScraperAPI returns documented JSON; your script still needs to validate the response and handle missing fields.

Are search and product responses identical?

No. Search documents a results array. Product lookup returns an object with nested product information and other fields.

Why is a price cell blank?

The field may be absent or have an unexpected value. Keep that distinct from a numeric zero and inspect the response before assigning a reason.

Does one ASIN work in every Amazon market?

Don't assume so. The product documentation describes ASINs as market-specific; choose the correct marketplace and targeting.

Can a failed-looking request consume credits?

Yes. A target 404 can be billed, and a request cancelled by your client before the documented processing window may still incur a charge. Review usage before retrying uncertain requests.

Make one useful export before scaling

Collect a small search response, inspect the JSON and open the CSV safely. Only then add pagination, product lookups or a schedule with an explicit credit budget. The extra requests are easy to write; making their output trustworthy takes more care.

Start with ScraperAPI's trial

We may earn a commission if you buy through some links on this page. It doesn’t change our picks or verdicts. How we make money

John F. · 15+ years in SEO and WordPress

John F. is the pen name of a ToolVerdict team member who has worked in SEO and WordPress for more than 15 years. Every guide is fact-checked by the ToolVerdict Editorial Team, and every price comes from the vendor's own pricing page, with the date we checked it.