Handling Rate Limits & Quotas¶
Learn how to gracefully handle rate limits (429) and insufficient credits (402) in your integration.
Overview¶
Circuit enforces two types of limits:
| Limit Type | HTTP Status | Description |
|---|---|---|
| Rate Limit | 429 |
Too many requests per minute |
| Credit Quota | 402 |
Insufficient credits to complete operation |
Your integration should handle both gracefully to provide a good user experience.
Rate Limits¶
Default Limits¶
Rate limits are per-API-key and vary by subscription tier:
| Tier | Limit (per minute) |
|---|---|
| Sandbox | 25 |
| Starter | 100 |
| Growth | 500 |
| Enterprise | Custom |
Limits are applied per endpoint:
| Endpoint | Default Limit |
|---|---|
/check-eligibility |
Tier limit |
/assess-risk |
Tier limit |
/ingest |
3x tier limit (encourages data sharing) |
/billing/* |
60/min (fixed) |
Rate Limit Headers¶
Every response includes rate limit information:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Your per-minute limit |
X-RateLimit-Remaining |
Requests remaining in window |
X-RateLimit-Reset |
Unix timestamp when window resets |
Handling 429 Responses¶
When rate limited, you'll receive:
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705329600
{
"detail": "Rate limit exceeded",
"retry_after": 45
}
Always check the Retry-After header - it tells you exactly how long to wait.
Implementing Retry Logic¶
Python Example¶
import time
from circuit_kyc import CircuitClient, RateLimitError
client = CircuitClient(api_key="sk_sandbox_...")
def check_eligibility_with_retry(email: str, max_retries: int = 3):
"""Check eligibility with exponential backoff."""
for attempt in range(max_retries):
try:
return client.check_eligibility(email=email)
except RateLimitError as e:
if attempt == max_retries - 1:
raise # Give up after max retries
# Use Retry-After if provided, otherwise exponential backoff
wait_time = e.retry_after or (2 ** attempt)
print(f"Rate limited. Waiting {wait_time}s before retry...")
time.sleep(wait_time)
# Usage
result = check_eligibility_with_retry("user@example.com")
TypeScript Example¶
import { CircuitClient, RateLimitError } from '@circuit-kyc/sdk';
const client = new CircuitClient({ apiKey: 'sk_sandbox_...' });
async function checkEligibilityWithRetry(
email: string,
maxRetries: number = 3
): Promise<EligibilityResult> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await client.checkEligibility({ email });
} catch (error) {
if (error instanceof RateLimitError) {
if (attempt === maxRetries - 1) throw error;
const waitTime = error.retryAfter ?? Math.pow(2, attempt);
console.log(`Rate limited. Waiting ${waitTime}s...`);
await sleep(waitTime * 1000);
} else {
throw error;
}
}
}
throw new Error('Max retries exceeded');
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Credit Quotas (402 Payment Required)¶
Understanding Credits¶
Each operation costs credits:
| Operation | Credits |
|---|---|
check-eligibility |
1 |
assess-risk |
5 |
claim-identity |
10 |
ingest |
FREE |
When your balance is insufficient, you'll receive:
HTTP/1.1 402 Payment Required
{
"detail": "Insufficient credits",
"current_balance": 3,
"required_credits": 5,
"operation": "assess-risk"
}
Handling 402 Responses¶
from circuit_kyc import CircuitClient, InsufficientCreditsError
client = CircuitClient(api_key="sk_sandbox_...")
try:
risk = client.assess_risk(subject_id="user-123")
except InsufficientCreditsError as e:
print(f"Need {e.required_credits} credits, have {e.current_balance}")
# Option 1: Enable auto-recharge
# Option 2: Redirect to billing page
# Option 3: Fallback to cheaper operation
# Fallback example:
result = client.check_eligibility(email="user@example.com") # Only 1 credit
import { CircuitClient, InsufficientCreditsError } from '@circuit-kyc/sdk';
const client = new CircuitClient({ apiKey: 'sk_sandbox_...' });
try {
const risk = await client.assessRisk({ subjectId: 'user-123' });
} catch (error) {
if (error instanceof InsufficientCreditsError) {
console.log(`Need ${error.requiredCredits}, have ${error.currentBalance}`);
// Fallback to cheaper operation
const result = await client.checkEligibility({ email: 'user@example.com' });
}
}
Proactive Monitoring¶
Check Balance Before Operations¶
For expensive operations, check balance first:
def assess_risk_safely(subject_id: str):
"""Check balance before expensive operation."""
balance = client.get_credit_balance()
if balance.available_credits < 5:
# Not enough for assess-risk (5 credits)
if balance.available_credits >= 1:
# Fall back to eligibility check
return client.check_eligibility(subject_id=subject_id)
else:
raise Exception("No credits available")
return client.assess_risk(subject_id=subject_id)
Set Up Low Balance Alerts¶
Configure webhook alerts when balance drops:
# In your webhook handler
async def process_event(event: dict):
if event["type"] == "billing.low_balance":
balance = event["data"]["current_balance"]
threshold = event["data"]["threshold"]
# Send alert to your team
send_slack_alert(f"Low Circuit credits: {balance} (threshold: {threshold})")
# Or trigger auto-purchase
# purchase_credits(package="small")
Enable Auto-Recharge¶
Prevent service interruption by enabling auto-recharge:
# Via SDK (if supported) or API
response = requests.post(
"https://api.circuit-kyc.com/api/v1/billing/auto-recharge",
headers={"X-API-Key": api_key},
json={
"enabled": True,
"threshold_credits": 100, # Recharge when below 100
"package": "small" # Purchase 1,000 credits
}
)
Best Practices¶
1. Implement Circuit Breaker Pattern¶
Don't hammer the API when it's failing:
import time
from dataclasses import dataclass
from enum import Enum
class CircuitState(Enum):
CLOSED = "closed" # Normal operation
OPEN = "open" # Failing - reject requests
HALF_OPEN = "half_open" # Testing if recovered
@dataclass
class CircuitBreaker:
failure_threshold: int = 5
recovery_timeout: int = 60
failures: int = 0
state: CircuitState = CircuitState.CLOSED
last_failure: float = 0
def record_failure(self):
self.failures += 1
self.last_failure = time.time()
if self.failures >= self.failure_threshold:
self.state = CircuitState.OPEN
def record_success(self):
self.failures = 0
self.state = CircuitState.CLOSED
def can_execute(self) -> bool:
if self.state == CircuitState.CLOSED:
return True
if self.state == CircuitState.OPEN:
if time.time() - self.last_failure > self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
return True
return False
return True # HALF_OPEN
2. Use Request Queuing¶
Queue requests and process at a controlled rate:
import asyncio
from collections import deque
class RequestQueue:
def __init__(self, rate_limit: int = 100):
self.queue = deque()
self.rate_limit = rate_limit
self.interval = 60 / rate_limit # seconds between requests
async def add(self, coro):
self.queue.append(coro)
async def process(self):
while self.queue:
coro = self.queue.popleft()
await coro
await asyncio.sleep(self.interval)
3. Cache Eligibility Results¶
Don't check the same user repeatedly:
from functools import lru_cache
from datetime import datetime, timedelta
# Simple in-memory cache (use Redis in production)
eligibility_cache = {}
CACHE_TTL = timedelta(hours=1)
def check_eligibility_cached(email: str):
cache_key = f"eligibility:{email}"
# Check cache
if cache_key in eligibility_cache:
result, timestamp = eligibility_cache[cache_key]
if datetime.now() - timestamp < CACHE_TTL:
return result
# Call API
result = client.check_eligibility(email=email)
# Cache result
eligibility_cache[cache_key] = (result, datetime.now())
return result
Summary¶
| Scenario | Response | Action |
|---|---|---|
| Rate limited | 429 |
Wait for Retry-After seconds |
| Low credits | 402 |
Purchase credits or use cheaper operation |
| Repeated failures | - | Implement circuit breaker |
| High volume | - | Queue requests, respect rate limit |
Building a robust integration means handling these edge cases gracefully!