Skip to main content

Rate limits

Limits are per API key, per resource, with two account- and network-wide caps layered on top as abuse backstops:

PlaneLimitApplies to
Read plane200 requests/minCatalog, Campaigns & Creatives, Deliveries — every GET outside Generation.
Generation, read120 requests/minGET /presets, GET /runs, GET /runs/{id}, GET /runs/{id}/items.
Generation, write20 requests/minPOST /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 reviews60 requests/minPOST /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 writes60 requests/minPATCH /creatives/{id} (updateCreative), DELETE /creatives/{id} (deleteCreative), POST /creatives/{creativeId}/undelete (undeleteCreative), shared between the three. Separate from reads and from reviews.
Asset writes60 requests/minPATCH /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 writes60 requests/minPOST /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:

TrafficAccount-widePer-IP
Read plane1,000 requests/min2,000 requests/min
Generation, reads and writes together360 requests/min600 requests/min
Writes: reviews, and creative, asset and campaign writes together300 requests/min600 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: limit is its quota, remaining is requests left in this window, and reset is seconds until its window resets. When two limits are equally close, the one with the smaller limit is reported.
  • RateLimit-Policy — every limit that applied to this request, each as quota;w=window-seconds, the closest one first (it is the one RateLimit describes). 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.