# Apiguru Amazon Data API > Real-time structured Amazon marketplace data: product details, reviews, search, best-sellers, deals, seller profiles, and live offers/stock across 23 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, connect the user's account (next section), send an API key as `X-API-KEY`, or come back after the window rolls. ## Connect the user's account (no wallet, no key to paste) Without a wallet you do not need the user to find and paste an API key. Ask for a connection code, show them one link, and pick up your own credential once they approve -- the way command-line tools sign in (OAuth 2.0 device authorization, RFC 8628). Every keyless 402 carries these two URLs as `connect_account`. ```bash curl -X POST https://agent.apiguru.app/agent/v1/device/code # -> {"device_code": "...", "user_code": "WDJB-MJHT", "interval": 5, "expires_in": 900, # "verification_uri_complete": "https://dash.apiguru.app/link?code=WDJB-MJHT..."} # Show the user verification_uri_complete. They sign in (or sign up) and approve. curl -X POST https://agent.apiguru.app/agent/v1/device/token -d device_code=THE_DEVICE_CODE # -> 400 {"error": "authorization_pending"} until they approve, then # -> 200 {"access_token": "agt_...", "token_type": "Bearer", "header": "X-API-KEY"} curl -H 'X-API-KEY: agt_...' \ 'https://agent.apiguru.app/agent/v1/v2/product-details/B09DJLW458/US' ``` - Poll no faster than `interval`; `slow_down` asks for 5 seconds more. `access_denied`: the user declined -- ask before starting again. `expired_token`: the code lapsed (15 minutes) or was already used; start again. GET works for both URLs if your client cannot POST. - The credential is yours and shown once: keep it like a password, never in a URL, a log or back in the chat. Calls bill the account that approved it, free trial requests first. The user can disconnect it at https://dash.apiguru.app/link at any time; it then answers 401 `invalid_agent_credential`. - It is accepted wherever an API key is: `X-API-KEY` or `Authorization: Bearer` here, and `X-API-KEY` on the keyed REST base and the MCP server. - Tell the user what the connection is for before you show the link. Approving gives this agent the use of their account. ## 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. Paged lists (`/search`, `/v2/seller-products`) carry `_links.next` while Amazon serves another page -- in path form (`https://agent.apiguru.app/agent/v1/v2/seller-products//US/2`) when the call had no filters, with the full query string when it had some, so the next page is the same list. Amazon serves at most 7 pages of one query: `metadata.reachable_pages` is the last one, and a page past it is a free 400 `page_out_of_range`. When `metadata.total_pages` is larger, `pagination_note` says how to see the rest: split the query into `min_price`/`max_price` bands or by `category_id`. `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 a call costs on a key.** The account is billed in requests at its plan's rate (https://dash.apiguru.app/pricing), not at the x402 prices above: - `/v2/product-details`: 1 request per call. - `/v2/product-reviews`: 1 request per call. - `/search`: 1 request per call. - `/product`: 1 request per item in `asins` (max 20). - `/stock`: 1 request per item in `asins` (max 10); `check_inventory=true` adds 1 per cart check that reads a quantity (one for `offers_count=winner` or an offer id, one per offer for `all`). - `/v2/best-sellers`: 1 request per call. - `/v2/deals`: 1 request per call. - `/seller-profile`: 1 request per item in `seller_ids` (max 10). - `/v2/seller-products`: 1 request per call. - `/v2/seller-reviews`: 1 request per call. Every answer on a key says what it cost and what is left, in headers: `X-Billed-Requests` (this call), `X-Balance-USD`, `X-Request-Price-USD` (the plan's rate), `X-Requests-Affordable` (how many more requests the account can pay for) and, while they last, `X-Trial-Requests-Remaining` or `X-Included-Requests-Remaining`. A 404 is billed like an answer; 400s, 429s and 5xx are not. `/product` costs the same per ASIN on a key as `/v2/product-details`, and its record lacks best-seller ranks, category path and merchant id: for a catalogue that needs those, call `/v2/product-details` per ASIN. The key goes in the header only; `?api_key=` in a URL is refused with a free 400 `api_key_in_query`. ## 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 (native USDC on each network; on Base 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)** on **Base mainnet (eip155:8453); also Polygon (eip155:137), Arbitrum (eip155:42161) and Avalanche (eip155:43114)**. - Watch `X-Free-Probes-Remaining` (or `X-Free-Probes-Available: yes|no` where no count is given) and `X-Price-Next-Call` on every response to know where you stand before you get a 402. ## Paying You need two things: an x402 client library, and an EVM wallet key whose address holds a little **USDC (native USDC on each network; on Base 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)** on **Base mainnet (eip155:8453); also Polygon (eip155:137), Arbitrum (eip155:42161) and Avalanche (eip155:43114)**. Nothing else: no account here, no ETH for gas (the facilitator submits the transfer), no minimum deposit. A call costs from $0.003; `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 (so up to $0.05 in one call); check `X-Price-Next-Call` or `/.well-known/x402` before raising the cap. No wallet and no way to get one? Connect the user's account with one link ("Connect the user's account" above), or use an API key from https://dash.apiguru.app/register, which 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/llms-full.txt` - `https://agent.apiguru.app/.well-known/x402` - `https://agent.apiguru.app/.well-known/agent-card.json` - `https://agent.apiguru.app/robots.txt` `/.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`). When the free calls run out, a tool returns an x402 PaymentRequired result; an x402-capable MCP client pays it in-band by resending the same call with the payment in `_meta["x402/payment"]`. - [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.003 per call. `fields=` returns only the keys you name. - **`GET /v2/product-reviews`** — Customer reviews for a single ASIN. Required: `asin`. Price: $0.003 per call. Options: `from_rating`, `to_rating`. `from_rating` / `to_rating` (1-5) pick the star range. - **`GET /search`** — Search Amazon products by keyword. Required: `query`. Price: $0.003 per call. Options: `page`, `sort_by`, `category_id`, `min_price`, `max_price`, `product_condition`, `brand`, `seller_id`, `today_deals`, `deal_type`. `page` up to `metadata.reachable_pages` (Amazon serves 7 at most), `sort_by`, ten filters; `_links.next` is the next page. - **`GET /product`** — Product detail for up to 20 ASINs in one call. Required: `asins`. Price: $0.0024 per item (max 20). App record: no best-seller ranks, category path or merchant id (use product_details for those). - **`GET /stock`** — Live offers and inventory for up to 10 ASINs. Required: `asins`. Price: $0.0045 per item (max 10). Options: `check_inventory`, `offers_count`, `condition`. `check_inventory=true` adds each offer's stock quantity (a billed cart check per offer); `offers_count=winner` keeps only the buy-box offer. - **`GET /v2/best-sellers`** — Best-seller rankings for a category. Required: none. Price: $0.003 per call. Options: `category`, `subcategory_code`, `page`. `category` (slug or name), `subcategory_code`, `page` 1-5 (50 rows each). - **`GET /v2/deals`** — Current Amazon deals with filters. Required: none. Price: $0.003 per call. Options: `offset`, `categories`, `brands`, `min_product_star_rating`, `min_price`, `max_price`, `min_discount`, `max_discount`, `prime_exclusive`, `prime_early_access`. `categories`, `brands` (ids from `available_filters`), price and discount bounds, `offset` (`next_offset`). - **`GET /seller-profile`** — Seller profiles for up to 10 seller IDs. Required: `seller_ids`. Price: $0.0036 per item (max 10). Each row has `status`: ok, not_found (billed) or unavailable (retry it). - **`GET /v2/seller-products`** — Products listed by a seller. Required: `seller_id`. Price: $0.003 per call. Options: `query`, `page`, `sort_by`, `category_id`, `min_price`, `max_price`, `product_condition`, `brand`, `today_deals`, `deal_type`. `page` up to `metadata.reachable_pages`, `sort_by`, the search filters; `_links.next`; no single sort order lists a whole storefront (`coverage_note`). - **`GET /v2/seller-reviews`** — Feedback reviews for a seller. Required: `seller_id`. Price: $0.003 per call. Options: `page`, `from_rating`, `to_rating`. `page`, `from_rating` / `to_rating`. Two answer shapes, for history's sake. The v2 endpoints answer `{success, data, request_id}`; `/stock`, `/product` and `/seller-profile` answer `{success, results, request_id, ...}` with one row per item, and on a key `billable_requests_count`. Both carry `_links`. Every answer that reached the data layer has the same id in the `X-Request-Id` header and the body's `request_id`: quote it when you report a problem. **A storefront is not one list.** Amazon counts and orders a seller's products differently per sort order, and no order holds all of them: on one store the default (relevance) order counted 321 products and repeated some across its pages while NEWEST counted 1,898; on another the two orders shared 90 of 161 products. To list a whole storefront, page through `sort_by=RELEVANCE` and `sort_by=NEWEST`, split a store larger than 336 products (7 pages of 48) into `min_price`/`max_price` bands, and dedupe by `asin`; page 1 says so in `coverage_note`. An empty storefront (`note`) looks the same for a seller with no listings and for an id that is not a seller: `/seller-profile` tells them apart. ## Recipe: watch a seller's storefront on a schedule A daily job that lists each seller's Amazon storefront and reports what changed: new products, products gone, price moves. 1. Run it on the user's account, not on free calls: a job that repeats needs `X-API-KEY` (or the account connected as above). On a key each page is billed as one request at the plan's rate (https://dash.apiguru.app/pricing). 2. For each seller id, fetch `https://agent.apiguru.app/agent/v1/v2/seller-products//` and follow `_links.next` until there is none or `metadata.reachable_pages` is reached (7 at most). For a store larger than one sort order shows, page `sort_by=NEWEST` as well and split it into `min_price`/`max_price` bands, as above; dedupe by `asin`. 3. Keep `asin`, `product_title` and `product_price` from each run and compare with the last one. 4. Optional: `/seller-profile/,,.../` (up to 10 sellers per call) for each seller's rating and feedback count, and `/v2/seller-reviews` for new feedback. A seller with seven pages costs seven requests a run, so ten sellers once a day is about 2,100 requests a month. Answers are fetched live: once a day is enough for a storefront. ## 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: Amazon's product page for that ASIN shows no badge 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` can disagree on a few fields.** For B0014C0LUC the batch reports `Date First Available: September 1, 2023` and the detail endpoint reports `April 3, 2025`, which is the date the listing itself shows. 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, yet the first date is the one Amazon's listing shows. 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 listing, 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** (23): US, CA, DE, MX, UK, FR, IT, ES, AU, BR, IN, JP, NL, AE, PL, SA, SG, SE, TR, BE, IE, ZA, EG. 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}$`. On agent.apiguru.app and the MCP server any case is accepted and upper-cased, and an Amazon product URL (/dp/ASIN, /gp/product/ASIN, ?asin=) works in place of an ASIN; its domain sets geo unless geo names another marketplace (then 400). The answer lists what was rewritten under parameters_interpreted (MCP: _input_interpreted). - **Seller ID format**: `^[Aa][A-Za-z0-9]{9,20}$`. An Amazon seller id is 'A' followed by 9-20 letters and digits (10-21 characters in all) -- the seller= or me= value of a storefront URL, e.g. A2A1RNLLUK3HYA. Anything else is rejected with 400 before any fetch. - **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), or a page past the last one Amazon serves for a list (`code: page_out_of_range`, with `reachable_pages`). On agent.apiguru.app an `api_key` in the URL is refused the same way (`code: api_key_in_query`): send it as the X-API-KEY header. 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 (over MCP: an x402 PaymentRequired tool result, payable in-band via _meta["x402/payment"]). 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 lookup 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 (`code: rate_limited`, `Retry-After: 1`). NOT billed. Back off and retry. - **500** — Internal error. NOT billed. - **502** — Bad gateway. NOT billed. Same class as 503: retry with backoff. - **503** — Temporary failure on our side (`code: upstream_unavailable`). NOT billed. Safe and correct to retry. - **504** — Gateway timeout: no answer within 55 s, so agent.apiguru.app stopped the request before it was billed (`code: deadline_exceeded`). NOT billed. Retry once; a batch with fewer items answers sooner. - **timeout** — No response before your own client's deadline. A keyless call without a payment is never billed. On an API key, or with an x402 payment attached, the request may still complete after your client gave up, and is then billed like any answered call. Some marketplaces answer more slowly than others; allow 60s rather than retrying early (agent.apiguru.app answers a 504 itself at 55 s). **Retry policy:** Retry 429, 500, 502, 503 and 504 with backoff -- none of them are billed -- unless the error body says `retryable: false`. A client-side timeout on a key or a payment may have completed and been billed; allow 60s before retrying it. Never retry 400, 401, 402, 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 The wall is public. Send only what the user has agreed to, and only about the API: the tool, the parameters, the field, what you expected and the `request_id` -- never an API key, personal data, or the user's own prompts or documents.