Rate limits
The REST API enforces rate limits to ensure fair usage and platform stability. Every API key is subject to a per-minute request quota. When you exceed the quota, the API returns a 429 Too Many Requests response until the window resets.
Default limits
| Plan | Requests per minute | Burst allowance |
|---|---|---|
| Standard | 120 | 20 |
| Enterprise | 600 | 100 |
Burst allowance permits short spikes above the base rate. Once the burst is exhausted, requests are throttled to the base rate until the window resets.
Limits by endpoint category
Individual endpoint categories have specific limits within the plan quotas above:
| Endpoint category | Limit | Window |
|---|---|---|
| GET events | 100 | per minute |
| POST/PUT users | 60 | per minute |
| Course management | 60 | per minute |
| Reporting | 30 | per minute |
Rate limit headers
Every response includes headers that report your current quota status:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1716505260
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Handling rate limit errors
When you exceed the limit, the API returns a 429 response with a Retry-After header:
Response: 429 Too Many Requests
{
"error": "rate_limit_exceeded",
"message": "Request rate limit reached. Retry after 32 seconds.",
"retryAfter": 32
}Retry strategy
Implement exponential backoff with jitter to handle rate-limited responses gracefully:
# Check the Retry-After header and wait before retrying
RETRY_AFTER=$(curl -s -o /dev/null -w "%header{retry-after}" \
-H "Authorization: Bearer ti_live_a1b2c3d4e5f6g7h8i9j0" \
"https://api.thoughtindustries.com/incoming/v2/users")
sleep "$RETRY_AFTER"
# Retry the requestasync function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) return response;
const retryAfter = parseInt(response.headers.get("Retry-After") || "5", 10);
const jitter = Math.random() * 1000;
await new Promise(r => setTimeout(r, retryAfter * 1000 + jitter));
}
throw new Error("Rate limit exceeded after max retries");
}import time
import random
import requests
def fetch_with_retry(url, headers, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code != 429:
return response
retry_after = int(response.headers.get("Retry-After", 5))
jitter = random.uniform(0, 1)
time.sleep(retry_after + jitter)
raise Exception("Rate limit exceeded after max retries")function fetchWithRetry(string $url, array $headers, int $maxRetries = 3): string {
for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode !== 429) return $response;
$retryAfter = 5; // Parse from headers in production
usleep(($retryAfter + mt_rand(0, 1000) / 1000) * 1000000);
}
throw new \Exception("Rate limit exceeded after max retries");
}Best practices
- Cache responses where possible to reduce request volume
- Use pagination with reasonable page sizes
- Spread bulk operations across multiple windows
- Monitor
X-RateLimit-Remainingproactively before hitting zero - Contact support if your integration consistently requires higher limits
Related
- Authentication — API key setup and usage
- Status codes — full error code reference
- Pagination — efficient data retrieval