Rate limits
When a rate limit is exceeded, the API returns HTTP 429 with one of three error codes: RATE_LIMIT_ACCOUNT, RATE_LIMIT_TENANT, or PLATFORM_RATE_LIMIT. Every response — including 2xx — carries RateLimit-Policy and RateLimit headers so your agent can pace itself before hitting the ceiling.
# On every successful response (2xx):
RateLimit-Policy: "tenant";q=2500;w=60
RateLimit: "tenant";r=2310;t=37
# On a 429 (rate limit exceeded):
RateLimit-Policy: "tenant";q=2500;w=60
RateLimit: "tenant";r=0;t=37
Retry-After: 37Response headers
| Header | Present on | Description |
|---|---|---|
RateLimit-Policy | Every response | Quota policy: "tenant";q=<quota>;w=<window_seconds>. q is the total requests allowed in the window; w is the window length in seconds. |
RateLimit | Every response | Current usage: "tenant";r=<remaining>;t=<seconds_until_reset>. r counts remaining requests; t is seconds until the window resets. |
Retry-After | 429 only | Seconds to wait before retrying. The retry_after_ms field in the error body carries the same value in milliseconds. |
Client algorithm
# On every successful response — read remaining headroom:
remaining = parse(RateLimit, "r") # remaining requests this window
quota = parse(RateLimit-Policy, "q") # total quota this window
window = parse(RateLimit, "t") # seconds until window resets
# Proactive throttle — slow down before hitting the ceiling:
if remaining < 0.20 * quota:
sleep(window / remaining) # spread remaining budget over the window
# On 429 — wait with jitter to avoid thundering herd:
wait = Retry-After + random(0, 2)
sleep(wait)
resume at reduced pace
# On repeated 429 — exponential backoff, capped at 60 seconds:
delay[n] = min(60, 2 * delay[n-1]) + random(0, initial_delay)
# example sequence: 2 → 4 → 8 → 16 → 32 → 60 → 60 → …TypeScript example
// TypeScript — proactive throttle + on-429 backoff
const API_KEY = process.env.CURVIATE_API_KEY ?? "cvt_live_<your_key>";
const BASE = "https://api.curviate.com";
function parseStructuredField(header: string, key: string): number {
const match = header.match(new RegExp(`${key}=(\\d+)`));
return match ? parseInt(match[1]!, 10) : 0;
}
async function apiRequest(path: string, body?: unknown): Promise<unknown> {
let delay = 2; // initial backoff seconds
for (;;) {
const res = await fetch(`${BASE}${path}`, {
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
// Proactive throttle on successful responses
if (res.ok) {
const policy = res.headers.get("RateLimit-Policy") ?? "";
const usage = res.headers.get("RateLimit") ?? "";
const quota = parseStructuredField(policy, "q");
const remaining = parseStructuredField(usage, "r");
const windowSec = parseStructuredField(usage, "t");
if (quota > 0 && remaining < 0.20 * quota && windowSec > 0) {
const wait = (windowSec / remaining) * 1000;
await new Promise(r => setTimeout(r, wait));
}
return res.json();
}
// Handle 429 — RATE_LIMIT_ACCOUNT, RATE_LIMIT_TENANT, or PLATFORM_RATE_LIMIT
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get("Retry-After") ?? "60", 10);
const jitter = Math.random() * 2;
const wait = (retryAfter + jitter) * 1000;
await new Promise(r => setTimeout(r, wait));
// Exponential backoff, capped at 60 seconds
delay = Math.min(60, delay * 2);
continue;
}
// Other errors
const err = await res.json() as { code: string; message: string };
throw new Error(`${err.code}: ${err.message}`);
}
}
// Usage
const accounts = await apiRequest("/v1/accounts");Python example
# Python — proactive throttle + on-429 backoff using httpx
import os, re, time, random
import httpx
API_KEY = os.environ.get("CURVIATE_API_KEY", "cvt_live_<your_key>")
BASE = "https://api.curviate.com"
def parse_sf(header: str, key: str) -> int:
m = re.search(rf"{key}=(\d+)", header)
return int(m.group(1)) if m else 0
def api_request(path: str, body: dict | None = None):
delay = 2 # initial backoff seconds
while True:
method = "POST" if body else "GET"
res = httpx.request(
method,
f"{BASE}{path}",
headers={"Authorization": f"Bearer {API_KEY}"},
json=body,
)
if res.is_success:
policy = res.headers.get("ratelimit-policy", "")
usage = res.headers.get("ratelimit", "")
quota = parse_sf(policy, "q")
remaining = parse_sf(usage, "r")
window = parse_sf(usage, "t")
if quota > 0 and remaining < 0.20 * quota and window > 0:
time.sleep(window / remaining)
return res.json()
if res.status_code == 429:
retry_after = int(res.headers.get("retry-after", "60"))
jitter = random.uniform(0, 2)
time.sleep(retry_after + jitter)
delay = min(60, delay * 2)
continue
err = res.json()
raise Exception(f"{err['code']}: {err['message']}")
# Usage
accounts = api_request("/v1/accounts")Writes and retries
Backoff and retry is safe for read operations and for operations that are naturally idempotent. For write operations — sending a message, creating an invite, publishing a post — retry only when you are certain the previous attempt did not apply. A network timeout does not guarantee the server did not process the request. Clients own retry safety.
Limits by seat count
Limits scale with active seats on your subscription and are designed to never constrain a well-behaved agent. The proactive throttle above will keep most clients well below the ceiling.
| Seats | Requests per 60 s | Requests per 10 s (burst) |
|---|---|---|
| 0 | 1 500 | 400 |
| 1 | 2 500 | 650 |
| 5 | 6 500 | 1 650 |
| 10 | 11 500 | 2 900 |
See Errors for the full error code reference, including RATE_LIMIT_ACCOUNT, RATE_LIMIT_TENANT, and PLATFORM_RATE_LIMIT.