Errors
A failure always has the same shape: a status code that says what kind of failure it is, and an error object with a code word that never changes, a message written for a person, and a docs link. Match on error.code in your code and show error.message to people; the message may be reworded, the code will not be.
| Status | Code | What it means | What to do |
|---|---|---|---|
| 400 | bad_request | A value was the wrong shape (a date that is not YYYY-MM-DD, a word where a number goes), or the key was sent in the address. | Read the message: it names the parameter and what it expects. |
| 401 | unauthorized | No key, or a key that has been replaced. | Send it as a header; check it in your account. |
| 403 | upgrade_required | The endpoint comes with a higher plan. | error.plan names it; upgrade and the same key works. |
| 404 | not_found | No such endpoint, or nothing with that id. | Check the path; find ids with /v1/search. |
| 422 | unknown_parameter | A parameter the endpoint does not take, often a typo. | error.allowed lists those it does. |
| 429 | rate_limited | Too many calls this minute. | Wait for Retry-After, usually seconds. |
| 429 | quota_exceeded | Today's calls are used. | They return at error.resets_at (00:00 UTC). |
| 500 | server_error | Our fault, not yours. | Try again shortly. Every one is logged and looked at. |
What the extras tell you
Some errors carry more than the three usual fields. An upgrade carries plan, the plan that has the endpoint, and upgrade, the pricing page. An unknown parameter carries allowed, every parameter the endpoint does take, which turns a silent typo into an answer you can fix in seconds. A used-up day carries resets_at.
{
"error": {
"code": "upgrade_required",
"message": "This endpoint comes with the Archive plan. Your key is on Free key.",
"plan": "Archive",
"upgrade": "https://badmintonapi.com/pricing",
"docs": "https://badmintonapi.com/documentation"
}
}Handling them in one place
const r = await fetch(url, { headers });
const body = await r.json();
if (!r.ok) {
switch (body.error.code) {
case "rate_limited":
// wait for Retry-After, then call again
break;
case "upgrade_required":
// body.error.plan names the plan that has it
break;
case "not_found":
return null;
default:
throw new Error(body.error.message);
}
}class ApiError(Exception):
def __init__(self, status, error):
super().__init__(error["message"])
self.status, self.code = status, error["code"]
self.extra = error
def call(path, **params):
r = requests.get(BASE + path, params=params, headers=AUTH)
body = r.json()
if "error" in body:
raise ApiError(r.status_code, body["error"])
return bodyWhich errors to try again
Only two are worth trying again without a change: 429, after the wait in Retry-After, and 500, after a short pause. Everything in the 400s means the call itself needs changing, and sending it again unchanged will give the same answer. A refused call is never counted against your limits.