Skip to content

Rate limits

Every call you make with an OAuth access token is limited per app, not per IP — so your limit doesn’t shrink because you share egress infrastructure with other traffic, and it doesn’t grow if you spread calls across many IPs either:

ScopeLimit
Per app, per business300 requests / minute
Per app, across all businesses10,000 requests / minute

Both are enforced together — whichever is hit first returns 429 with code: "APP_RATE_LIMITED" and a Retry-After header (seconds to wait).

Every request your token sends counts, including ones that are then refused: the limit is checked before scopes and permissions, so a call rejected with 403 for a missing scope or permission still uses up budget. Don’t let a misconfigured call loop on its 403.

Responses to OAuth-authenticated calls also carry:

HeaderMeaning
RateLimit-LimitThe per-app, per-business limit — these headers always describe that budget, never the across-all-businesses one
RateLimit-RemainingRequests left in the current window for this business
RateLimit-ResetSeconds until that window resets
Retry-AfterPresent on 429 responses — seconds to wait, from whichever limit refused the request

The across-all-businesses limit has no headers of its own: you only notice it when it refuses a request, through the 429 and its Retry-After. A rejected request still shows in your app’s analytics — as a call, as an error and as rate-limited.

The endpoints you use to obtain or manage a token aren’t called with one, so they’re limited by IP address and client — one misbehaving app behind a shared egress IP can’t use up, or lock out, the others’ budget. POST /oauth/token, POST /oauth/introspect and POST /oauth/revoke each keep their own counters:

LimitCounted perDefault
Requests that present a client secret (HTTP Basic, or client_secret in the body)IP + client_id600 / minute
Requests without a secret — a PUBLIC client’s code exchange, refresh, device-code polling and token revocation (MCP connectors, native and device apps)IP + client_id300 / minute
Everything one IP sends to that endpoint, all clients togetherIP1,200 / minute
POST /oauth/token once a client has proven its secret — every grant, including each token exchangeclient, across all the IPs it calls from600 / minute
POST /oauth/device/authorizeIP30 / minute

Any secret a request presents must be correct, whatever the client type: a wrong one is 401 invalid_client and counts as a failed client authentication. A public client that sends no secret is unaffected. /oauth/introspect reads client credentials from the Authorization header only. POST /oauth/device/authorize authenticates the client exactly as /oauth/token does: a CONFIDENTIAL client must send its secret — with HTTP Basic or as client_secret in the body — and a PUBLIC client sends its client_id alone.

Failed client authentications lock the caller out. More than 30 failed client authentications (any 401: an unknown or suspended client, a missing or wrong secret) in one minute from the same IP for the same client_id get that IP-and-client pair 429 for the next 60 seconds; more than 100 from one IP across all client IDs lock the whole IP out for 60 seconds. The lockout is checked before any client is looked up. Token, introspection, revocation and device authorization each count their own failures and keep their own lockout, so failing on one never locks a client out of another — a burst of failed device authorizations never costs you token refreshes. A backend deployed with a stale secret locks itself out this way — fix the secret rather than retrying.

Every 429 from these endpoints carries Retry-After and, like every OAuth endpoint error, a standard OAuth error body rather than the APP_RATE_LIMITED one above:

{
"error": "temporarily_unavailable",
"error_description": "Too many requests - retry after 42 seconds"
}

Branch on the status or on error, and take the wait from Retry-After — the description is prose for your logs.

Calls made with a OneBooks session cookie rather than an OAuth token (the merchant web app itself, and a few console/session-only routes) fall back to the general per-IP default of 600 requests/minute. Partner integrations don’t normally hit this — it’s listed here only for completeness.

Exceeding any limit returns 429 Too Many Requests with a Retry-After header (seconds until the window frees up).

  • Honor Retry-After when present; otherwise back off exponentially with jitter (e.g. 1 s, 2 s, 4 s… ±25%). The Node SDK does this for you.
  • Read RateLimit-Limit rather than hard-coding 300, and pace a busy business’s traffic with RateLimit-Remaining.
  • Don’t tight-loop the token endpoint. Its budget is modest — 600 a minute for a confidential client across all your servers, 300 a minute per IP for a public one. Refresh once per expiry per token set — single-flight, never per request, and cache the access token for its full hour.
  • Exchange session tokens once, not per request. An embedded app’s backend should exchange a session token the first time it sees a business and user, then reuse the stored tokens — a per-request exchange both fails (a session token is single-use) and burns token-endpoint budget.
  • Batch instead of spraying. POST /invoices/bulk-import takes up to 200 invoices per request — one call instead of 200. App data writes take up to 25 values per PUT /app-data. See integration patterns.
  • Spread scheduled work. If you sync many businesses on a cron, add jitter to start times so they don’t all hit the API in the same minute.
  • Use webhooks, not polling. Most “did anything change?” polling disappears once you subscribe to webhooks.

The per-app API limits above key on your app, not your IP — so if your platform makes calls for many businesses from a single egress IP (NAT gateway, serverless egress), that’s no longer a shared budget for ordinary API calls. The same goes for the OAuth endpoints: token, introspection and revocation requests are counted per IP and client, and a confidential client’s token requests also against its own 600-a-minute budget. What stays purely per-IP is device authorization (30 a minute) and the 1,200-a-minute ceiling on everything one IP sends to each OAuth endpoint — so a fleet of devices or workers behind one NAT gateway should still pace those.