Rate limits
The REST API applies throttling on a per-company basis to a specific set of routes. Throttled routes count each request as one point against a fixed-size 60‑second window (5 minutes for a few IP‑scoped routes). When a company exceeds the limit for a given tier, the API returns 429 Too Many Requests until the window resets.
Throttling is route-scoped, not global: many endpoints have no throttle attached and are not counted against any tier. See Which routes are throttled below.
Throttle tiers
Each throttled route is bound to a tier. The default limit applies to all companies; the increased limit applies only when the account flag increasedAPIRateLimits is enabled or when a custom per-company override is configured.
| Tier | Default limit | Increased limit | Window | Scope |
|---|---|---|---|---|
userAPI | 20 / min | 250 / min | 60 s | company |
eventAPI | 60 / min | 200 / min | 60 s | company |
meetingAPI | 40 / min | 500 / min | 60 s | company |
ssoAPI | 500 / min | 1000 / min | 60 s | company |
redemptionCodes, publicCertificate | 30 / 5 min | — | 300 s | request IP |
certificateShareToken | 10 / 5 min | — | 300 s | request IP |
There is no "Standard" vs. "Enterprise" plan tiering, no burst allowance, and no cross‑endpoint quota. Each tier maintains its own independent counter.
A separate
heliumAPItier (2500 / min, per-company) governs the GraphQL/heliumsurface, not the REST API documented here.
Which routes are throttled
Only routes that opt in through the throttle middleware are counted. The current attachments include:
userAPI— user create/update/delete endpoints (for example,POST /v2/users,PUT /v2/users).eventAPI— events ingest endpoints (for example,GET/POSTon/v2/events).meetingAPI— meeting endpoints.ssoAPI— SSO endpoints.redemptionCodes,publicCertificate,certificateShareToken— IP‑scoped public routes.
Routes without a throttle attached are not rate-limited by this middleware. Notable examples include POST /v2/content/course/create, PUT /v2/content/course/update, webinar and in‑person bulk imports, GET /v1/users, and most GET endpoints. These may still be subject to platform-level protections such as edge WAF or connection limits.
Attachment is per-route in the API code; the exact set of throttled endpoints can change between releases. Treat throttling as an infrastructure guarantee for the tiers above rather than as an endpoint contract.
Rate limit headers
Rate limit headers are set only on throttled routes (routes without a throttle attached do not emit them).
On a successful response from a throttled route, all three headers are returned:
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 17
X-RateLimit-Reset: 1716505260
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per window for the tier bound to this route |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets |
On a rejected 429 response, only X-RateLimit-Reset and Retry-After are set — X-RateLimit-Limit and X-RateLimit-Remaining are not included on the fallback path.
Handling rate limit errors
When the limit is exceeded on a throttled route, the API returns 429 Too Many Requests with a Retry-After header. The response body is a plain error envelope:
Response: 429 Too Many Requests
{
"errors": ["Rate limit exceeded, retry in 32 seconds"]
}Retry-After is the authoritative signal for how long to wait; clients should read it rather than parsing the message string.
Retry strategy
Read Retry-After and back off before retrying. Add jitter to avoid retry storms across parallel clients.
# Read Retry-After from the response and sleep before retrying
RETRY_AFTER=$(curl -s -o /dev/null -D - -w "%header{retry-after}" \
-H "Authorization: Bearer ti_live_a1b2c3d4e5f6g7h8i9j0" \
"https://api.thoughtindustries.com/incoming/v2/users")
sleep "${RETRY_AFTER:-5}"
# 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;
// Prefer Retry-After; fall back to a small default.
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
# Prefer Retry-After; fall back to a small default.
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;
// In production, parse Retry-After from the response headers.
$retryAfter = 5;
usleep((int) (($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 and, where possible, across tiers.
- Monitor
X-RateLimit-Remainingon throttled routes and back off before it reaches zero. - Contact support if your integration consistently needs the
increasedAPIRateLimitstier or a custom override.
Related
- Authentication — API key setup and usage
- Status codes — full error code reference
- Pagination — efficient data retrieval