Rate limits
The Caramel API rate-limits at two layers: the gateway in front, and individual tools behind it. Both apply to every request. The most restrictive limit wins. You get 429 Too Many Requests with a Retry-After header before any other failure mode.
Gateway limits
Section titled “Gateway limits”Three sliding-window caps apply simultaneously. A request is rejected if any cap is full.
| Dimension | Limit | Window | Bucket key |
|---|---|---|---|
| Per OAuth bearer token | 600 requests | 60 seconds | SHA-256 hash of the token |
| Per IP address | 60 requests | 60 seconds | Source IP |
| Per host (global) | 50,000 requests | 60 seconds | Hostname |
The window is sliding, not fixed — a request now and one 30 seconds later both count toward the same bucket.
The per-bearer limit is the one you’ll hit in practice. 600 rpm = 10 rps sustained. If you need higher caps for a server-side integration handling a burst, email aymen@reactmotion.com with usage estimates.
Per-tool limits
Section titled “Per-tool limits”Most tools rely on the gateway’s per-bearer cap. Two tools have additional limits:
| Tool | Extra limit | Window | Scope | Reason |
|---|---|---|---|---|
caramel.v1.form.submit | 10 submissions | 60 seconds | Per form_id + IP | Anti-abuse — stops form-bombing |
generate_campaign, refine_campaign | AI credit quota | 1 month | Per business | Credits decrement per call, not a sliding window |
When a per-tool limit is hit you get 429 with a Retry-After that reflects that tool’s window, not the gateway’s.
Response headers on a 429
Section titled “Response headers on a 429”HTTP/1.1 429 Too Many RequestsRetry-After: 7x-caramel-request-id: req_01h7m9k0aabcdefghj1234567x-ratelimit-limit: 600x-ratelimit-remaining: 0x-ratelimit-reset: 1717684350Content-Type: application/json
{ "error": "rate_limited", "message": "Per-token rate cap exceeded.", "status": 429}| Header | Description |
|---|---|
Retry-After | Seconds until you can retry. Integer; sometimes 0 if the window cleared in flight. |
x-ratelimit-limit | Current bucket capacity. |
x-ratelimit-remaining | Requests left in the current window. |
x-ratelimit-reset | Unix epoch seconds when the bucket fully resets. |
Backoff strategy
Section titled “Backoff strategy”Always honor Retry-After. Always add jitter.
async function rateLimitAware(fn) { while (true) { const res = await fn(); if (res.status !== 429) return res;
const wait = (Number(res.headers.get('retry-after')) || 1) * 1000; const jittered = wait + Math.random() * 500; await new Promise(r => setTimeout(r, jittered)); }}import time, random
def rate_limit_aware(fn): while True: res = fn() if res.status_code != 429: return res wait = int(res.headers.get('retry-after', '1')) time.sleep(wait + random.random() * 0.5)Without jitter, every client retries at the same moment and re-triggers the limiter immediately.
Avoid hitting limits
Section titled “Avoid hitting limits”A few patterns stay well inside the caps:
Cache caramel.v1.meta.capabilities. The tool catalog changes only when a new tool ships — roughly monthly. Cache it for the session. Every uncached call wastes a bucket slot.
Cache list_businesses for single-business accounts. Don’t call it on every page load.
Batch status reads. Fetch all campaigns once, then refresh on user action — not on a timer every 5 seconds.
Serialize bulk writes. When importing contacts via caramel.v1.contact.upsert, send at 5 rps. The limiter throttles faster traffic; you don’t gain throughput.
Monthly quotas
Section titled “Monthly quotas”Some tools have monthly quotas tied to subscription tier. These return 429 submission_cap or 402 insufficient_credits — distinct from the gateway’s 429 rate_limited.
| Quota | Returned by | Error code |
|---|---|---|
| Forms submissions / month | caramel.v1.form.submit | submission_cap |
| AI credits / month | generate_campaign, refine_campaign | insufficient_credits (402) |
Check current usage with caramel.v1.meta.usage:
# MCP call{ "jsonrpc": "2.0", "method": "tools/call", "id": 1, "params": { "name": "caramel.v1.meta.usage", "arguments": { "business_id": "YOUR_BUSINESS_ID" } }}Poll this once per day for usage dashboard widgets, not on every API call.
What is not rate-limited
Section titled “What is not rate-limited”These calls are free and don’t consume any bucket:
caramel.v1.meta.capabilities— tool discovery- OAuth discovery (
/.well-known/oauth-authorization-server)
The OAuth token endpoint has its own anti-abuse limits independent of the API limiter.
Next steps
Section titled “Next steps”- Errors — the full error catalog, including all 429 variants.
- Tiers and scopes — monthly quotas per tier.