All systems normalData updated just nowStatusChangelogSupport
badmintonapi

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.

StatusCodeWhat it meansWhat to do
400bad_requestA 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.
401unauthorizedNo key, or a key that has been replaced.Send it as a header; check it in your account.
403upgrade_requiredThe endpoint comes with a higher plan.error.plan names it; upgrade and the same key works.
404not_foundNo such endpoint, or nothing with that id.Check the path; find ids with /v1/search.
422unknown_parameterA parameter the endpoint does not take, often a typo.error.allowed lists those it does.
429rate_limitedToo many calls this minute.Wait for Retry-After, usually seconds.
429quota_exceededToday's calls are used.They return at error.resets_at (00:00 UTC).
500server_errorOur 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.

403: a Free key asking for an Archive endpoint
{
  "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

JavaScript
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);
  }
}
Python
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 body

Which 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.