Public API

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 /operations and GET /openapi.json are 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: 1790000060

X-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 header Idempotent-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_conflict with Retry-After: 1; try again in a moment.
  • A request that fails (for example 402 insufficient_credits or 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.

On this page