GuidesRate Limiting

Rate Limiting

The API enforces rate limits to ensure fair usage and system stability.

Limits

TierBurst (per minute)Sustained (per hour)
Free tier ($2.50 credits)60 requests600 requests
Standard100 requests1,000 requests
EnterpriseCustomCustom

Rate limits apply per API key. Different keys for the same organization have independent limits.

Response Headers

Every API response includes rate limit headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1711036800
Retry-After: 30
HeaderDescription
X-RateLimit-LimitMaximum requests allowed per window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds to wait before retrying (only on 429)

429 Response

When you exceed the rate limit:

{
  "error": {
    "type": "rate_limit_error",
    "message": "Too many requests. Please retry after 30 seconds.",
    "request_id": "req_01HV..."
  }
}

Exponential Backoff

When you receive a 429, use exponential backoff with jitter:

async function fetchWithBackoff(url: string, options: RequestInit, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);
 
    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get('Retry-After') ?? '1', 10);
      const delay = retryAfter * 1000 + Math.random() * 1000; // Add jitter
      await new Promise(resolve => setTimeout(resolve, delay));
      continue;
    }
 
    return response;
  }
  throw new Error('Max retries exceeded');
}
# cURL: check rate limit headers
curl -i https://api.firsthandapi.com/v1/jobs \
  -H "Authorization: Bearer fh_live_..." \
  2>&1 | grep -i "ratelimit\|retry"

Best Practices

  • Check X-RateLimit-Remaining before making requests in tight loops
  • Always honor Retry-After — don’t retry sooner than the server says
  • Add jitter to backoff delays to avoid thundering herd on recovery
  • Cache responses when possible — GET /v1/jobs/:id results rarely change between polls
  • Use webhooks instead of polling — job.completed fires when all files are collected, eliminating the need to poll
  • Use long-pollingGET /v1/jobs/:id with Prefer: wait=30 blocks until state changes, reducing request count