Rate Limits
All One Stack APIs enforce rate limits to ensure fair usage and platform stability.
Limits
| Scope | Limit | Window |
|---|---|---|
| Per tenant | 100 requests | 1 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:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per window (100) |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix 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.