# ASINSpotlight Scraping API — Full Reference for LLMs > Real-time Amazon product, search, and seller data via REST. Single API key, JSON in/JSON out, 20 marketplaces. This document is consolidated reference content intended for LLM crawling and direct paste into AI coding tools. - Landing page: https://www.asinspotlight.com/api - OpenAPI spec (canonical contract): https://www.asinspotlight.com/scraping-api-docs/openapi.json - AI Integration Guide (browser): https://www.asinspotlight.com/scraping-api-docs/ai - Sign up: https://board.asinspotlight.com/dashboard/api ## Base URL ``` https://api.asinspotlight.com/v1 ``` ## Authentication Every request requires an API key in the `x-api-key` header. Generate one at https://board.asinspotlight.com/dashboard/api. ```bash curl -H "x-api-key: $ASINSPOTLIGHT_API_KEY" \ "https://api.asinspotlight.com/v1/product?asin=B0B3ZD8QXJ&marketplace=us" ``` ## Endpoints The API has four endpoints. Three are typed (specific Amazon page shapes); the fourth is a general-purpose URL scraper. ### GET /v1/product Fetch product detail page (PDP) data for a single ASIN. | Param | Type | Required | Default | Description | |---|---|---|---|---| | `asin` | string | yes | — | 10-character Amazon Standard Identification Number | | `marketplace` | string | no | `us` | Two-letter marketplace code (see list below) | | `language` | string | no | `en` | `en` or `native` (see Display language below) | Returns: `data.{asin, title, buybox, bb_price, rating, reviews, bsr, in_stock, is_prime, promotion, brand, category, image_url, sellers_all, bought_past_month}`. `promotion` is `null` when the product is at its normal price, and otherwise `{deal, list_price, lowest_price_30_days}`. `deal` is the badge Amazon renders only while a promotion is live: `kind` (`limited_time`/`lightning`/`prime_exclusive`, or `null` when the wording names no mechanic), verbatim `label`, `deal_id` (like `amzn1.deal.b12c9bd2`, stable for the lifetime of one deal, so compare it across scrapes to tell a continuing deal from a new one), `ends_at` (exact UTC instant), `ends_on` (UTC day), `ends_in_days` (`0` means today), and `ends_on_text` (Amazon's own end-date sentence, absent on amazon.com). `list_price` and `lowest_price_30_days` are struck-through references `{amount, label, savings_amount, savings_percent}`; the 30-day one appears on EU storefronts only (Omnibus Directive), and `savings_amount` is `null` (never zero or negative) when the reference is not above the current price. The parts are independent: a struck-through price can appear with no badge and a badge with no reference price. Clip-to-apply coupons stay in the separate `coupon` field (`{"unit": "percent", "value": 15}` or `{"unit": "currency", "value": 5}`, identical on product and list pages), where `value` is what the coupon takes off, never the price left after it, and is not reflected in `price`. Use it for: product cards, daily price snapshots, identifier lookups, cross-marketplace listing, deal and discount monitoring. ### GET /v1/search Search Amazon by keyword. Returns one page of organic results. | Param | Type | Required | Default | Description | |---|---|---|---|---| | `keyword` | string | yes | — | Keyword query (URL-encode multi-word terms) | | `marketplace` | string | no | `us` | Two-letter marketplace code | | `page` | integer | no | `1` | Page number (1-based). Amazon usually caps a keyword search at 20 pages | | `language` | string | no | `en` | `en` or `native` (see Display language below) | Returns: `data.{title, current_page, last_page_number, total_results_count, shallow_parts: [{asin, title, index_on_page, page_number, price, shipping, is_prime, rating, reviews, bought_past_month, in_stock, coupon, image_url, promotion, has_variation_swatches, advertised_variation_count, sampled_variation_asins}]}`. `shallow_parts` already includes everything needed to shortlist — no follow-up `/product` call required. Each entry's `promotion` is the shallow half of the product-page object: the deal is named (`kind`, `label`) and the struck-through `list_price` is present, but there is no `deal_id` or `ends_on_text`, `ends_at` appears only on countdown badges, and `ends_in_days` is always `null`. Call `/v1/product` on the ASIN when you need the deal's identity or exact deadline. The three `variation` fields are best-effort swatch signals: `has_variation_swatches` is positive-only, `advertised_variation_count` includes the `+N more` overflow, and `sampled_variation_asins` is a partial sibling sample (typically 6 or fewer), not the full family. All of these fields also appear on category and bestseller list pages via `POST /v1/scrape`. For pages 2+, pass `page=N`. The response's `data.current_page` and `data.last_page_number` tell you where you are and how deep the query goes. Each paginated request costs one credit. Amazon serves one of two result-page layouts, chosen per query: up to 48 entries per page stopping at 7 pages, or up to 16 entries per page stopping at 20. Read `shallow_parts.length` and `data.last_page_number` instead of assuming fixed values, and don't page past `last_page_number` (`total_results_count` reports the whole match set, not what Amazon will paginate through). Use it for: product discovery, keyword research, niche scanning, ranking trackers. ### GET /v1/offers Get the full seller offer panel for an ASIN — every seller currently listing with price, shipping, condition, fulfillment method (FBA/FBM/Amazon), seller rating. | Param | Type | Required | Default | Description | |---|---|---|---|---| | `asin` | string | yes | — | 10-character ASIN | | `marketplace` | string | no | `us` | Two-letter marketplace code | | `condition` | string | no | `all` | Filter by condition: `all`, `new`, `used` (any used tier), `used_like_new`, `used_very_good`, `used_good`, `used_acceptable`, `collectible`. The response is filtered to the requested condition and `fba_count`/`fbm_count`/`sold_by_amazon` reflect the filtered set. | | `page` | integer | no | `1` | Page of the offer panel (1-based). See pagination below. | | `language` | string | no | `en` | `en` or `native` (see Display language below). Condition slugs stay canonical either way, so `condition` filtering works unchanged. | Returns: `data.{sold_by_amazon, fba_count, fbm_count, product_sellers_info: [{name, price, shipping_price, is_fba, is_amazon, stock_qty, rating_percent, reviews_count, condition, condition_text, seller_id, domain, is_just_launched, is_pinned, delivery: [{kind, days_min, days_max, date_min, date_max, is_free, text}]}], page_index, total_pages}`. Each offer's `condition` is a normalized slug (`new`/`used_like_new`/`used_very_good`/`used_good`/`used_acceptable`/`collectible`) or `null` when Amazon's heading is unrecognized; `condition_text` is the verbatim heading (e.g. "Used - Like New"); `seller_id` is `null` for Amazon's own offers. One request does NOT return every seller. Amazon's offer panel holds 10 offers per page, and page 1 also carries the pinned Buy Box offer, so a full first page returns up to 11 entries. `total_pages > 1` means the seller list you are holding is partial; fetch the rest with `page=2`, `page=3`, and so on. `page_index` is 0-based, so `page=2` answers with `page_index: 1`. Read `total_pages` from the page 1 response and loop up to it: page 1 counts the offers beside the featured one, which matches how the pages actually fill, while page 2+ count the featured offer too and can report one page more than exists. A page past the end is NOT empty: Amazon clamps it and re-serves the last page, and page_index echoes the page you requested, so a loop trusting the higher number silently appends duplicate offers and pays for the extra request. Never page past the page-1 total_pages. Each paginated request costs one credit. Cross-check completeness against `sellers_all` from `/v1/product` on the same ASIN. Verified example: `B004YAVF8I` on `us` has 15 offers; page 1 returns 11 (pinned + 10), page 2 returns 4, `sellers_all` is 15. Two things differ on pages after the first: no offer is ever pinned there (Amazon renders the featured offer only on the first panel, so read the Buy Box owner from page 1), and `fba_count`/`fbm_count`/`sold_by_amazon` describe the requested page rather than the whole listing, so aggregate them across pages yourself. A `condition` filter applies per page, so a filtered page whose offers were all filtered out returns an empty `product_sellers_info`; that is not the end of the list, so keep going until you reach `total_pages`. `is_pinned` is `true` on the featured (Buy Box) offer, at most one per response. Read the Buy Box owner from this flag rather than inferring it from price. Array position is not a substitute: a response with no pinned offer means Amazon rendered no featured offer for that ASIN. A `condition` filter can drop the pinned offer, because Amazon returns the featured offer whatever condition is requested. `delivery` holds the promises Amazon displayed for that offer, and is `[]` when the offer shows no delivery block. `kind` is `standard` (primary slot) or `fastest` (secondary slot, which Amazon also uses for Prime upsells, so it is not necessarily a faster option). `date_min`/`date_max` are absolute `YYYY-MM-DD` dates; `days_min`/`days_max` are whole days from the request date, where `0` means today. Unresolved promises set every date and day field to `null`, never a placeholder number, so `null` means unknown and not zero. `is_free` (`true`/`false`/`null`) is the reliable free-delivery signal, since `shipping_price` reports `0` both for free delivery and for a cost that could not be read. `text` is Amazon's verbatim wording and can embed a live countdown, so it is not stable between requests and must not be used as a cache key. Use it for: Buy Box monitoring, seller tracking, repricing decision logic, filtering New vs used inventory, delivery-speed comparison. Note: `/v1/product` returns the Buy Box price; `/v1/offers` returns the Buy Box owner. ### POST /v1/scrape Submit an arbitrary Amazon URL. The page type is auto-detected from the URL pattern. Body (JSON): | Field | Type | Required | Default | Description | |---|---|---|---|---| | `url` | string | yes | — | Full Amazon URL | | `marketplace` | string | no | `us` | Two-letter marketplace code | | `language` | string | no | `en` | `en` or `native` (see Display language below) | Detected page types: `product`, `search`, `offers`, `category`, `bestseller`, `storefront`, `reviews`. The `data` shape matches the detected type. Use it for: deal pages, marketplace-specific layouts, bestsellers, category pages, anything that doesn't fit a typed endpoint. ## Display language Every endpoint accepts an optional `language` parameter: `en` (default) scrapes the English render of the page; `native` scrapes the marketplace's own language (currently supported: `it`, `fr`, `de`, `es`; more marketplaces rolling out). With `native`, free-text fields (title, category and brand names, `condition_text`, delivery text) return in the marketplace's language, while normalized fields (condition slugs, prices, dates, units) stay canonical, so filters and parsing logic work unchanged. The requested language is echoed in `meta.language`. Requesting `native` on a marketplace without native support returns 400 `NATIVE_LANGUAGE_NOT_SUPPORTED` and is not billed. ## Response envelope All successful responses follow this shape: ```json { "success": true, "found": true, "page_type": "product|search|offers|...", "data": { ... }, "meta": { "marketplace": "us", "language": "en", "timing_ms": 1842, "request_url": "https://api.asinspotlight.com/v1/product?asin=B0B3ZD8QXJ&marketplace=us", "timestamp": "2026-05-03T10:14:22.318Z", "request_id": "550e8400-e29b-41d4-a716-446655440000", "usage": {"requests_consumed": 1, "requests_remaining": 49999} } } ``` Read `meta.usage.requests_remaining` after every successful call. There is no separate quota endpoint — this is how you build quota-aware loops. A missing entity (invalid ASIN/seller, removed listing) is **not** an error: the response is `200` with `found: false` and `data: null`, and it still costs one credit because the page was fetched and parsed. Branch on `found`, not on the HTTP status. ## Error envelope ```json { "success": false, "meta": {"usage": {"requests_consumed": 0, "requests_remaining": 49999}}, "error": {"code": "ERROR_CODE", "message": "Human-readable description"} } ``` `meta` is included on quota/rate-limit errors so client code can read remaining credits even when a request is rejected. It is absent on auth errors. | HTTP | Code | Meaning | Counts against quota? | |---|---|---|---| | 400 | (validation) | Missing/invalid parameter | No | | 400 | `NATIVE_LANGUAGE_NOT_SUPPORTED` | `language=native` on a marketplace without native support | No | | 401 | `MISSING_API_KEY` | No `x-api-key` header | No | | 401 | `INVALID_API_KEY` | API key not recognized | No | | 429 | `RATE_LIMIT_EXCEEDED` | Too many parallel requests | No | | 429 | `MONTHLY_QUOTA_EXCEEDED` | No active credits | No | | 500 | `SCRAPE_FAILED` | Parser reached the page but couldn't extract data | No | | 500 | `INTERNAL_ERROR` | Service or transport failure | No | | 503 | `CAPTCHA_DETECTED` | Amazon returned a captcha | No | | 503 | `API_USAGE_UNAVAILABLE` | Usage tracking transiently down | No | There is no 404: a page that doesn't exist on Amazon returns `200` with `found: false` (see above). Retry policy: 429 (wait then retry), 500 + 503 (exponential backoff). Don't retry 400 or 401. ## Marketplaces 20 supported codes. Default is `us`. | Code | Country | Domain | |---|---|---| | us | United States | amazon.com | | uk | United Kingdom | amazon.co.uk | | de | Germany | amazon.de | | fr | France | amazon.fr | | it | Italy | amazon.it | | es | Spain | amazon.es | | ca | Canada | amazon.ca | | au | Australia | amazon.com.au | | jp | Japan | amazon.co.jp | | in | India | amazon.in | | mx | Mexico | amazon.com.mx | | br | Brazil | amazon.com.br | | tr | Turkey | amazon.com.tr | | sa | Saudi Arabia | amazon.sa | | ae | UAE | amazon.ae | | sg | Singapore | amazon.sg | | nl | Netherlands | amazon.nl | | pl | Poland | amazon.pl | | se | Sweden | amazon.se | | be | Belgium | amazon.com.be | ## Patterns AI-generated code should imitate ### Quota-aware loops ```typescript for (const asin of asins) { const res = await fetchProduct(asin); if (!res.success) handleError(res.error.code); if (res.meta.usage.requests_remaining < FLOOR) break; store(res.data); } ``` ### Marketplace switching Always pass `marketplace` explicitly. The default `us` is a footgun for European or Japanese workflows. ### Pagination `/v1/search` returns page 1 by default. For pages 2+, pass `page=N` and watch `data.last_page_number` to know when to stop. ### Error handling - 429: back off (rate-limit) or stop (quota exceeded — check `error.code`). - 500, 503: exponential backoff retry. - `200` with `found: false`: mark the entry gone; it costs 1 credit, so stop refreshing it. - 4xx (validation, auth, unsupported native language): don't retry. ## Pitfalls AI-generated code commonly hits - Confusing `/v1/product` (returns Buy Box price) with `/v1/offers` (returns Buy Box owner + every seller). - Treating one `/v1/offers` response as the complete seller list. It is one page of 10 (11 on page 1, which includes the pinned offer). Check `total_pages` and walk `page=2..total_pages` before claiming you have every seller on a listing. - Polling for quota instead of reading `meta.usage.requests_remaining` from each response. - Forgetting the `marketplace` param — silently routes everything to amazon.com. - Treating a `found: false` response as a free error (it costs 1 credit because the page was fetched and parsed). - Requesting `language=native` on every marketplace. Native mode is per-marketplace (currently `it`, `fr`, `de`, `es`); unsupported combos return 400 `NATIVE_LANGUAGE_NOT_SUPPORTED`. - Blindly retrying 4xx errors that won't fix themselves. - Generating a follow-up `/product` call for each `shallow_parts` entry — that data is already in the search response. ## Prompt recipes ### Cross-marketplace price comparison ``` Build a script that compares prices for a list of ASINs across multiple Amazon marketplaces using the ASINSpotlight Scraping API. OpenAPI spec: https://www.asinspotlight.com/scraping-api-docs/openapi.json API base URL: https://api.asinspotlight.com (note: api., not www.) Auth: API key in the x-api-key header (env var ASINSPOTLIGHT_API_KEY). Endpoint: GET /v1/product?asin=...&marketplace=... Inputs: - List of ASINs (e.g., ["B0B3ZD8QXJ", "B0CQXMXJC5"]) - List of marketplace codes (e.g., ["us", "uk", "de"]) For each (asin, marketplace) pair, fetch product details and collect: asin, marketplace, title, bb_price, in_stock, rating. Output: CSV with columns asin, marketplace, title, bb_price, in_stock, rating. Constraints: - Stop early and surface the error if a request returns 401, 429, or 5xx. - After each successful response, read meta.usage.requests_remaining and abort if it drops below 50. - Don't run more than 5 requests in parallel. ``` ### Daily keyword tracker ``` Build a daily keyword tracker that records the top 10 ASINs for a list of keywords using the ASINSpotlight Scraping API. OpenAPI spec: https://www.asinspotlight.com/scraping-api-docs/openapi.json API base URL: https://api.asinspotlight.com (note: api., not www.) Auth: API key in the x-api-key header (env var ASINSPOTLIGHT_API_KEY). Endpoint: GET /v1/search?keyword=...&marketplace=... For each keyword, fetch the search results page, take the first 10 entries from data.shallow_parts, and write one row per (keyword, rank, asin, title, price, rating, reviews, bought_past_month, captured_at). Storage: append to a SQLite table keyword_rankings keyed on (keyword, marketplace, captured_at, rank). Run cadence: once per day, intended to be invoked by cron. Don't: - Don't try to "follow up" with a /v1/product call for each search result — shallow_parts already contains everything we need. - Don't paginate past page 1; we only care about the top 10. - Don't retry on 4xx errors; only on 5xx and CAPTCHA_DETECTED (with backoff). ``` ### Buy Box ownership monitor ``` Build a Buy Box ownership monitor for a watchlist of ASINs using the ASINSpotlight Scraping API. OpenAPI spec: https://www.asinspotlight.com/scraping-api-docs/openapi.json API base URL: https://api.asinspotlight.com (note: api., not www.) Auth: API key in the x-api-key header (env var ASINSPOTLIGHT_API_KEY). Endpoint: GET /v1/offers?asin=...&marketplace=... For each ASIN, call /v1/offers?...&condition=new and identify the current Buy Box owner: the seller in data.product_sellers_info whose is_pinned is true. At most one offer carries that flag, so read it directly rather than guessing from price. If no offer is pinned (Amazon showed no featured offer, or the payload predates the flag), fall back to the lowest price + shipping_price among is_fba: true sellers. Compare against the previously recorded owner stored in a JSON file buybox_state.json keyed by asin. When the owner changes, log a line to stdout with: timestamp, asin, previous owner, new owner, new price. Run as a loop with a 10-minute interval between full sweeps. Don't: - Don't conflate /v1/product and /v1/offers — /v1/product returns the Buy Box price but not the full seller list. - Don't stop at the first /v1/offers response when you need the whole seller list; it holds one page. Read total_pages and fetch page=2..total_pages, then concatenate. Buy Box detection itself only needs page 1, since only page 1 carries the pinned offer. - Don't compare used and new offers as interchangeable — each offer has a condition; the Buy Box is a new offer, so pass condition=new (or filter to condition: "new") before picking the lowest price. - Don't ignore stock_qty: 0 sellers; filter those out before picking a winner. - Don't treat the first entry in product_sellers_info as the Buy Box owner; order does not encode it. Use is_pinned. - Don't run all watchlist requests at once; respect the parallel limit (default ~5). ``` ### ASIN list batch refresh ``` Build an ASIN-list batch refresh that updates cached product data using the ASINSpotlight Scraping API. OpenAPI spec: https://www.asinspotlight.com/scraping-api-docs/openapi.json API base URL: https://api.asinspotlight.com (note: api., not www.) Auth: API key in the x-api-key header (env var ASINSPOTLIGHT_API_KEY). Endpoint: GET /v1/product?asin=...&marketplace=... Inputs: a JSON file watchlist.json of {asin, marketplace, last_refreshed_at} records. Refresh strategy: 1. Sort the list by last_refreshed_at ascending (oldest first). 2. For each entry, call /v1/product. On a successful 200, write the response data and the new timestamp back to the file. 3. After every response, read meta.usage.requests_remaining. If it would dip below the configured floor (default 1000), stop the run and log how many entries were skipped. 4. Run requests in batches of 5 in parallel. Error handling: - PAGE_NOT_FOUND (404): mark the entry as status: gone and continue. PAGE_NOT_FOUND consumes 1 credit — that's expected. - CAPTCHA_DETECTED (503): retry up to 3 times with exponential backoff. - RATE_LIMIT_EXCEEDED (429): wait 30 seconds and retry the same entry. - Any other error: log and stop. ``` ### Search-to-database pipeline (with pagination) ``` Build a search-to-database pipeline that captures the full first 5 pages of results for a keyword using the ASINSpotlight Scraping API. OpenAPI spec: https://www.asinspotlight.com/scraping-api-docs/openapi.json API base URL: https://api.asinspotlight.com (note: api., not www.) Auth: API key in the x-api-key header (env var ASINSPOTLIGHT_API_KEY). Endpoint: GET /v1/search?keyword=...&marketplace=...&page=N Steps: 1. Call GET /v1/search?keyword=...&marketplace=us (page 1 by default). Read data.last_page_number to know the upper bound. 2. For pages 2..min(5, last_page_number), call the same endpoint with page=N. 3. Append every entry to a Postgres table search_results with columns (keyword, marketplace, page, rank, asin, title, price, rating, reviews, captured_at). Don't: - Don't fetch past last_page_number — Amazon returns an empty result set and you'll waste credits. - Don't run more than 5 requests in parallel; the account has a parallel-request limit. ```