For AI agents: a documentation index is available at /llms.txt. A markdown version of this page is available at /guide/api/rate-limits.md.
Rate limits
120 requests per minute and 5,000 per day per key, with headers telling you where you stand.
The Developer API enforces two limits at once. Exceeding either returns
429 Too Many Requests.
| Window | Limit |
|---|---|
| Per minute | 120 requests |
| Per day | 5,000 requests |
Both limits are counted per API key. Requests without an X-API-KEY header
are counted per source IP instead, which matters when a misconfigured client
starts failing authentication, because those attempts still consume quota.
#Response headers
Every response carries your current standing, so you can throttle before you get blocked rather than after:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 43
When you are blocked, a third header tells you how long to wait:
Retry-After: 60
The value is in seconds. Honor it rather than guessing. Retrying early just burns the daily allowance.
#Staying under the limits
Batch with filters, not loops
One
tasks/listcall with afiltersobject beats fetching every project and looping. Most list endpoints filter server-side.Cache what does not change
The user directory and project list change rarely. Cache them for minutes, not seconds. This alone usually removes most of the traffic.
Back off on 429
Sleep for
Retry-After, then retry with exponential backoff and jitter so a fleet of workers does not resynchronize into a thundering herd.Use a separate key per integration
Limits are per key, so a nightly bulk sync on its own key cannot starve your interactive integrations.
Generate fresh signing values on every retry
A retry must carry a new X-TIMESTAMP, a new X-NONCE and a recomputed
X-SIGNATURE. Replaying the original headers is rejected as a reused nonce,
and if the wait exceeded five minutes, as an expired timestamp too.
#A worked backoff
async function callWithRetry(path, payload, attempt = 0) {
const response = await call(path, payload); // re-signs on every invocation
if (response.status !== 429 || attempt >= 4) return response;
const retryAfter = Number(response.headers.get('Retry-After') ?? 60);
// Exponential growth plus jitter, so parallel workers don't resynchronize.
const backoff = retryAfter * 1000 * 2 ** attempt + Math.random() * 1000;
await new Promise((resolve) => setTimeout(resolve, backoff));
return callWithRetry(path, payload, attempt + 1);
}
#MCP traffic is limited separately
The MCP server has its own budget: the same 120/minute and 5,000/day figures, but a separate bucket, because it authenticates with OAuth rather than a partner key. Tool calls from Claude, Cursor or Copilot do not eat into your REST allowance.
The MCP bucket is counted per IP
MCP clients do not always present a credential during the initialize handshake, so that limiter keys on the calling IP address rather than on a key or a user. Everyone connecting from the same office NAT or VPN egress shares one bucket. See MCP rate limits.
#Need more headroom?
The limits are configurable per deployment. If a legitimate integration needs more, contact your Orangescrum account manager with the expected request volume and pattern.