A few years ago I integrated an Amazon data provider into a pricing service. The sales page said "real-time data". The docs said "high success rate". Both were technically true and both were useless.
What I eventually learned: the pricing service was reading from a cache refreshed once a day, and "high success rate" counted HTTP 200 responses, which included a meaningful number of captcha pages. We shipped two weeks of pricing recommendations built partly on blocked page responses, because our client only checked the status code.
This post is the test I wish I had run first. It takes an afternoon, it is about a hundred lines of Python, and it turns every vague vendor claim into four numbers you can put on a dashboard.
The four numbers that actually matter
Before the code, the definitions. These four are the difference between a provider you can build on and a provider you have to babysit.
| Metric | Definition | Why it matters |
|---|---|---|
| Median latency | 50th percentile response time | Your everyday experience |
| p95 latency | 95th percentile response time | Sets your timeout and retry policy |
| Success rate | Parsed successfully ÷ total requests | Not HTTP 200. Determines your real unit price |
| Field completeness | Records with all required fields ÷ successful records | Whether the data is usable at all |
The success rate definition deserves emphasis, because it is where most home-grown checks go wrong. An HTTP 200 that returns a bot-detection page is a failure, not a success. If your client only looks at the status code, that bad record flows straight into your database and you find out two weeks later when someone asks why the dashboard looks strange.

On p95: it determines your timeout. If p95 is five seconds and you set a three second timeout, you are throwing away a slice of requests that would have succeeded. Set it to ten seconds and you slow the whole pipeline. A reasonable starting point is 1.5× p95, with a retry cap of two. More retries than that usually means you are being throttled, and retrying into a throttle makes it worse.
Why per-request pricing is the wrong unit
Here is the part that changes how you read a pricing page.
A failed request still bills. A blocked page still bills. A response missing the field you need still bills.
So the unit worth comparing is cost per thousand usable records:
cost per 1k usable records = (monthly spend ÷ records parsed successfully with all required fields) × 1000
Worked example, because the gap is counterintuitive:
- Plan A: $1.20 per 1k requests, 92% success, 88% field completeness. Usable share is roughly 81%, so the real cost is about $1.48 per 1k usable records.
- Plan B: $1.60 per 1k requests, 99% success, 98% field completeness. Usable share is about 97%, so the real cost is about $1.65 per 1k usable records.
Plan A looks 25% cheaper on the rate card. The real gap is about 10%. Add the debugging hours Plan A will generate and the ranking usually flips.
So the question to ask a vendor is never "how much per thousand requests". It is: how do you define success rate, what is your field completeness, and do failures bill?
The four-layer pipeline
Whichever route you take, the chain has the same shape. What differs is which layers you own.
Amazon public pages (product / search / review / best sellers / sponsored)
↓
[Collection] anti-bot · browser rendering · proxies and geo · retries
↓
[Structuring] parsing · field normalization · type contract · failure semantics
↓
[Delivery] REST API ── or ── MCP tools
↓
Your application / data pipeline / AI agent
Four routes, and how they split ownership:
| Route | Collection | Structuring | Delivery | You maintain |
|---|---|---|---|---|
| Self-built scraper | You | You | You | Everything |
| General scraping API | Vendor | You | Vendor | Parsing, field drift |
| Amazon-native data API | Vendor | Vendor | Vendor | Business logic only |
| Official SP-API | Vendor | Vendor | Vendor | Quota and auth |
Choosing a route is really deciding which layers you want to own. More layers means more control and more responsibility.
One thing worth stating plainly: if you only need data tied to your own seller account, use the official SP-API and skip third parties entirely. Orders, inventory, your own listings. It is the most compliant and cheapest path, and recommending anything paid for that job would be dishonest.
The real fork is whether you need public marketplace facts — competitors, categories, search results, sponsored placements. That is where the other three routes become relevant.
Build the test
Enough theory. Here is the script. It samples across marketplaces and object types, then reports all four numbers plus a breakdown of which fields went missing.
#!/usr/bin/env python3
"""
Amazon Data API acceptance test.
Samples N calls across marketplaces and object types, then reports
median latency, p95 latency, success rate and field completeness.
"""
import argparse
import os
import statistics
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
import requests
API_BASE = "https://api.pangolinfo.com" # confirm against current docs
# Your business-critical fields. Change these to match what you actually store.
REQUIRED_PRODUCT_FIELDS = ["asin", "title", "price", "rating", "bsr", "fetchedAt"]
REQUIRED_SEARCH_FIELDS = ["keyword", "page", "results", "sponsoredCount", "fetchedAt"]
MARKETPLACES = ["amazon.com", "amazon.co.uk", "amazon.de"]
SAMPLE_ASINS = [
"B0CXYZ1234", "B0ABCDEFGH", "B012345678",
"B0TEST0001", "B0TEST0002", "B0TEST0003",
]
SAMPLE_KEYWORDS = ["insulated water bottle", "standing desk", "air purifier"]
@dataclass
class CallResult:
ok: bool = False
latency_ms: float = 0.0
fields_ok: bool = False
missing: list = field(default_factory=list)
error: str = ""
def check_fields(payload: dict, required: list) -> tuple:
missing = [f for f in required if f not in payload or payload[f] in (None, "", [], {})]
return (not missing), missing
def call_product(api_key: str, asin: str, marketplace: str) -> CallResult:
result = CallResult()
started = time.perf_counter()
try:
resp = requests.get(
f"{API_BASE}/product",
params={"asin": asin, "marketplace": marketplace},
headers={"Authorization": f"Bearer {api_key}"},
timeout=30,
)
result.latency_ms = (time.perf_counter() - started) * 1000
# HTTP 200 is not success. A blocked page also returns 200.
if resp.status_code != 200:
result.error = f"HTTP {resp.status_code}"
return result
payload = resp.json()
data = payload.get("data", payload)
result.ok = True
result.fields_ok, result.missing = check_fields(data, REQUIRED_PRODUCT_FIELDS)
except Exception as exc: # noqa: BLE001
result.latency_ms = (time.perf_counter() - started) * 1000
result.error = f"{type(exc).__name__}: {exc}"
return result
def call_search(api_key: str, keyword: str, marketplace: str, page: int) -> CallResult:
result = CallResult()
started = time.perf_counter()
try:
resp = requests.get(
f"{API_BASE}/search",
params={"keyword": keyword, "marketplace": marketplace, "page": page},
headers={"Authorization": f"Bearer {api_key}"},
timeout=30,
)
result.latency_ms = (time.perf_counter() - started) * 1000
if resp.status_code != 200:
result.error = f"HTTP {resp.status_code}"
return result
payload = resp.json()
data = payload.get("data", payload)
result.ok = True
result.fields_ok, result.missing = check_fields(data, REQUIRED_SEARCH_FIELDS)
except Exception as exc: # noqa: BLE001
result.latency_ms = (time.perf_counter() - started) * 1000
result.error = f"{type(exc).__name__}: {exc}"
return result
def build_tasks(api_key: str, samples: int):
"""Alternate product and search calls; rotate marketplace and page depth."""
tasks = []
for i in range(samples):
marketplace = MARKETPLACES[i % len(MARKETPLACES)]
if i % 2 == 0:
asin = SAMPLE_ASINS[i % len(SAMPLE_ASINS)]
tasks.append(("product", call_product, (api_key, asin, marketplace)))
else:
keyword = SAMPLE_KEYWORDS[i % len(SAMPLE_KEYWORDS)]
page = (i % 3) + 1 # 1..3, also probes deep-page availability
tasks.append(("search", call_search, (api_key, keyword, marketplace, page)))
return tasks
def percentile(values: list, pct: float) -> float:
if not values:
return 0.0
ordered = sorted(values)
idx = min(int(len(ordered) * pct / 100), len(ordered) - 1)
return ordered[idx]
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--samples", type=int, default=100)
parser.add_argument("--concurrency", type=int, default=8)
args = parser.parse_args()
api_key = os.environ.get("PANGOLINFO_API_KEY")
if not api_key:
raise SystemExit("Set PANGOLINFO_API_KEY first")
tasks = build_tasks(api_key, args.samples)
results: list = []
with ThreadPoolExecutor(max_workers=args.concurrency) as pool:
futures = [pool.submit(fn, *params) for _, fn, params in tasks]
for future in as_completed(futures):
results.append(future.result())
latencies = [r.latency_ms for r in results]
succeeded = [r for r in results if r.ok]
complete = [r for r in succeeded if r.fields_ok]
total = len(results)
success_rate = len(succeeded) / total * 100 if total else 0
completeness = len(complete) / len(succeeded) * 100 if succeeded else 0
usable_rate = len(complete) / total * 100 if total else 0
print("\n=== Amazon Data API acceptance report ===")
print(f"samples : {total}")
print(f"median latency : {statistics.median(latencies):.0f} ms")
print(f"p95 latency : {percentile(latencies, 95):.0f} ms")
print(f"max latency : {max(latencies):.0f} ms")
print(f"success rate : {success_rate:.1f}%")
print(f"field completeness : {completeness:.1f}%")
print(f"usable share : {usable_rate:.1f}%")
missing_counter: dict = {}
for r in succeeded:
for field_name in r.missing:
missing_counter[field_name] = missing_counter.get(field_name, 0) + 1
if missing_counter:
print("\nmissing fields (by occurrence):")
for field_name, count in sorted(missing_counter.items(), key=lambda x: -x[1]):
print(f" {field_name}: {count}")
errors: dict = {}
for r in results:
if r.error:
key = r.error.split(":")[0]
errors[key] = errors.get(key, 0) + 1
if errors:
print("\nerror distribution:")
for key, count in sorted(errors.items(), key=lambda x: -x[1]):
print(f" {key}: {count}")
print("\nFeed the usable share into the formula to get your real unit price:")
print(" cost per 1k usable records = monthly spend / usable records * 1000")
print(f" at {usable_rate:.1f}% usable, the list price is effectively {100 / usable_rate:.2f}x higher\n")
if __name__ == "__main__":
main()
Reading the output
A healthy run looks something like this:
=== Amazon Data API acceptance report ===
samples : 100
median latency : 2980 ms
p95 latency : 5240 ms
max latency : 8120 ms
success rate : 99.0%
field completeness : 98.0%
usable share : 97.0%
Two things to look at beyond the headline numbers.
The missing-fields breakdown. If gaps cluster on one field, that field has weak coverage in your target categories. That is a specific conversation with the vendor, not a reason to reject the whole provider.
The error distribution. If you see a lot of timeouts, you have a latency problem. If you see parse failures on otherwise healthy responses, you have a structuring problem. These two have completely different fixes, and lumping them into one error log is how teams spend a week debugging the wrong thing.
Then re-run it weekly. A single run proves it works today. Weekly runs prove it still works. Quality degradation is almost always gradual — drifting from 99% to 95% takes weeks, and humans notice about two weeks after it matters.
The field matrix behind the checklist
The REQUIRED_PRODUCT_FIELDS list should not be invented. It should come from a matrix of what public Amazon pages can actually be structured into.
| Object | Key fields | Common gaps |
|---|---|---|
| Product | ASIN, title, brand, price, rating, BSR, availability, variants | Incomplete variant dimensions; lost parent-child ASIN links; sale price mixed with list price |
| Search | Keyword, page, organic rank, ad rank, isSponsored | Sponsored mixed with organic; deep pages (7+) truncated |
| Review | Rating, title, body, date, verified purchase, variant, helpful votes | Truncated bodies from summary pages; no variant attribution |
| Offers | Seller, price, shipping, Buy Box holder, stock | Inconsistent Buy Box logic; only first offer returned |
| Best Sellers | Category, rank, ASIN, rank movement | Unlabeled category-tree changes; non-continuous history |
| Seller | Seller ID, name, rating, listing count | Weak seller-to-brand mapping; IDs differ by marketplace |
| Category | Category tree, node ID, filters, item count | Node IDs vary by site; incomplete filter enums |
| Sponsored | Ad type (SP/SB/SD), placement, rank, creative | Ads not separated from organic; missing creative fields |
Two fields I would put on every checklist.
fetchedAt. A capture timestamp. Without it the record cannot enter a time series and cannot be audited. Three months later, when a price looks wrong, you cannot tell whether it was wrong then or got mangled later.
isSponsored on search results. Without it, organic rank and ad placement are indistinguishable. You think you rank third; the top two slots are ads; your true organic position is first. Every optimization decision built on the contaminated number points the wrong way.
Not everything needs to be real time
Having argued that "real time" is widely abused, it is worth being equally clear about the opposite mistake: refreshing everything at maximum frequency.
Field change rates vary by orders of magnitude. Brand names, category assignments and variant dimensions barely move. Price, stock, Buy Box ownership and sponsored placements move constantly. Treating them identically is the most common form of budget waste in this category.
A three-tier split is usually right:
High frequency, minutes to hours — price, stock, Buy Box holder, sponsored placements, BSR. For these, a stale value does not weaken the insight, it invalidates it. A competitor's price from yesterday cannot support a pricing decision today.
Daily — new reviews, rating changes, seller lists, best seller ranks. Trend signals that matter over weeks rather than minutes.
Weekly or on demand — brand, category, variant dimensions, images, A+ content. Refreshing these hourly is pure waste with no analytical benefit.
The practical observation from many deployments: the top 20% of ASINs drive roughly 80% of the decisions. Running that 20% at high frequency and the long tail weekly typically cuts cost by half with almost no measurable business impact. It is the highest-leverage cost optimization available, and it requires no vendor negotiation — it is purely a scheduling decision in your own ingestion layer.
Two implementation notes.
First, implement the tiering in your own ingestion layer rather than hoping the provider does it. You know which ASINs matter to your business; they do not.
Second, revisit it periodically. Business priorities shift, a category that was long tail becomes strategic, and a tiering decision made eighteen months ago is usually wrong today. Put a recurring reminder on it.
REST or MCP
Worth separating, because they get conflated constantly.
REST fits deterministic, scheduled batch work. You know which ASINs to pull, how often, and which table the results land in. Data pipelines and scale collection.
MCP fits exploratory work that needs reasoning. A user or agent states a business question and the agent decides which tools to call, how many pages to check, and how to cross-validate. Research, diagnostics, one-off analysis.
One line: with REST you write a program that fetches data; with MCP you ask an agent to fetch and explain. Backend service consumer, use REST. AI agent consumer, MCP removes a lot of glue code.
The decision tree
Step 1. Is the data only about your own seller account? Yes, use the official SP-API. No, continue.
Step 2. Do you need public marketplace facts — competitors, categories, search, sponsored placements? No, revisit the requirement. Yes, continue.
Step 3. Do you have engineers who will own anti-bot, rendering and parsing maintenance long term? No, pick an Amazon-native data API. Yes, convert engineering hours to money and compare against purchase price on a per-thousand-usable-records basis.
Step 3 is where teams go wrong by comparing cash only. Price the engineering time at real internal cost. Plenty of "building is cheaper" instincts reverse once that number is on the page.
For reference, Pangolinfo publishes a median latency around 3 seconds, a 99% success rate, and more than 30 million calls per day; on sponsored placements, the published collection rate across 13 marketplaces is 91.4%. The point of those numbers is not that they are large. It is that you can reproduce or refute them with the script above.
Wrapping up
If the numbers come back bad, here is the triage.
Low success rate with high latency is usually capacity. Lower your concurrency, or ask about quotas and whether you are being throttled.
High success rate with low field completeness is a coverage problem. Identify exactly which fields are missing and ask about those specific fields in your target categories. This is often fixable, or at least scoped to particular categories you can route around.
Good numbers on page one that collapse by page three is a deep-page limitation. This one is usually a hard boundary rather than a transient issue, and it matters enormously if your business depends on tracking keywords where you rank deep.
Knowing which of the three you have determines whether the fix is configuration, a conversation, or a different provider. Collapsing them into "the data is bad" is how teams waste a week.
If you take one thing from this post, take the ordering:
- Write the field checklist first (15–30 fields).
- Score candidates field by field, not "do you support products?".
- Convert list price to cost per thousand usable records.
- Run the 100-call test and record all four numbers.
- Look at the price sheet last.
If you want to run the script against real data, grab a key from the console or read the Amazon Data MCP docs. The underlying products are Amazon Scraper API for product, search, best seller, category and sponsored objects, Amazon Review API for reviews and customer voice, Amazon Data MCP when an agent needs to call data directly, and Amazon Scraper Skill for conversational workflows.
The full buyer's guide with the field matrix, three real JSON samples and the acceptance method is here: Amazon Data API: The Complete Buyer's Guide.
If you run the test, I would be curious what numbers you get — especially p95. That is the one where published specs and reality tend to diverge most.

Top comments (0)