‹ API Foundations Lesson 12 of 16
Contents Lesson 12 of 16

5 min read · practitioner

What is the API telling you when it says no?

A status code is a diagnosis. Read it properly and you know whose problem it is, whether waiting will help, and what to change. Seven of them do almost all the work here.

400 Bad Request — a malformed request. Yours. Never retryable — it will fail identically forever. Note what is not on this list, because the previous unit measured it: an out-of-enum value, an unparseable date and a missing "required" parameter do not produce a 400 here. They produce a 200 and an answer to a different question.

422 Unprocessable Content — the code this API actually returns when it does reject your input, and the one you will meet most often, because it is the one your own typos produce. /intraday/{ticker}?from=2021-08-02 (a date where an integer is required) returns 422 with {"errors":{"from":["The from must be a number."]}}; a bare /calendar/dividends returns 422 demanding its filter. Yours. Never retryable, exactly like a 400 — and if your error handling was written from a list that stops at six codes, this is the branch that falls through to your default.

401 Unauthorized — invalid or missing token. Your credentials. Also never retryable.

403 Forbidden — access denied for the current subscription. This is the one people misdiagnose. The URL is correct, the symbol exists, the parameters are legal — the plan simply does not include that endpoint or dataset. Editing the ticker will never fix a 403. The fix is entitlement, not code.

404 Not Found — the symbol or resource does not exist. A typo, a delisting, a rename, or a missing exchange suffix off the US.

429 Too Many Requests — the per-minute rate limit. The one code in this list that genuinely deserves a retry, and the response tells you when: Retry-After in seconds, plus X-RateLimit-Limit and X-RateLimit-Remaining.

500 Internal Server Error — the server's problem, not yours. Retryable with backoff, because a struggling service does not need your traffic doubled.

Reading the body, not just the code

There are three error body shapes in this API, and you will meet all three.

The spec defines an error object with three fields:

{"status": 400, "error": "Bad Request", "message": "Detailed error message"}

The message is the field worth logging — it is the only part that says what specifically was wrong.

A 422 returns something else entirely — a validation envelope keyed by field name:

{"errors": {"from": ["The from must be a number."]}}

And some errors are not JSON at all. Called live, /eod/VOW3 returned a 404 whose body was the plain string Ticker Not Found. The spec is consistent with this: several of its documented error responses are typed text/html, not application/json.

So branch on the status code before you touch the body, and never let an error-handling path throw its own exception because it assumed a shape. A client that pipes every response into a JSON parser reports a parse error where it should have reported a bad parameter, and you debug the wrong layer.

The retry rule, and the trap it avoids

Retry 429 and 5xx. Do not retry 400, 422, 401, 403 or 404.

When you do retry, back off. Honour Retry-After if it is present; otherwise wait 1 second, then 2, then 4, then stop and raise a real error. Three attempts, seven seconds of waiting, one loud failure.

Here is what the alternative costs. A job over 200 symbols that retries every failure immediately, five times, turns a 200-call job into a 1,000-call job the moment the service has one bad minute. Worse, every one of those retries also counts against the per-minute limit that produced the 429 in the first place. Retrying a 429 without waiting is the cause of the next 429. You are not recovering from the problem; you are manufacturing it.

A 200 is not proof of success

The last habit, and the one that catches experienced people. An empty array is a 200. A date range containing no trading days is a 200 with []. A filtered request that matched nothing is a 200.

So checking status == 200 and moving on is not validation. Check that the payload contains what you asked for — the right number of rows, the right date range, the fields you intend to read. Unit 4 is entirely about that.

Try it now

  1. Here is a request for a symbol that does not exist, made on 28 September 2026, with the status, the declared content type and the whole body. Say which of the three body shapes above this is, whether a JSON parser would accept it, and which line of your own client would read it first.
request status Content-Type body
/eod/NOSUCHTICKER.US 404 text/html; charset=utf-8 Ticker Not Found.
  1. Go through your own error handling and list which of the seven codes it currently retries. Delete the retries on 400, 422, 401, 403 and 404 — and check that 422 has a branch at all, since it is the one most likely to be falling through to your default.
  2. A date range that is entirely a weekend, /eod/AAPL.US?from=2026-09-26&to=2026-09-27&fmt=json, returned status 200 and the whole body below on 28 September 2026. Decide what your code should do about that case, and how it tells it apart from a range that should have held rows.
[]