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 a429 Too Many Requests response:
{
"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: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",
},
});
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'
}
)
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();
}
Best practices
1. Implement retry logic
Always implement retry logic with exponential backoff:// 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: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: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:// 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: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: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:// 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:- Handle 429 responses gracefully even on endpoints without current limits
- Monitor response headers for future rate limit information
- Implement configurable delays between requests
- Use pagination to reduce the number of requests
- Cache data where appropriate