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

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.

TierDefault limitIncreased limitWindowScope
userAPI20 / min250 / min60 scompany
eventAPI60 / min200 / min60 scompany
meetingAPI40 / min500 / min60 scompany
ssoAPI500 / min1000 / min60 scompany
redemptionCodes, publicCertificate30 / 5 min300 srequest IP
certificateShareToken10 / 5 min300 srequest 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 heliumAPI tier (2500 / min, per-company) governs the GraphQL /helium surface, 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/POST on /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
HeaderDescription
X-RateLimit-LimitMaximum requests allowed per window for the tier bound to this route
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix 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 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;

    // 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-Remaining on throttled routes and back off before it reaches zero.
  • Contact support if your integration consistently needs the increasedAPIRateLimits tier or a custom override.