Skip to content

Developers

Rate limits

Per-minute burst, monthly quota, the 429 response and how to retry well.

Per-minute limits

Two per-minute ceilings always apply, different for reads and writes: one per token and one per account, shared by all its tokens.

TypePer tokenPer accountWhat counts
Read1,200 per minute2,400 per minuteRoutes whose permission is a read (read).
Write300 per minute600 per minuteRoutes whose permission is create, update, delete or a critical action.

The type is decided by the permission the route requires, not by the HTTP verb. When you create or edit a token you can give it its own ceiling under Limits per minute; the account ceiling still applies. BMC can set an account ceiling other than the default.

The counter is a fixed window per calendar minute: it resets at the start of each minute. A rejected request does not use up quota.

Monthly quota

On top of the per-minute burst, each account has a monthly call quota, 50,000 by default. All tokens of the account share it: creating more tokens does not add quota. BMC can raise it for a given account. When you create a token you can give it an Own monthly quota within the account's. The quota resets on the 1st of each month (UTC). The month's consumption is shown in the client area, under Developers › API & MCP › API usage.

Token, account and user

Limits are hierarchical. A request passes only if it is within all the levels that apply:

  • Token: always applies (1,200 / 300 per minute, unless it has its own ceiling).
  • Account: always applies. All tokens whose primary account it is share 2,400 / 600 per minute, unless BMC set another ceiling on the account. Creating more tokens does not multiply the burst.
  • User: if BMC set a ceiling on the user who created the token, all that user's tokens share it.

The 429 response

When a limit is reached, the API answers 429 with these headers and a body that says which level and window were hit:

HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 1200
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1791195600
X-RateLimit-Window: minute
Content-Type: application/problem+json

{
  "type": "https://bm.consulting/en/developers/errors#rate_limited",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Per-minute limit reached (1200, token). Retry in 23 seconds.",
  "code": "rate_limited",
  "scope": "token",
  "window": "minute",
  "error": "Too Many Requests"
}
HeaderMeaning
Retry-AfterSeconds until the next window opens. Always honour it.
X-RateLimit-LimitThe cap of the window that was hit.
X-RateLimit-RemainingRequests left in that window.
X-RateLimit-ResetUnix time (seconds) when it resets.
X-RateLimit-Windowminute (burst) or month (monthly quota).

scope is token, acct (account) or user.

Good practice

  • Use webhooks instead of polling. If you need to know that an invoice was issued or a contact changed, subscribe to the event and fetch the detail only when it arrives (Webhooks).
  • Respect Retry-After. Wait those seconds before retrying.
  • Exponential backoff. On 429 and 5xx, double the wait on each attempt and cap the number of attempts.
  • Cache what changes rarely (chart of accounts, contacts) and request it again only when needed.
  • Filter at the source with each route's date parameters instead of downloading everything and filtering afterwards.
  • Page with a large size (limit up to the route's maximum) to spend fewer requests (Pagination).
  • Do not fire unbounded parallel requests. Cap your client's concurrency.
TypeScript (fetch)
async function bmcFetch(url: string, init: RequestInit = {}, attempts = 5): Promise<Response> {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(url, {
      ...init,
      headers: { ...init.headers, Authorization: `Bearer ${process.env.BMC_TOKEN}` },
    });
    if (res.status !== 429 && res.status < 500) return res;
    const wait = Number(res.headers.get("Retry-After")) || 2 ** i;
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
  throw new Error("Too many retries");
}
Python (requests)
import os
import time
import requests

def bmc_get(url, params=None, attempts=5):
    for i in range(attempts):
        res = requests.get(
            url,
            params=params,
            headers={"Authorization": f"Bearer {os.environ['BMC_TOKEN']}"},
            timeout=30,
        )
        if res.status_code != 429 and res.status_code < 500:
            return res
        time.sleep(int(res.headers.get("Retry-After", 2 ** i)))
    raise RuntimeError("Too many retries")
Email
Contact