Skip to main content
https://.thoughtindustries.com

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

PlanRequests per minuteBurst allowance
Standard12020
Enterprise600100

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 categoryLimitWindow
GET events100per minute
POST/PUT users60per minute
Course management60per minute
Reporting30per 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
HeaderDescription
X-RateLimit-LimitMaximum requests allowed per window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix 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 request
async 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-Remaining proactively before hitting zero
  • Contact support if your integration consistently requires higher limits