All systems normalData updated just nowStatusChangelogSupport
badmintonapi

Rate limits

Each key has two limits. Calls a minute smooth out bursts, so one loop gone wrong cannot crowd out everyone else. Calls a day are the allowance of your plan. Both count per key, never per address, so the same key used from several servers shares one allowance. The day resets at 00:00 UTC.

PlanPriceCalls a minuteCalls a dayEndpoints
Free keyFree10509
Archive$19 a month605,00038
Odds$49 a month12020,00043
Live$149 a month300100,00045

The headers on every answer

Every answer, errors included, says where you stand, so you never have to guess or keep your own count.

HeaderWhat it says
X-RateLimit-LimitCalls allowed a minute on your plan
X-RateLimit-RemainingCalls left this minute
X-RateLimit-ResetWhen the minute resets, as a Unix time
X-Quota-RemainingCalls left today
Retry-AfterSeconds to wait, on a 429 only
Reading the headers with curl
curl -s -D - -o /dev/null https://api.badmintonapi.com/v1/results/yesterday \
  -H "Authorization: Bearer $BADMINTON_API_KEY" | grep -i -E 'ratelimit|quota'

X-RateLimit-Limit: 10
X-RateLimit-Reset: 1790719740
X-RateLimit-Remaining: 9
X-Quota-Remaining: 49

When you reach a limit

You get a 429. Its error.code says which limit: rate_limited means too many calls this minute, and a few seconds' wait is enough; quota_exceeded means today's calls are used, and error.resets_at says when they return. Either way Retry-After gives the seconds to wait. A refused call is never counted, so waiting and trying again costs nothing.

Python: wait when told to
import time, requests

def get(path, **params):
    for attempt in range(5):
        r = requests.get(BASE + path, params=params, headers=AUTH)
        if r.status_code != 429:
            return r.json()
        if r.json()["error"]["code"] == "quota_exceeded":
            raise RuntimeError("today's calls are used")
        time.sleep(int(r.headers.get("Retry-After", 5)))
    raise RuntimeError("still limited after 5 tries")
JavaScript: the same
async function get(path) {
  for (let i = 0; i < 5; i++) {
    const r = await fetch(BASE + path, { headers: AUTH });
    if (r.status !== 429) return r.json();
    const wait = Number(r.headers.get("Retry-After") || 5);
    await new Promise(ok => setTimeout(ok, wait * 1000));
  }
  throw new Error("still limited after 5 tries");
}

Staying well inside them

Most of the archive does not change: a finished draw, a player's past matches or last season's calendar will read the same tomorrow, so keep them and ask again only for what moves. Walk long lists with per_page=200 and the cursor, one call where the default page size would take four. For live scores on the Live plan, use the stream or webhooks rather than calling /v1/live in a tight loop. GET /v1/usage shows today's calls and your limits, and is never counted.

Several servers, one key

Because limits count per key, ten workers sharing one key share one minute's allowance. Either give each worker a slice (a Live key allows 300 calls a minute, so ten workers can make 30 each), or put the calls through one queue that reads X-RateLimit-Remaining and waits when it reaches zero. A burst that runs into the minute limit is not harmful: the refused calls are not counted, and waiting for Retry-After puts you straight back on track.