Rate limits
Limits are per API key, per resource, with two account- and network-wide caps layered on top as abuse backstops:
| Plane | Limit | Applies to |
|---|---|---|
| Read plane | 200 requests/min | Catalog, Campaigns & Creatives, Deliveries — every GET outside Generation. |
| Generation, read | 120 requests/min | GET /presets, GET /runs, GET /runs/{id}, GET /runs/{id}/items. |
| Generation, write | 20 requests/min | POST /runs (createRun), POST /runs/{id}/stop (stopRun). Tighter, because a create starts paid generation — 20/min is one create every three seconds sustained, well above any real create cadence, with headroom for the retry loop Idempotency-Key prescribes. |
| Creative reviews | 60 requests/min | POST /creatives/{creativeId}/approve (approveCreative), POST /creatives/{creativeId}/reject (rejectCreative), shared between the two. A budget of their own: a burst of reviews never eats into the key's read budget, and heavy reading never blocks a review. |
| Creative writes | 60 requests/min | PATCH /creatives/{id} (updateCreative), DELETE /creatives/{id} (deleteCreative), POST /creatives/{creativeId}/undelete (undeleteCreative), shared between the three. Separate from reads and from reviews. |
| Asset writes | 60 requests/min | PATCH /assets/{id}, DELETE /assets/{id} and the archive, unarchive and undelete actions, shared between them. Separate from reads and from every other write budget. |
| Campaign writes | 60 requests/min | POST /campaigns, PATCH /campaigns/{id}, DELETE /campaigns/{id} and POST /campaigns/{campaignId}/undelete, shared between them. Separate from reads and from every other write budget. |
The two backstops are counted separately for each kind of traffic, so running one out never throttles another:
| Traffic | Account-wide | Per-IP |
|---|---|---|
| Read plane | 1,000 requests/min | 2,000 requests/min |
| Generation, reads and writes together | 360 requests/min | 600 requests/min |
| Writes: reviews, and creative, asset and campaign writes together | 300 requests/min | 600 requests/min |
Account-wide sums every key on the account — a second key doesn't multiply the account's total headroom. Per-IP counts every request from one source IP, regardless of which key (or no key) it carries — an abuse backstop, not a budget to plan integrations against.
These numbers are defaults. Mocart can adjust any of them at runtime, for every account or for a single one, without a changelog entry — so read the headers below rather than hard-coding a limit into your integration.
POST /runs/estimate (estimateRun) counts against the Generation read limit, not the write
limit — it writes nothing.
Headers
Every response carries rate-limit headers in the
IETF RateLimit draft-7
format. A request is counted against several limits at once — your key's limit, the account-wide
cap and the per-IP cap above — and the headers report the one you are closest to running out
of, so remaining is always the honest answer to "how many more requests can I send":
RateLimit: limit=200, remaining=142, reset=17
RateLimit-Policy: 200;w=60, 1000;w=60, 2000;w=60
RateLimit— where you currently stand on that closest limit:limitis its quota,remainingis requests left in this window, andresetis seconds until its window resets. When two limits are equally close, the one with the smallerlimitis reported.RateLimit-Policy— every limit that applied to this request, each asquota;w=window-seconds, the closest one first (it is the oneRateLimitdescribes). Identical limits are listed once.
The list only holds limits that were checked for this request: a 401 for a bad key stops before
the account-wide cap is counted, so it lists your key's limit and the per-IP cap only. On a 429,
the limit that rejected you is the one reported, with remaining=0.
For a typical key the first number is therefore its own limit — 200 on the read plane, 120 or
20 on Generation, 60 on each kind of write (all by default) — until the account-wide or per-IP
cap becomes the tighter one.
On a 429, the response also carries:
Retry-After: 12
seconds to wait before your next request is likely to succeed.
The 429 body depends on the plane
On the Generation plane (GET /presets, every /runs operation), a 429 is already
application/problem+json with code: "RATE_LIMITED", exactly like every other error there.
On the read plane (Catalog, Campaigns & Creatives, Deliveries), a 429 today returns a non-JSON
body instead — not application/problem+json. This is scheduled to change on that plane too, but
only after a 90-day deprecation notice (see Versioning). Don't parse a 429
body from the read plane. Either way, rely on the 429 status code and Retry-After — both are
stable today across every plane, and will remain so through the transition.
Every write operation returns the same non-JSON 429 body. Don't parse it there either.
A 429 on a write is safe to retry with the same Idempotency-Key (or none, for a PATCH or DELETE you sent
without one) once Retry-After has passed.
Backoff recipe
async function withBackoff(fn, { maxAttempts = 5 } = {}) {
for (let attempt = 1; ; attempt++) {
const res = await fn();
if (res.status !== 429) return res;
if (attempt >= maxAttempts) {
throw new Error('rate limited after max attempts');
}
const retryAfter = Number(res.headers.get('Retry-After') ?? '1');
// Add jitter so a fleet of callers doesn't retry in lockstep.
const jitterMs = Math.random() * 250;
await new Promise((r) => setTimeout(r, retryAfter * 1000 + jitterMs));
}
}
Honor Retry-After exactly rather than a fixed exponential backoff — the server has told you the
soonest a retry is likely to succeed, and waiting longer than necessary only slows you down. See
Runs for why a large run's poll interval should be derived from durationSeconds
rather than tight-looped in the first place — that's the more effective fix for a run-plane 429.