Rate limits

Endpoint groupPer-user/min
POST /api/v1/prepare60
POST /api/assets/:id/regenerate30
POST /api/projects/:id/deep-crawl10 (Firecrawl-bounded)
POST /api/projects/:id/detect-brand30
POST /api/projects/:id/webhooks/test30
Other authenticated /api endpoints300

Quota vs rate limit

Two independent limits apply:

  • Quota — monthly generation cap (5 / 100 / 500 by tier). Hits return 429 { error: 'monthly limit reached', upgrade_url }.
  • Rate limit — burst protection. Hits return 429 { error: 'rate limited', retry_after_ms }.

Headers

text
HTTP/1.1 429 Too Many RequestsRetry-After: 5

Retry-After is in seconds, rounded up. The body's retry_after_ms is more precise.

Backoff strategy

ts
async function callWithBackoff<T>( fn: () => Promise<Response>, maxAttempts = 5,): Promise<T> { for (let attempt = 1; attempt <= maxAttempts; attempt++) { const res = await fn(); if (res.status !== 429) { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } const retryMs = Number(res.headers.get('Retry-After')) * 1000; await new Promise((r) => setTimeout(r, retryMs)); } throw new Error('rate limit exceeded after max retries');}

Public proxy URLs

The proxy URL itself (/v1/...) has no per-IP rate limit, since edge cache absorbs repeat hits. The first generation per (asset_id) does count against monthly quota; subsequent same-URL requests are free cache hits.

Allowlisted origin spam

If a third party scrapes proxy URLs and you see surprising quota burn, switch to signed URLs — they expire and can't be re-used past exp.

SSE connection limits

Up to 5 minutes per connection. Reconnect with Last-Event-ID for longer streams. No per-asset connection cap (fan-out is in-process on the worker).

Ask a question... ⌘I