Skip to main content

Rate Limits

All One Stack APIs enforce rate limits to ensure fair usage and platform stability.

Limits

ScopeLimitWindow
Per tenant100 requests1 minute

Rate limits apply across all API keys and sessions for a given tenant. For example, if two different API keys belong to the same tenant, their requests count toward the same 100 requests/minute budget.

Rate Limit Response

When you exceed the rate limit, the API returns a 429 Too Many Requests response:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 23

{
"error": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED",
"details": {
"limit": 100,
"window": "1m",
"retryAfter": 23
}
}

The Retry-After header contains the number of seconds to wait before making another request.

Rate Limit Headers

Every API response includes headers to help you track your remaining budget:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed per window (100)
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the current window resets

Best Practices

Use Batch Operations

Instead of making individual requests for each item, use batch endpoints where available:

# Instead of this (5 requests):
curl -X GET /api/tickets/t1
curl -X GET /api/tickets/t2
curl -X GET /api/tickets/t3
curl -X GET /api/tickets/t4
curl -X GET /api/tickets/t5

# Do this (1 request):
curl -X POST /api/tickets/batch \
-d '{"ids": ["t1", "t2", "t3", "t4", "t5"]}'

Use Webhooks Instead of Polling

Instead of polling for changes, configure webhooks to receive real-time notifications when data changes:

# Bad: Polling every 10 seconds
while true; do
curl /api/tickets?updatedAfter=2026-03-09T00:00:00Z
sleep 10
done

# Good: Register a webhook for ticket.updated events
# and let the platform push changes to you

Implement Exponential Backoff

When you receive a 429 response, wait for the duration specified in Retry-After before retrying. For general resilience, implement exponential backoff:

async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);

if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '5', 10);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}

return response;
}
throw new Error('Max retries exceeded');
}

Cache Responses

For data that does not change frequently (e.g., service catalog items, board configurations, chart of accounts), cache responses locally and use conditional requests:

curl -X GET /api/service-catalog \
-H "If-None-Match: \"etag-abc123\""

If the data has not changed, the API returns 304 Not Modified with no body, which does not count against your rate limit.

Need Higher Limits?

If your integration requires higher throughput, contact support at [email protected] to discuss custom rate limit arrangements.