API Rate Limits
Adrata uses one-minute rate-limit windows to keep the API predictable for sellers, managers, leaders, agents, and integrations. Every metered response includes rate-limit headers so clients can pace proactively instead of waiting for a 429.
Plan Tiers
| Tier | Requests / min | AI tokens / min | Enrichment credits / day |
|---|---|---|---|
| Trial | 100 | 10,000 | 50 |
| Pro | 1,000 | 100,000 | 500 |
| Max | 5,000 | 500,000 | 2,500 |
| Enterprise | 10,000 | 1,000,000 | 5,000 |
Response Headers
These headers are exposed through CORS for browser clients. Header names are case-insensitive; examples below show canonical casing.
| Header | Description | Example |
|---|---|---|
| X-RateLimit-Limit | Budget for the active one-minute window. | 1000 |
| X-RateLimit-Remaining | Budget left after this response is counted. | 742 |
| X-RateLimit-Used | Budget consumed in the active window. | 258 |
| X-RateLimit-Reset | Unix epoch seconds when the current window resets. | 1748001234 |
| X-RateLimit-Scope | Logical limiter scope, such as api-rate or ai-tokens. | api-rate |
| X-RateLimit-Bucket | Specific bucket checked for this request. | rl:api:key:01HX... |
| Retry-After | Seconds to wait before retrying. Present on 429 responses. | 58 |
AI Token Bucket
AI routes use a separate ai_tokens_per_minute bucket. The debit formula is prompt_tokens * 1 + completion_tokens * 4, so long generated responses consume more AI budget than short prompts. AI bursts do not consume the CRUD request budget used by companies, people, buyer groups, opportunities, and other data endpoints.
AI 429 responses use application/problem+json and include "bucket": "ai_tokens" in the body.
Retry Strategy
Respect Retry-After first. If it is absent, use exponential backoff with jitter and cap the wait so background jobs do not stall indefinitely.
async function fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 5
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) return response;
if (attempt === maxRetries) {
throw new Error('Rate limit exceeded after max retries');
}
const retryAfter = response.headers.get('retry-after');
const waitMs = retryAfter
? Number.parseInt(retryAfter, 10) * 1000
: Math.min(1000 * 2 ** attempt + Math.random() * 500, 30_000);
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
throw new Error('Unreachable');
}import random
import time
import requests as req
def fetch_with_retry(
url: str,
headers: dict[str, str],
max_retries: int = 5,
) -> req.Response:
for attempt in range(max_retries + 1):
response = req.get(url, headers=headers, timeout=30)
if response.status_code != 429:
return response
if attempt == max_retries:
response.raise_for_status()
retry_after = response.headers.get("Retry-After")
wait = (
float(retry_after)
if retry_after
else min(1 * 2**attempt + random.random() * 0.5, 30)
)
time.sleep(wait)
raise RuntimeError("unreachable")Route-specific Limits
Most API routes use the plan tier request budget. Auth, public lookup, AI, enrichment, and agent-registration routes may use stricter buckets. The OpenAPI contract includes rate-limit response headers for 429s and route metadata for non-default limits as those routes are promoted into the public API surface.