Rate Limiting
The OnboardingHub API enforces rate limits to ensure fair usage and protect against abuse.
Limits
All authenticated API v1 endpoints and the MCP endpoint share these limits:
| Window | Limit |
|---|---|
| Per minute | 60 requests per token |
| Per hour | 500 requests per token |
Both windows are tracked independently. Hitting either limit returns a 429 Too Many Requests response.
Rate limits are enforced per access token -- if you have multiple API keys or OAuth tokens, each has its own limits.
Response headers
Every API response includes rate limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests allowed in the current window |
X-RateLimit-Remaining |
Requests remaining before the limit is reached |
X-RateLimit-Reset |
Unix timestamp when the current window resets |
Example headers on a successful response:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1707436860
Handling 429 responses
When you exceed the rate limit, the API returns:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
"error": {
"type": "rate_limited",
"message": "Too many requests. Please retry later."
}
}
The Retry-After header tells you how many seconds to wait before retrying.
Implementing retry logic
Ruby:
def api_request(uri, headers, max_retries: 3)
retries = 0
loop do
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(Net::HTTP::Get.new(uri, headers))
end
return JSON.parse(response.body) unless response.code.to_i == 429
retries += 1
raise "Rate limited after #{max_retries} retries" if retries > max_retries
wait = response["Retry-After"]&.to_i || 60
sleep(wait)
end
end
Python:
import requests
import time
def api_request(url, headers, max_retries=3):
for attempt in range(max_retries + 1):
response = requests.get(url, headers=headers)
if response.status_code != 429:
return response.json()
if attempt == max_retries:
raise Exception("Rate limited after max retries")
wait = int(response.headers.get("Retry-After", 60))
time.sleep(wait)
JavaScript:
async function apiRequest(url, headers, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, { headers });
if (response.status !== 429) {
return response.json();
}
if (attempt === maxRetries) {
throw new Error("Rate limited after max retries");
}
const wait = parseInt(response.headers.get("Retry-After") || "60");
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
}
}
Best practices
- Monitor rate limit headers -- check
X-RateLimit-Remainingproactively to avoid hitting limits - Implement exponential backoff -- if retrying after a 429, wait progressively longer on each retry
- Batch operations -- use bulk endpoints or reduce call frequency where possible
- Cache responses -- avoid unnecessary requests for data that does not change frequently
- Use webhooks -- instead of polling for changes, subscribe to webhook events
IP-level blocking
Clients that repeatedly exceed rate limits (10+ throttle hits within 1 minute) are temporarily blocked for 1 hour. During a block, all requests from that IP return:
HTTP/1.1 403 Forbidden
{
"error": "Access denied. Your IP has been temporarily blocked due to excessive requests."
}
Next steps
- Error handling -- understanding all error types
- Webhooks -- use webhooks instead of polling