Rate limits and idempotency
Understand per-key request limits and safely retry job creation.
Rate limits
Each API key has two per-minute budgets:
- Creating jobs (
POST /jobs): 30 requests per minute for organizations on the free tier and 120 per minute for organizations with an active plan. - Everything else (reading and polling jobs, listing, cancelling, deleting, webhooks, multipart uploads).
GET /operationsandGET /openapi.jsonare public and are not rate limited: 300 requests per minute, so polling never trips the limit.
Every response that passes authentication tells you where you stand for the budget that request used:
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 12
X-RateLimit-Reset: 1790000060X-RateLimit-Reset is the Unix time (seconds) when the minute ends. When you run out, the API answers 429 with the code rate_limited, X-RateLimit-Remaining: 0 and a Retry-After header with the seconds to wait. Several keys do not share a budget: one busy key never slows another. Every retry of POST /jobs counts against the creation budget, including a retry that is answered from the idempotency cache.
GET /jobs/{id}?wait= counts as one request however long it waits, so long-polling is much cheaper than polling every second. Better still, use webhooks.
Requests with a bad or missing key are throttled separately, per IP address: after 30 failed attempts in a minute, further requests from that address, even with a valid key, get 429 with details.reason set to ip_throttled until the minute ends. Good requests never count toward this. A per-key limit has details.reason set to key_rate_limited.
Active jobs on the free tier
Organizations on the free tier can have at most 5 active jobs (waiting, queued or processing) at a time, across all of their live keys. A sixth POST /jobs answers 429 with the code rate_limited and error.details.reason set to active_job_limit_reached. There is no Retry-After for this error: create again once one of your jobs has finished, failed or been cancelled. A waiting job that never receives its upload stops counting after an hour. Test-mode keys are not capped. Organizations with an active plan have no such cap; jobs beyond the platform's current capacity simply wait as queued.
Idempotency
Send an Idempotency-Key header on POST /jobs so that retrying after a network failure cannot create a second job. The key is any 1–255 printable ASCII characters; use a unique value per intended job (an order id plus a counter works well). Keys are remembered for 24 hours and belong to the API key that used them.
- Same key, same body: you get the original response with its original status (
201) and the headerIdempotent-Replayed: true. No new job is created. - Same key, different body:
422 idempotency_conflict. - Same key while the first request is still running:
409 idempotency_conflictwithRetry-After: 1; try again in a moment. - A request that fails (for example
402 insufficient_creditsor a validation error) does not save the key, so you can fix the problem and retry with the same key.
curl -X POST "$BASE/jobs" \
-H "Authorization: Bearer $KEY" \
-H 'Idempotency-Key: order-842-job-1' \
-H 'Content-Type: application/json' \
-d '{"tasks":{"import-1":{"operation":"import/upload"},"convert-1":{"operation":"convert","input":"import-1","input_format":"png","output_format":"webp"},"export-1":{"operation":"export/url","input":"convert-1"}}}'Send the identical body when you retry. Other endpoints do not need a key: GET and DELETE are safe to repeat, and cancelling twice is answered with a clear 409.