Rate Limiting

The OnboardingHub API enforces rate limits to ensure fair usage and protect against abuse.

Limits

All authenticated API v1 endpoints and the MCP endpoint share these limits:

Window Limit
Per minute 60 requests per token
Per hour 500 requests per token

Both windows are tracked independently. Hitting either limit returns a 429 Too Many Requests response.

Rate limits are enforced per access token -- if you have multiple API keys or OAuth tokens, each has its own limits.

Response headers

Every API response includes rate limit headers:

Header Description
X-RateLimit-Limit Maximum requests allowed in the current window
X-RateLimit-Remaining Requests remaining before the limit is reached
X-RateLimit-Reset Unix timestamp when the current window resets

Example headers on a successful response:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1707436860

Handling 429 responses

When you exceed the rate limit, the API returns:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
  "error": {
    "type": "rate_limited",
    "message": "Too many requests. Please retry later."
  }
}

The Retry-After header tells you how many seconds to wait before retrying.

Implementing retry logic

Ruby:

def api_request(uri, headers, max_retries: 3)
  retries = 0
  loop do
    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
      http.request(Net::HTTP::Get.new(uri, headers))
    end

    return JSON.parse(response.body) unless response.code.to_i == 429

    retries += 1
    raise "Rate limited after #{max_retries} retries" if retries > max_retries

    wait = response["Retry-After"]&.to_i || 60
    sleep(wait)
  end
end

Python:

import requests
import time

def api_request(url, headers, max_retries=3):
    for attempt in range(max_retries + 1):
        response = requests.get(url, headers=headers)

        if response.status_code != 429:
            return response.json()

        if attempt == max_retries:
            raise Exception("Rate limited after max retries")

        wait = int(response.headers.get("Retry-After", 60))
        time.sleep(wait)

JavaScript:

async function apiRequest(url, headers, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, { headers });

    if (response.status !== 429) {
      return response.json();
    }

    if (attempt === maxRetries) {
      throw new Error("Rate limited after max retries");
    }

    const wait = parseInt(response.headers.get("Retry-After") || "60");
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
  }
}

Best practices

  • Monitor rate limit headers -- check X-RateLimit-Remaining proactively to avoid hitting limits
  • Implement exponential backoff -- if retrying after a 429, wait progressively longer on each retry
  • Batch operations -- use bulk endpoints or reduce call frequency where possible
  • Cache responses -- avoid unnecessary requests for data that does not change frequently
  • Use webhooks -- instead of polling for changes, subscribe to webhook events

IP-level blocking

Clients that repeatedly exceed rate limits (10+ throttle hits within 1 minute) are temporarily blocked for 1 hour. During a block, all requests from that IP return:

HTTP/1.1 403 Forbidden
{
  "error": "Access denied. Your IP has been temporarily blocked due to excessive requests."
}

Next steps