When a call fails, beginners tend to change things at random until it works: re-copy the key, add a header, try a different parameter. Experienced developers read two things first, the status code and the error message, and usually know what to fix before they touch the code.
The status code is the three-digit number on the first line of every HTTP response, the 200 you saw with curl -i in lesson 1. It is designed to be read by a program, so your code can decide what to do without parsing English. The error field in the envelope is the same verdict written for a human. Between them, they answer the two questions that matter: what kind of problem is this, and what should change.
The five families
The first digit carries most of the meaning. You can look any code up with the HTTP Status Code Lookup API, which is a neat way to practise, since it is an API about the very thing that comes back from APIs:
curl "https://api.apiverve.com/v1/httpstatuslookup?code=429" \
-H "x-api-key: YOUR_API_KEY"Read name and category, then try 401, 403 and 503.
{
"status": "ok",
"error": null,
"data": {
"code": 429,
"name": "Too Many Requests",
"description": "The user has sent too many requests in a given amount of time",
"category": "Client Error",
"is_error": true,
"is_success": false,
"is_redirect": false,
"is_informational": false
}
}
| First digit | Family | Whose move is it? |
|---|---|---|
1xx | Informational | Rarely seen directly; your HTTP client handles them |
2xx | Success | Nobody's. Read data |
3xx | Redirect | Your HTTP client usually follows it automatically |
4xx | Client error | Yours. Something about the request needs to change |
5xx | Server error | Theirs. The request may be fine; try again later |
That last column is the most useful idea on this page. A 4xx means sending the same request again will get the same answer, so retrying is pointless until you change something. A 5xx means the server had a problem, and the identical request may well succeed in a few seconds. One exception to the rule lives in the 4xx range, and it gets its own section below.
A call returns a status in the 4xx range. In general, what should happen next?
Answer the check above to continue.
429: the code with two meanings
429 Too Many Requests is the exception to "4xx means change the request". On APIVerve it means one of two very different things:
- You are going too fast. You exceeded your plan's per-minute rate limit. The identical request will succeed once you slow down.
- You are out of credits. Your monthly allowance is spent. The identical request will fail until the allowance renews or you upgrade, so retrying is pointless and only wastes time.
Both arrive with the same status code, so the code alone cannot tell you which. The reliable test is the x-api-remaining-credits response header. If it is 0, you are out of credits. If it is above zero, you were rate-limited.
curl -sS -D - -o /dev/null 'https://api.apiverve.com/v1/numbertowords?number=7' \
-H "x-api-key: $APIVERVE_API_KEY" | grep -i '^x-api'-D - prints the headers, and -o /dev/null throws the body away, so you see just the numbers. On a rate-limited 429, the response also carries a retry-after header: the number of seconds to wait before trying again.
The error sentences differ too ("Rate limit exceeded..." versus "Monthly credit limit reached..."), and they are fine for a human to read. For code, test the header, not the wording.
if (res.status === 429) {
if (Number(res.headers.get("x-api-remaining-credits")) === 0) {
// Out of credits: stop and tell a human. Retrying cannot help.
} else {
// Rate limited: wait retry-after seconds, then repeat the request.
}
}You get a 429 and the response header x-api-remaining-credits is 0. What should your code do?
5xx: not your fault, usually temporary
500 Internal Server Error means something failed on the API's side. 503 Service Unavailable means a source the API depends on did not answer. Your request may be perfectly correct, and the same request a few seconds later often succeeds, so these are worth retrying, with a growing delay between attempts.
If a 5xx keeps happening for the same call, report it rather than working around it. Every response carries a cf-ray header identifying that exact request; include it, and support can find your call instead of guessing.
One handler for every call
Here is the decision table, all in one place:
| Status | Meaning | Retry? |
|---|---|---|
200 | Success | Not needed |
400 | The request is wrong | No. Fix the input the message names |
401 | Key missing or not recognised | No. Fix the key or the header |
403 | Valid key, not allowed this call | No. Check the key's restrictions |
404 | Unknown API, or nothing found for this input | No. Read the message to see which |
429 + credits left | Rate limited | Yes, after retry-after |
429 + 0 credits | Out of credits | No. Stop |
500, 503 | Server-side failure | Yes, with backoff |
Because the envelope is the same for every API, that table fits in one function you can reuse everywhere:
const KEY = process.env.APIVERVE_API_KEY;
if (!KEY) throw new Error("APIVERVE_API_KEY is not set");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function call(api, params = {}, tries = 4) {
const url = new URL(`https://api.apiverve.com/v1/${api}`);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
for (let attempt = 0; attempt < tries; attempt++) {
const res = await fetch(url, { headers: { "x-api-key": KEY } });
const body = await res.json().catch(() => null);
if (res.ok && body?.status === "ok") return body.data;
const outOfCredits = res.status === 429 && Number(res.headers.get("x-api-remaining-credits")) === 0;
const retryable = res.status >= 500 || (res.status === 429 && !outOfCredits);
if (!retryable || attempt === tries - 1) {
const err = new Error(body?.error ?? `HTTP ${res.status}`);
err.status = res.status;
throw err;
}
const wait = Number(res.headers.get("retry-after")) || 2 ** attempt;
await sleep(wait * 1000 + Math.random() * 400);
}
}
// const data = await call("numbertowords", { number: 4021 });import os
import random
import time
import requests
KEY = os.environ["APIVERVE_API_KEY"]
class ApiError(Exception):
def __init__(self, status, message):
super().__init__(message)
self.status = status
def call(api, params=None, tries=4):
for attempt in range(tries):
res = requests.get(
f"https://api.apiverve.com/v1/{api}",
params=params or {},
headers={"x-api-key": KEY},
timeout=15,
)
try:
body = res.json()
except ValueError:
body = None
if res.ok and body and body.get("status") == "ok":
return body["data"]
out_of_credits = res.status_code == 429 and res.headers.get("x-api-remaining-credits") == "0"
retryable = res.status_code >= 500 or (res.status_code == 429 and not out_of_credits)
if not retryable or attempt == tries - 1:
raise ApiError(res.status_code, (body or {}).get("error") or f"HTTP {res.status_code}")
wait = float(res.headers.get("retry-after") or 2 ** attempt)
time.sleep(wait + random.random() * 0.4)
# data = call("numbertowords", {"number": 4021})A few decisions in there are worth understanding rather than copying:
- It checks
body.statusas well as the HTTP status. Some paths, such as batch requests, can answer200overall while individual items failed. Checking both costs nothing. res.json()is wrapped in a catch. If something between you and the API answers with an HTML page instead of JSON, parsing would throw a confusing syntax error. The handler falls back to reporting the HTTP status.- The delay grows each attempt (1, 2, 4 seconds) unless the server says exactly how long to wait with
retry-after. - The random extra milliseconds are deliberate. If ten copies of your program get rate-limited at the same moment and all wait exactly two seconds, they all come back at the same moment and get limited again. A little randomness, called jitter, spreads them out.
- It gives up. Four attempts, then the real error goes to the caller with its status attached. A retry loop without a limit is how a small outage becomes a large bill.
Try it on a failure on purpose: call it with call("numbertoword", { number: 4021 }), one letter short, and you should get an immediate error reading Unknown API 'numbertoword' with status 404, with no retries, because a 404 is not retryable.
curl "https://api.apiverve.com/v1/numbertowords?number=4021" \
-H "x-api-key: YOUR_API_KEY"The success case for the handler above: data.words is four thousand, twenty-one.
A debugging routine that works
When a call fails and the cause is not obvious, go through these in order. Each step either finds the problem or rules out a whole category.
- Read the status code. It tells you whose problem it is: 4xx yours, 5xx theirs, 429 one of two.
- Read the
errorsentence literally. It is written to name the fix. - Re-run the request with
curl -i. If curl works and your code does not, the bug is in how your code builds the request: encoding, a missing header, an unset variable. - Check the credit header if anything looks like a limit.
- Keep the
cf-rayheader if you need to ask for help.
The error handling docs and rate limits docs cover the same ground in reference form when you need to look something up later.
What you built, and what's next
You can read a status code by its family, you know what 400, 401, 403, 404, 429 and 5xx mean on a real API and what to do about each, you can tell a rate limit from an empty credit balance with one header, and you have a reusable handler that retries only what is worth retrying.
That completes API Foundations. You have made real calls with curl and code, read nested JSON safely, handled a key properly, sent parameters both ways, and handled failure. The next course, Calling APIs from Code, turns these pieces into client code you would actually ship, with timeouts, caching and credit budgets. Before moving on, the course certificate exam tests everything from these five lessons with real calls of its own.