> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firstquadrant.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limiting

> Understanding and working with API rate limits

The FirstQuadrant API implements rate limiting to ensure fair usage and maintain service reliability for all users. This guide explains our rate limits and how to handle them gracefully.

## Current rate limits

### Authentication endpoints

Authentication endpoints have stricter rate limits for security:

| Endpoint           | Limit       | Window   |
| ------------------ | ----------- | -------- |
| `/v5/auth`         | 30 requests | 1 minute |
| `/v5/auth/refresh` | 30 requests | 1 minute |
| `/v5/auth/login`   | 30 requests | 1 minute |

### Standard API endpoints

Currently, standard API endpoints do not have publicly-available rate limits. We recommend implementing rate limit handling in your code to ensure compatibility with future updates.

## Rate limit response

When you exceed the rate limit, you'll receive a `429 Too Many Requests` response:

```json theme={null}
{
  "code": "rate_limited",
  "status": 429,
  "message": "Too many requests",
  "description": "You have exceeded the rate limit. Please wait before making more requests."
}
```

## Handling rate limits

### Exponential backoff

The recommended approach is to implement exponential backoff with jitter:

<CodeGroup>
  ```javascript JavaScript theme={null}
  async function makeRequestWithRetry(url, options, maxRetries = 3) {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        const response = await fetch(url, options);

        if (response.status === 429) {
          if (attempt === maxRetries) {
            throw new Error("Rate limit exceeded after maximum retries");
          }

          // Exponential backoff with jitter
          const baseDelay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
          const jitter = Math.random() * 1000; // 0-1s random jitter
          const delay = baseDelay + jitter;

          console.log(`Rate limited. Retrying in ${delay}ms...`);
          await new Promise((resolve) => setTimeout(resolve, delay));
          continue;
        }

        return response;
      } catch (error) {
        if (attempt === maxRetries) throw error;
      }
    }
  }

  // Usage
  const response = await makeRequestWithRetry("https://api.us.firstquadrant.ai/v5/contacts", {
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
      "FirstQuadrant-Organization-ID": "org_YOUR_ORG_ID",
    },
  });
  ```

  ```python Python theme={null}
  import time
  import random
  import requests
  from typing import Optional, Dict, Any

  def make_request_with_retry(
      url: str,
      headers: Dict[str, str],
      method: str = 'GET',
      data: Optional[Any] = None,
      max_retries: int = 3
  ) -> requests.Response:
      """Make an API request with exponential backoff retry logic."""

      for attempt in range(max_retries + 1):
          try:
              response = requests.request(
                  method=method,
                  url=url,
                  headers=headers,
                  json=data
              )

              if response.status_code == 429:
                  if attempt == max_retries:
                      response.raise_for_status()

                  # Exponential backoff with jitter
                  base_delay = (2 ** attempt)  # 1s, 2s, 4s
                  jitter = random.random()  # 0-1s random
                  delay = base_delay + jitter

                  print(f"Rate limited. Retrying in {delay:.2f} seconds...")
                  time.sleep(delay)
                  continue

              response.raise_for_status()
              return response

          except requests.exceptions.RequestException as e:
              if attempt == max_retries:
                  raise

              # Network errors also trigger retry
              delay = (2 ** attempt) + random.random()
              print(f"Request failed: {e}. Retrying in {delay:.2f} seconds...")
              time.sleep(delay)

  # Usage
  response = make_request_with_retry(
      url='https://api.us.firstquadrant.ai/v5/contacts',
      headers={
          'Authorization': 'Bearer YOUR_API_KEY',
          'FirstQuadrant-Organization-ID': 'org_YOUR_ORG_ID'
      }
  )
  ```

  ```typescript TypeScript theme={null}
  interface RetryOptions {
    maxRetries?: number;
    initialDelay?: number;
    maxDelay?: number;
  }

  class RateLimitError extends Error {
    constructor(public retryAfter?: number) {
      super("Rate limit exceeded");
      this.name = "RateLimitError";
    }
  }

  async function fetchWithRetry(url: string, options: RequestInit, retryOptions: RetryOptions = {}): Promise<Response> {
    const { maxRetries = 3, initialDelay = 1000, maxDelay = 32000 } = retryOptions;

    let lastError: Error | null = null;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        const response = await fetch(url, options);

        if (response.status === 429) {
          if (attempt === maxRetries) {
            throw new RateLimitError();
          }

          // Calculate delay with exponential backoff and jitter
          const baseDelay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay);
          const jitter = Math.random() * 1000;
          const delay = baseDelay + jitter;

          console.log(`Rate limited. Retry ${attempt + 1}/${maxRetries} in ${Math.round(delay)}ms`);
          await new Promise((resolve) => setTimeout(resolve, delay));
          continue;
        }

        return response;
      } catch (error) {
        lastError = error as Error;

        if (attempt < maxRetries) {
          const delay = initialDelay * Math.pow(2, attempt);
          await new Promise((resolve) => setTimeout(resolve, delay));
        }
      }
    }

    throw lastError || new Error("Request failed after retries");
  }

  // Usage with a rate limit handler
  async function apiRequest<T>(endpoint: string, options?: RequestInit): Promise<T> {
    const response = await fetchWithRetry(`https://api.us.firstquadrant.ai/v5${endpoint}`, {
      ...options,
      headers: {
        Authorization: "Bearer YOUR_API_KEY",
        "FirstQuadrant-Organization-ID": "org_YOUR_ORG_ID",
        "Content-Type": "application/json",
        ...options?.headers,
      },
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message);
    }

    return response.json();
  }
  ```
</CodeGroup>

## Best practices

### 1. Implement retry logic

Always implement retry logic with exponential backoff:

```javascript theme={null}
// Good: Exponential backoff with jitter
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;

// Bad: Fixed delay
const delay = 1000;

// Bad: No retry logic
if (response.status === 429) throw new Error("Rate limited");
```

### 2. Queue requests

For high-volume applications, implement a request queue:

```javascript theme={null}
class RequestQueue {
  constructor(maxConcurrent = 5, minDelay = 100) {
    this.queue = [];
    this.active = 0;
    this.maxConcurrent = maxConcurrent;
    this.minDelay = minDelay;
    this.lastRequestTime = 0;
  }

  async add(requestFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ requestFn, resolve, reject });
      this.process();
    });
  }

  async process() {
    if (this.active >= this.maxConcurrent || this.queue.length === 0) {
      return;
    }

    // Ensure minimum delay between requests
    const now = Date.now();
    const timeSinceLastRequest = now - this.lastRequestTime;

    if (timeSinceLastRequest < this.minDelay) {
      setTimeout(() => this.process(), this.minDelay - timeSinceLastRequest);
      return;
    }

    const { requestFn, resolve, reject } = this.queue.shift();
    this.active++;
    this.lastRequestTime = Date.now();

    try {
      const result = await requestFn();
      resolve(result);
    } catch (error) {
      reject(error);
    } finally {
      this.active--;
      this.process();
    }
  }
}

// Usage
const queue = new RequestQueue(5, 200); // Max 5 concurrent, 200ms between requests

async function fetchAllContacts() {
  const pagePromises = [];

  for (let page = 1; page <= 10; page++) {
    pagePromises.push(queue.add(() => fetch(`/v5/contacts?page=${page}`, { headers })));
  }

  return Promise.all(pagePromises);
}
```

### 3. Monitor rate limit usage

Track your API usage to avoid hitting limits:

```javascript theme={null}
class RateLimitMonitor {
  constructor(limit, window) {
    this.limit = limit;
    this.window = window;
    this.requests = [];
  }

  canMakeRequest() {
    const now = Date.now();
    const windowStart = now - this.window;

    // Remove old requests outside the window
    this.requests = this.requests.filter((time) => time > windowStart);

    return this.requests.length < this.limit;
  }

  recordRequest() {
    this.requests.push(Date.now());
  }

  async waitForSlot() {
    if (this.canMakeRequest()) return;

    const oldestRequest = this.requests[0];
    const waitTime = this.window - (Date.now() - oldestRequest) + 100;

    console.log(`Rate limit approaching. Waiting ${waitTime}ms...`);
    await new Promise((resolve) => setTimeout(resolve, waitTime));
  }
}

// Usage for auth endpoints (30 req/min)
const authLimiter = new RateLimitMonitor(30, 60000);

async function authenticate() {
  await authLimiter.waitForSlot();

  const response = await fetch("/v5/auth", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ token: refreshToken }),
  });

  authLimiter.recordRequest();

  return response;
}
```

### 4. Batch operations

Reduce API calls by batching operations where possible:

```javascript theme={null}
// Instead of individual requests
for (const contact of contacts) {
  await updateContact(contact.id, contact.data); // ❌ Many requests
}

// Batch updates in groups
const batchSize = 50;
for (let i = 0; i < contacts.length; i += batchSize) {
  const batch = contacts.slice(i, i + batchSize);
  // Process batch together
  await processBatch(batch); // ✅ Fewer requests
}
```

### 5. Cache responses

Implement caching to reduce redundant API calls:

```javascript theme={null}
class APICache {
  constructor(ttl = 300000) {
    // 5 minutes default
    this.cache = new Map();
    this.ttl = ttl;
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;

    if (Date.now() > item.expiry) {
      this.cache.delete(key);
      return null;
    }

    return item.value;
  }

  set(key, value) {
    this.cache.set(key, {
      value,
      expiry: Date.now() + this.ttl,
    });
  }

  async fetch(key, fetchFn) {
    const cached = this.get(key);
    if (cached) return cached;

    const value = await fetchFn();
    this.set(key, value);
    return value;
  }
}

// Usage
const cache = new APICache();

async function getContact(id) {
  return cache.fetch(`contact:${id}`, async () => {
    const response = await fetch(`/v5/contacts/${id}`, { headers });
    return response.json();
  });
}
```

## Error recovery strategies

### Circuit breaker pattern

Implement a circuit breaker to prevent cascading failures:

```javascript theme={null}
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failureCount = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN
    this.nextAttempt = Date.now();
  }

  async execute(requestFn) {
    if (this.state === "OPEN") {
      if (Date.now() < this.nextAttempt) {
        throw new Error("Circuit breaker is OPEN");
      }
      this.state = "HALF_OPEN";
    }

    try {
      const result = await requestFn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    this.state = "CLOSED";
  }

  onFailure() {
    this.failureCount++;

    if (this.failureCount >= this.threshold) {
      this.state = "OPEN";
      this.nextAttempt = Date.now() + this.timeout;
      console.log(`Circuit breaker opened. Retry after ${new Date(this.nextAttempt)}`);
    }
  }
}

// Usage
const breaker = new CircuitBreaker();

async function makeAPICall() {
  return breaker.execute(async () => {
    const response = await fetch("/v5/contacts", { headers });

    if (response.status === 429) {
      throw new Error("Rate limited");
    }

    return response.json();
  });
}
```

## Testing rate limits

When developing, test your rate limit handling:

```javascript theme={null}
// Simulate rate limit scenarios
async function testRateLimitHandling() {
  const requests = [];

  // Make rapid requests to trigger rate limit
  for (let i = 0; i < 40; i++) {
    requests.push(
      makeRequestWithRetry("/v5/auth", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ test: true }),
      }),
    );
  }

  try {
    const results = await Promise.allSettled(requests);

    const successful = results.filter((r) => r.status === "fulfilled").length;
    const failed = results.filter((r) => r.status === "rejected").length;

    console.log(`Successful: ${successful}, Failed: ${failed}`);
  } catch (error) {
    console.error("Test failed:", error);
  }
}
```

## Future considerations

While most endpoints currently don't have publicly-available rate limits, this may change. Design your integration to:

1. **Handle 429 responses** gracefully even on endpoints without current limits
2. **Monitor response headers** for future rate limit information
3. **Implement configurable delays** between requests
4. **Use pagination** to reduce the number of requests
5. **Cache data** where appropriate

By following these practices, your integration will continue to work smoothly as the API evolves.
