Skip to content

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.

Three sliding-window caps apply simultaneously. A request is rejected if any cap is full.

DimensionLimitWindowBucket key
Per OAuth bearer token600 requests60 secondsSHA-256 hash of the token
Per IP address60 requests60 secondsSource IP
Per host (global)50,000 requests60 secondsHostname

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.

Most tools rely on the gateway’s per-bearer cap. Two tools have additional limits:

ToolExtra limitWindowScopeReason
caramel.v1.form.submit10 submissions60 secondsPer form_id + IPAnti-abuse — stops form-bombing
generate_campaign, refine_campaignAI credit quota1 monthPer businessCredits 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.

HTTP/1.1 429 Too Many Requests
Retry-After: 7
x-caramel-request-id: req_01h7m9k0aabcdefghj1234567
x-ratelimit-limit: 600
x-ratelimit-remaining: 0
x-ratelimit-reset: 1717684350
Content-Type: application/json
{
"error": "rate_limited",
"message": "Per-token rate cap exceeded.",
"status": 429
}
HeaderDescription
Retry-AfterSeconds until you can retry. Integer; sometimes 0 if the window cleared in flight.
x-ratelimit-limitCurrent bucket capacity.
x-ratelimit-remainingRequests left in the current window.
x-ratelimit-resetUnix epoch seconds when the bucket fully resets.

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.

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.

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.

QuotaReturned byError code
Forms submissions / monthcaramel.v1.form.submitsubmission_cap
AI credits / monthgenerate_campaign, refine_campaigninsufficient_credits (402)

Check current usage with caramel.v1.meta.usage:

Terminal window
# 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.

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.

  • Errors — the full error catalog, including all 429 variants.
  • Tiers and scopes — monthly quotas per tier.