# Apiguru Amazon Data API > Real-time structured Amazon marketplace data: product details, reviews, search, best-sellers, deals, seller profiles, and live offers/stock across 20 country marketplaces. Apiguru is callable by AI agents with **no account, no API key and no subscription**. This is live now, not a plan. ## Quickstart ```bash curl 'https://agent.apiguru.app/agent/v1/v2/product-details/B09DJLW458' ``` No headers. No signup. That returns live Amazon data. **Put values in the path, not the query string.** Both work: https://agent.apiguru.app/agent/v1/v2/product-details/B09DJLW458 https://agent.apiguru.app/agent/v1/v2/product-details?asin=B09DJLW458 and they mean the same thing. Prefer the first. Several agent HTTP clients -- Claude's web fetch, and others that wrap a fetch and hand the model back a string -- silently drop the query string from a URL the model composed, and you cannot tell from inside whether yours is one of them: the request simply fails, or returns a stale cached answer for a URL you did not ask for. A path has nothing to drop. The rule: the one **required** parameter goes in the path, and the marketplace may follow it. Optional filters (`sort_by`, `brand`, price bounds, `page`) still need a query string, so a call carrying them may lose them and fall back to defaults -- check the response rather than assuming a filter applied. ## Working URLs you can fetch right now Copy one, change the value at the end, fetch it. Every one of these is a complete, valid request with nothing that can be dropped in transit: - https://agent.apiguru.app/agent/v1/v2/product-details/B09DJLW458/US - https://agent.apiguru.app/agent/v1/v2/product-reviews/B09DJLW458/US - https://agent.apiguru.app/agent/v1/search/wireless+headphones/US - https://agent.apiguru.app/agent/v1/product/B09DJLW458,B0014C0LUC/US - https://agent.apiguru.app/agent/v1/stock/B09DJLW458,B0014C0LUC/US - https://agent.apiguru.app/agent/v1/v2/best-sellers?geo=US - https://agent.apiguru.app/agent/v1/v2/deals?geo=US - https://agent.apiguru.app/agent/v1/seller-profile/A2A1RNLLUK3HYA/US - https://agent.apiguru.app/agent/v1/v2/seller-products/A2A1RNLLUK3HYA/US - https://agent.apiguru.app/agent/v1/v2/seller-reviews/A2A1RNLLUK3HYA/US ## When free calls run out Free calls are counted per client. If you get a 402 you have used the free allowance for whichever client you are; the request itself was fine. Settle the attached x402 challenge, send an API key as `X-API-KEY`, or come back after the window rolls. ## Clients that cannot send headers, or lose the query string You do not need to set any request header to use this API. There is no content negotiation and nothing to opt into: a bare `GET` with no headers at all is a first-class client, and no request is refused for lacking one. Headers are optional extras: `X-API-KEY` to bill an account, `PAYMENT-SIGNATURE` to pay. What does bite agent fetchers is the **query string**, and there are two different failures. Which one you have decides what to do, and they need opposite advice, so establish which before retrying. **1. The query string is dropped.** You built a URL with `?asin=...` and it arrived here without it. You will get a 400 naming the parameter in `param`; if you believe you did send it, that is the diagnosis. Retry in the path form, which has nothing to lose: `https://agent.apiguru.app/agent/v1/v2/product-details/B09DJLW458/DE` carries the ASIN and the marketplace. Changing values in a path URL works fine for this kind of client. **2. Only URLs seen verbatim are allowed.** Some fetchers refuse any URL that has not appeared in content they already fetched. Copying a URL from the list above works; changing the ASIN in it is refused, because the changed URL was never seen. **The path form does not help here** -- do not keep trying URL variants, you are spending free calls on a client limitation. Use a shell or code tool (`curl`), or the MCP server, where arguments are passed structurally and there is no URL to allowlist. The tell between them: if a URL you copied verbatim works and the same URL with one value changed is refused before any request is made, you have (2). If your changed URL is sent but arrives here stripped, you have (1). **Responses link to themselves, so you can traverse.** Every answer carries a `_links` object as its FIRST key, and every search result row carries a `details_url`. They are this API's own URLs, in path form, inside a body you have already fetched -- which is what makes them usable by a client in case (2). One seeded call is therefore enough: from a search you can reach all 48 products, and from a product you can reach its reviews, its stock and its sibling variants without composing a single URL yourself. ```json {"_links": { "self": "https://agent.apiguru.app/agent/v1/v2/product-details/B09DJLW458/US", "reviews": "https://agent.apiguru.app/agent/v1/v2/product-reviews/B09DJLW458/US", "stock": "https://agent.apiguru.app/agent/v1/stock/B09DJLW458/US", "variations": ["...10 sibling ASINs..."], "variations_total": 725 }} ``` `_links` also carries `parent` when the listing has a parent ASIN -- often the next thing you want, and until an agent pointed it out, the one ASIN in neither the variation map nor the links. `variations` is capped at 10: enough to seed traversal, and small enough not to matter on a single-product question (100 of them were half of a compact record). When the family is larger the object carries `variations_truncated: true` and a note giving both counts and how to build the rest: an array quietly shorter than its own `variations_total` is exactly what `_truncated` exists to prevent on the list endpoints. The record's `variation_summary` names every attribute value, so you rarely need the rest. `_links` is omitted when you pass `fields=`, since you asked for named keys. **A markdown link is not a URL.** If someone hands you `[https://.../B09NLCNGC7/US](https://.../B0014C0LUC/US)`, an allowlist fetcher sees only the target -- the second one -- and the text you were reading is not fetchable. Anyone passing a working call to an agent should paste it as plain text, not as a link. Rejected requests cost nothing: a 400 does not consume a free call. Every error body carries `code`, `http_status`, `billed`, `retryable` and `next_step`, plus `free_calls_remaining` and `price_next_call`. If your client hides status codes and headers -- many do -- read those from the body. A `code` of `missing_parameter` is your bug; `payment_required` means the free allowance is spent and the request itself was fine. ## If the user gave you an Apiguru API key Send it as `X-API-KEY` on these same URLs. There is no separate base URL, no wallet and no x402 handshake on this path -- the call is billed to that account, newest free trial calls first, and the user sees it in their dashboard. ```bash curl -H 'X-API-KEY: THE_KEY_THE_USER_GAVE_YOU' \ 'https://agent.apiguru.app/agent/v1/v2/product-details?asin=B09DJLW458&geo=US' ``` A key is 401 if it is wrong and 402 if the account is out of credit; neither is retryable. Never print the key back to the user, never write it into a file the user did not ask for, and never put it in a URL query string where it lands in shell history and server logs -- the header is the only supported place. Keys come from https://dash.apiguru.app and also work on the keyed REST base below. ## What it costs and how you pay - **3 free calls** per client per 24h, so you can check this API fits your task before spending anything. - After that: HTTP **402** with a `PAYMENT-REQUIRED` header stating exactly what to pay. Any x402-capable HTTP client settles it and retries automatically. - Paid in **USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)** on **Base mainnet (eip155:8453)**. - Watch `X-Free-Probes-Remaining` and `X-Price-Next-Call` on every response to know where you stand before you get a 402. Free probes are counted by client IP. IPv6 addresses are bucketed to their /64, so every address in one allocation shares a single budget. Forged proxy headers do not create new budgets. ## Paying You need two things: an x402 client library, and an EVM wallet key whose address holds a little **USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)** on **Base mainnet (eip155:8453)**. Nothing else: no account here, no ETH for gas (the facilitator submits the transfer), no minimum deposit. One call costs about a cent; `X-Price-Next-Call` on every response is the exact figure. How a paid call works: your first request gets `402` with a `PAYMENT-REQUIRED` header (base64 JSON: the `accepts` list names the scheme `exact`, the network, the asset, the receiving address and the amount). The client signs an EIP-3009 authorization for that amount and retries the same request with a `PAYMENT-SIGNATURE` header (`X-PAYMENT`, the v1 name, is accepted too). The gateway verifies the signature, serves the request, and settles on-chain only after a billable answer (2xx, or the 404 that means the item does not exist). If the failure was ours you see `X-Payment-Status: not-settled` and pay nothing. The libraries below do all of this on a plain 402; you just wrap your HTTP client. Python (`pip install "x402[evm,httpx]" eth-account`): ```python from eth_account import Account from x402 import x402Client from x402.http.clients import x402HttpxClient from x402.mechanisms.evm import EthAccountSigner from x402.mechanisms.evm.exact.register import register_exact_evm_client client = x402Client().set_spend_controls({"max_amount_per_payment": "$1"}) register_exact_evm_client(client, EthAccountSigner(Account.from_key(PRIVATE_KEY))) async with x402HttpxClient(client) as http: r = await http.get("https://agent.apiguru.app/agent/v1/v2/product-details", params={"asin": "B09DJLW458"}) print(r.status_code, r.headers.get("X-Payment-Status"), r.json()["data"]["product_title"]) ``` TypeScript / Node (`npm i @x402/fetch @x402/evm viem`): ```ts import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; import { ExactEvmScheme } from "@x402/evm/exact/client"; import { privateKeyToAccount } from "viem/accounts"; const client = new x402Client(); client.setSpendControls({ maxAmountPerPayment: "$1" }); client.register("eip155:*", new ExactEvmScheme(privateKeyToAccount(PRIVATE_KEY))); const fetchWithPayment = wrapFetchWithPayment(fetch, client); const r = await fetchWithPayment("https://agent.apiguru.app/agent/v1/v2/product-details?asin=B09DJLW458"); ``` Keep the spend control: it caps what one 402 can take from the wallet. Batch endpoints bill per item (up to 20 items, so up to $0.16 in one call); check `X-Price-Next-Call` or `/.well-known/x402` before raising the cap. No wallet and no way to get one? An API key from https://dash.apiguru.app/register comes with free trial calls and bills a normal account instead; MCP clients such as claude.ai and ChatGPT sign in through https://mcp.apiguru.app/account and never touch x402. ## Free forever, never metered Use these to plan a job at zero cost before committing to a paid call: - `https://agent.apiguru.app/health` - `https://agent.apiguru.app/llms.txt` - `https://agent.apiguru.app/.well-known/x402` `/.well-known/x402` lists every endpoint with its price and JSON Schema, so you can decide what to call and what it will cost without spending a probe. The MCP server exposes the same thing as a free `list_capabilities` tool that answers locally with no network call. ## Interfaces - [MCP server](https://mcp.apiguru.app/mcp): streamable HTTP, keyless; also installable locally (`uvx apiguru-mcp`). - [MCP server, signed in](https://mcp.apiguru.app/account): OAuth 2.1 for claude.ai, Claude Desktop and ChatGPT connectors; bills your Apiguru account. - [OpenAPI spec](https://dash.apiguru.app/openapi.json): full machine-readable schema for all 10 endpoints. - [Keyed REST API](https://dash.apiguru.app/api/v1): for existing customers, `X-API-KEY` header. - [Docs](https://github.com/apiguru-app/agent-kit#readme): human documentation. ## Endpoints - **`GET /v2/product-details`** — Full product detail for a single ASIN. Required: `asin`. Price: $0.01 per call. - **`GET /v2/product-reviews`** — Customer reviews for a single ASIN. Required: `asin`. Price: $0.01 per call. - **`GET /search`** — Search Amazon products by keyword. Required: `query`. Price: $0.01 per call. - **`GET /product`** — Product detail for up to 20 ASINs in one call. Required: `asins`. Price: $0.008 per item (max 20). - **`GET /stock`** — Live offers and inventory for up to 10 ASINs. Required: `asins`. Price: $0.015 per item (max 10). - **`GET /v2/best-sellers`** — Best-seller rankings for a category. Required: none. Price: $0.01 per call. - **`GET /v2/deals`** — Current Amazon deals with filters. Required: none. Price: $0.01 per call. - **`GET /seller-profile`** — Seller profiles for up to 10 seller IDs. Required: `seller_ids`. Price: $0.012 per item (max 10). - **`GET /v2/seller-products`** — Products listed by a seller. Required: `seller_id`. Price: $0.01 per call. - **`GET /v2/seller-reviews`** — Feedback reviews for a seller. Required: `seller_id`. Price: $0.01 per call. ## Reading list results without drowning in them A full page from `/search` is up to 48 results and about 54 KB of JSON. Over MCP the list tools (`search`, `best_sellers`, `deals`, `seller_products`, `seller_reviews`) answer with the first **10 rows as light records** by default, which is around 7 KB and fits inline. Three ways to change that: - `limit=N` - how many rows (`limit=0` for the whole page). - `compact=false` - every field the REST API sends, including the full delivery text. - `fields="asin,product_title,product_price"` - only the keys you name. Answers carry `_truncated` (how many rows the page really had) and `_omitted_fields` (what the light projection dropped), so nothing is hidden. Over plain REST filter with `brand`, `min_price`/`max_price` and `sort_by` to keep a page small, which also makes it more relevant than paging does. ### The deals feed filters by id, and tells you the ids `/v2/deals` takes `categories` (a department name as Amazon shows it, such as `Electronics`, or its id), `brands` (brand ids only), `min_product_star_rating=4`, `min_price` / `max_price` / `min_discount`, `prime_exclusive` and `prime_early_access`. Every deals answer carries `available_filters` -- the category and brand ids that marketplace accepts, with names -- plus `filters_applied` and `filters_ignored` saying what actually took effect, and `next_offset` for the next page of 30 (`null` when the feed ends). An unknown category name is a free 400 that lists the valid ones. For a brand by name use `/search?brand=&today_deals=true`. https://agent.apiguru.app/agent/v1/v2/deals?geo=US&categories=Electronics&min_discount=40 ### One product is the big one, not the list `/v2/product-details` returns about 135 KB for a listing with many variations, and `fields=` works there too -- over REST as well as MCP, which it did not until an agent pointed out that the only documented way to slim a response was for list tools: https://agent.apiguru.app/agent/v1/v2/product-details?asin=B09DJLW458&fields=product_title,product_price,customers_say Measured on the ASIN above: 2,223 bytes against 135,923 for the whole record, a 61x reduction. It works on `/v2/product-reviews` too. Names you ask for that do not exist come back in `_unknown_fields` rather than being silently dropped, and `_omitted_fields` lists what was left out. `asin` is always included so a response can be matched back to its request. **Variants are summarised, not listed.** A listing family can have hundreds of siblings -- the Crocs Classic Clog has 733 -- and the ASIN-keyed maps that hold them (`variation` on details, `product_variations` on the batch endpoint) were 52 KB and 130 KB on it, most of the record and none of it about the product asked for. Both records now carry `variation_summary` instead: the attributes that vary, the distinct values of each, the sibling count, this ASIN's own values and `parent_asin`, about 1.5 KB at worst. The maps are still in the full record and in `_omitted_fields`; ask for one by name (`fields=variation`) when you need to resolve a value to a sibling ASIN. **If you cannot send a query string**, you do not need `fields=` as much as you might think: the record is ordered with the useful fields first. `asin`, title, price, star rating, rating count, `customers_say` and `badges` are inside the first 400 bytes; the bulk arrays (`variation`, `all_product_variations`, `product_reviews`, image lists, `tech_specs`) come last. Truncating the response is therefore safe -- you lose the bulk, not the summary. It used to be alphabetical, which put 53 KB of variations ahead of the 1 KB review summary. **Fields that repeat each other.** `product_information`, `tech_specs` and `product_details` are three views of the same specification table, and `product_photos`, `product_images` and `images_meta` overlap the same way. They are all passed through as Amazon presents them rather than merged, because different callers depend on different ones. Pick one and name it in `fields=`. ## Three things about Amazon's data that mislead - **Reviews pool across the listing family, counts AND text.** Every colour of one shoe reports the same `product_num_ratings`, so it is not a per-variant figure. The same is true of the review bodies returned by `/v2/product-reviews`: a query for a black Classic Clog can come back with a review of a different colour, or of the Bistro model altogether, because Amazon shows them on that listing. Do not attribute a complaint to the exact variant you asked about without checking the review text for a colour or model mentioned in it. - **A variant's title can be the parent's.** In search results the ASIN and the title can disagree; the URL slug usually shows the real variant. When the exact variant matters, call `/v2/product-details` on that ASIN - it is authoritative for the ASIN you passed. - **Amazon's Choice is awarded per search term, not per product.** This is the one that looks most like a bug in this API and is not. `/search` for `crocs black` returns B0014C0LUC with `badges: ["Overall Pick"]` and `is_amazon_choice: true`, while `/v2/product-details` for the same ASIN, seconds later, returns `amazon_choice: false`. Both are correct. We captured the live product page to check: it contains no badge markup at all. The badge belongs to the pair (product, query), so it exists in a result list and not on the product itself. Read it from the `/search` row that carried it, and record the query alongside it -- "Amazon's Choice" with no query attached does not mean anything. - **`/product` and `/v2/product-details` read different Amazon surfaces, and some fields differ because of it.** The batch endpoint reads Amazon's mobile API, the single one reads the product page. For B0014C0LUC the batch reports `Date First Available: September 1, 2023` (mobile field `site_launch_date`) and the detail page reports `April 3, 2025`. We checked the page: it says April 3, 2025 and contains no mention of 2023, so both are faithful readings of their own source rather than a parsing fault. Use `/v2/product-details` when you want what a person sees on the listing. If a date matters to your decision, say which endpoint it came from. **Do not read `Date First Available` as the product's age at all**: it tracks the listing record, which Amazon re-dates. Seen twice -- B0014C0LUC shows April 3, 2025 against 488,443 ratings, B008YA0Z44 shows October 1, 2025 against 50,879. Neither count can accumulate in that window. We checked the page for the first and it carries the date we return, so this is Amazon's value, not a parse fault. For how established a product is, use `product_num_ratings` and the best-seller rank. - **Refurbished listings report their real condition.** This was briefly wrong and is fixed: B0G4RV4F71, titled "... (Renewed Premium)", returned `condition: "new"`. Its buy box reads "Refurbished - Premium", so that was our bug, not a value from Amazon. It now returns `condition: "refurbished - premium"` with `buybox_winner.condition.is_new: false`. The correction only ever moves a condition away from new and only on evidence from the page, never the reverse -- calling a refurbished item new is the expensive direction of this mistake. Cross-check `product_title` for "Renewed" or "Refurbished" anyway if the distinction matters to you. `badges` is the source of truth for Amazon's Choice / Best Seller / Overall Pick (Amazon renamed that slot). `is_amazon_choice` and `is_best_seller` are conveniences derived from it. Note the shape differs by endpoint for historical reasons: `/search` rows carry a list of label strings, `/v2/product-details` carries an object of booleans. Check the type before indexing it. `customers_say` is Amazon's AI summary of the reviews, with per-aspect sentiment and mention counts. It is null when Amazon shows no summary for that listing, which is common on low-review products -- a null there is an answer, not a failure. ## Conventions - **Marketplaces** (20): US, CA, DE, MX, UK, FR, IT, ES, AU, BR, IN, JP, NL, AE, PL, SA, SG, SE, TR, BE. Pass as `geo`, chosen from the user's request or the Amazon domain they mention (amazon.de -> DE). The API assumes `US` only when the parameter is omitted; do not rely on that. - **ASIN format**: `^[A-Z0-9]{10}$`. Uppercase only. Lowercase ASINs are rejected with 400 - normalise before calling. - **Seller ID format**: `^[A-Za-z0-9]{13,15}$`. - **Sample ASIN for testing**: `B09DJLW458`. ## Error semantics These matter for cost control — some errors bill and some do not: - **400** — Bad input (bad ASIN format, unknown geo, missing required param). NOT billed. - **401** — Missing or invalid API key on the keyed path. - **402** — Payment required. On the agent path this carries a PAYMENT-REQUIRED challenge. On the keyed path it means the account balance is exhausted. - **403** — Account disabled, or no active subscription plan. - **404** — The ASIN genuinely does not exist on that marketplace. BILLED - the upstream fetch was performed and the bad input was the caller's. Retrying will not help; try a different geo. - **413** — Too many items in a batch request. - **429** — Per-second rate limit exceeded for the plan. Back off and retry. - **500** — Internal error. NOT billed. - **502** — Bad gateway -- our reverse proxy could not get an answer from the gateway. NOT billed. Same class as 503: retry with backoff. - **503** — Upstream fetch failed on our side (block, parse fault). NOT billed. Safe and correct to retry. - **504** — Gateway timeout. The upstream fetch ran past its deadline. NOT billed. Retry with backoff; a narrower query often succeeds. - **timeout** — No response before your own client's deadline. Nothing is billed for a request we never answered. Cold-geo sessions are the slow case and are bounded at 25s server-side; allow 60s. **Retry policy:** Retry 429, 500, 502, 503, 504 and client-side timeouts with backoff -- none of them are billed. Never retry 400, 401, 403, 404 or 413: the request itself is the problem and repeating it will not change the answer. ## Telling us what is broken This API has been fixed more than once because an agent said what was wrong with it. If a field is empty, mistyped, welded together or simply missing, say so: - **Preferred - GitHub issues:** https://github.com/apiguru-app/agent-kit/issues. An issue can hold a conversation; you get a reply on the thread. - **No GitHub account? The wall:** `POST https://dash.apiguru.app/api/v1/feedback` with `{"message": "...", "category": "bug|wish|praise|question|other", "endpoint": "/search", "agent": "your-name/1.0"}`. No key, no signup, never billed. - **Over MCP:** the free `send_feedback` tool does the same thing. - **Read what others wrote:** https://dash.apiguru.app/feedback