DEV Community

Cover image for Amazon's advertising API docs return HTTP 200 for every URL, including ones that don't exist
amzi smith
amzi smith

Posted on

Amazon's advertising API docs return HTTP 200 for every URL, including ones that don't exist

That project is now 2,140 commits, 189 database migrations and 3,436 tests. It runs my account every night, and a handful of other people's.

Almost nothing that slowed me down was code. What slowed me down was that the documentation described a system slightly different from the one that was actually answering my requests. These are the specific things I wish someone had told me on day one. Four of the five are not Amazon-specific at all.


1. The docs site returns 200 for pages that were never real

Run this:

curl -s -o /dev/null -w "%{http_code}\n" -L \
  "https://advertising.amazon.com/API/docs/en-us/sponsored-products/3-0/openapi"

curl -s -o /dev/null -w "%{http_code}\n" -L \
  "https://advertising.amazon.com/API/docs/en-us/this-page-does-not-exist-abc123"

curl -s -o /dev/null -w "%{http_code}\n" -L \
  "https://advertising.amazon.com/API/docs/en-us/completely/made/up/nonsense/xyz789"
Enter fullscreen mode Exit fullscreen mode

All three return 200. I invented the last two.

It is a single-page app. The server hands back the same HTML shell for every path and the route gets resolved in the browser, so anything that judges a page by its status code gets a confident yes for a URL that never existed. That includes your link checker, your scraper, and any AI agent you hand a documentation URL to.

I lost most of a day to this. I had asked an assistant to read a docs page, it fetched a URL that resolved to the shell, and it summarized the shell back to me as though it were the page. The summary was plausible and entirely invented. Nothing in the pipeline failed loudly.

The lesson that generalizes: for any SPA-served docs site, a status code is not an existence check. Assert on something only the real page contains.

2. The real OpenAPI specs are not on the docs site

I spent a long time trying to scrape endpoint definitions out of rendered HTML before finding that the actual machine-readable specs sit on a CDN:

https://d1y2lf8k3vrkfu.cloudfront.net/openapi/en-us/dest/SponsoredProducts_prod_3p.json
Enter fullscreen mode Exit fullscreen mode

That is a 652 KB OpenAPI document — every endpoint, every model, every enum. Swap the product name for the other ad types.

It is not linked from anywhere obvious. Once I had it, a whole category of guesswork disappeared: I stopped inferring request shapes from prose and started reading them from the schema.

The lesson that generalizes: before you scrape a docs site, spend twenty minutes looking for the artifact the docs site is itself rendering. It is usually there and it is usually better.

3. A 429 does not always come from the API

We started getting throttled on one endpoint. The obvious reading was that we were calling it too much, so the obvious fix was to slow down.

That was wrong, and it took real measurement to see it. Our total call volume had been flat for ten days. The refusal rate climbed while that particular endpoint's volume fell. And the API's own logs showed zero throttles — because the requests were never reaching the API. Something in front of it was answering.

The tell is in the headers. A response that genuinely came from Amazon's API carries x-amzn-requestid. An edge or CDN layer refusing you before the request lands does not have one to give.

x-amzn-requestid present  -> the service throttled you. Back off.
x-amzn-requestid absent   -> something in front of the service refused you.
                             Backing off may not help at all.
Enter fullscreen mode Exit fullscreen mode

Those two situations look identical in a log line that only records the status code, and they need opposite responses. Log the request id, or you will spend a week tuning a backoff against a wall that does not care how slowly you knock.

The lesson that generalizes: when an external system rejects you, establish which system rejected you before you design around it.

4. The query that quietly returns exactly 1,000 rows

Not Amazon. This one is PostgREST, which means Supabase, which means a lot of people reading this.

A plain select has a default ceiling. Ask for 4,000 rows and you get 1,000, with no error, no warning, and no flag on the response saying it truncated. Your code carries on and treats a quarter of the data as all of it.

The reason it is nasty is that it is invisible until you cross the line, and it stays invisible afterward. Every symptom looks like a data problem somewhere else.

Two habits fixed it for me:

  • Treat a result length of exactly your limit as suspicious by default, not as a complete answer.
  • Page explicitly with .range() when a set can plausibly grow past the cap.

I now have a shared helper for this, because I did not want the decision to be made independently in forty places.

5. An update that changed nothing reports success

Also PostgREST, same family of problem.

await db.from("campaigns").update({ daily_budget: 45 }).eq("id", someId);
Enter fullscreen mode Exit fullscreen mode

If no row matches that id, this is not an error. It is a successful update of zero rows. You get no exception and, unless you asked for the rows back, nothing to tell you the write did not land.

I had writes silently doing nothing for a while before I noticed. Where the row genuinely ought to exist, .upsert() says what I actually meant. Where it might not, I check the returned count instead of trusting the absence of an exception.

Related, and it caught me twice: .eq() never matches NULL. That is correct SQL, and it is still surprising when a filter you think is exhaustive silently excludes every row where the column was never set. A two-value filter quietly becomes a three-value problem.


What building this as a non-engineer actually taught me

I use AI assistants heavily. I could not have built this without them. But the thing that made the difference was not the code they wrote — it was learning, slowly and expensively, that a confident answer is not a verified one.

Every problem above has the same shape. Something returned a value that looked like an answer. A 200 that was a shell. A thousand rows that were four thousand. A successful write that wrote nothing. A 429 from something that was not the API.

None of those announce themselves. They all read as success. And an AI assistant reading the same signals will tell you everything is fine, in fluent and reassuring prose, because everything it can see says so.

So the habit I have built, which is the only real skill I have picked up in ten months, is to ask one question before believing anything:

If this had failed, what would I be looking at right now?

If the honest answer is "exactly what I am looking at," then I have not checked anything yet. That question has caught more bugs than any tool I own.


I build RedHen Labs, flat-fee Amazon advertising software. The SP-API code examples and an MIT-licensed search term report parser are public at github.com/elementenergy41-cell/RedHen-Labs, along with a break-even calculator you are welcome to host on your own site.

Top comments (0)