Developer Platform
Docs chevron_right Developer Platform chevron_right Idempotency & rate limits

Idempotency & rate limits

Two platform-wide behaviors that make integrations robust: idempotency keys turn retries after network timeouts into safe no-ops, and per-key rate limits with standard headers tell your client exactly when to back off.

Idempotency

Any write request (POST, PUT, PATCH, DELETE) may carry an optional Idempotency-Key header — up to 100 printable ASCII characters of your choosing. If the request succeeds and you retry it with the same key and the same body within 24 hours, the API replays the original response byte-for-byte instead of executing the operation again, and marks the replay with an Idempotency-Replayed: true response header. This makes retries after network timeouts safe: a list, campaign, or subscriber is created exactly once no matter how many times the request is repeated.

# Create a campaign — safe to retry on timeout
curl -X POST https://emailflow.ai/api/v1/campaigns \
  -H "Authorization: Bearer efa_YOUR_KEY" \
  -H "Idempotency-Key: spring-sale-launch-2026" \
  -d list_uid=ab12cd34ef \
  -d name="Spring sale" \
  -d subject="20% off this week" \
  -d from_email=hi@acme.com \
  -d from_name="Acme"

The rules in detail:

SituationResult
Same key, same body, within 24 hoursStored response replayed with Idempotency-Replayed: true.
Same key, different body409 IDEMPOTENCY_CONFLICT — pick a fresh key for a different request.
Retry while the first attempt is still running409 IDEMPOTENCY_CONFLICT with details.reason: "in_flight" — wait and retry.
First attempt failed with a 5xxNothing is stored; the retry executes normally.
Key longer than 100 characters or non-printable400 INVALID_REQUEST with param: "Idempotency-Key".
GET request with the headerHeader ignored.

Keys are scoped to your account, so they never collide with other customers'. A good key encodes the operation and its trigger, for example order-created-8861-1718200000 (<event>-<your id>-<timestamp>), or simply a random UUID generated per logical operation. The TypeScript SDK can generate one per call with idempotencyKey: true.

Rate limits

Requests are limited per minute by operation class. Each API key gets its own independent bucket (legacy-token callers share one bucket per user), so one busy integration never starves another. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (the Unix time when the window resets); when you exceed a limit the API returns 429 with a RATE_LIMITED error envelope whose retryAfter matches the Retry-After header — wait that many seconds and retry.

ClassRequests / minuteApplies to
read 100 All read (GET) requests on resource endpoints.
write 60 Create, update, and delete requests on resource endpoints.
batch 10 Bulk subscriber import (sync and async) and batch operations.
ai 20 AI email generation and edit requests.
sends 10 Campaign run (send) triggers.
default 60 All other endpoints (administrative and legacy surfaces).
speed
Watch your real traffic against these limits under Account → API → API usage — request volume, error rate, and latency per key and per route. See usage analytics.

Putting the two together: a robust retry loop

Idempotency and rate limits are designed to compose into one simple client policy:

  1. Generate an Idempotency-Key (a UUID is fine) before the first attempt of any write.
  2. On a network timeout or 5xx, retry with the same key and body — failed attempts are never stored, so a real failure re-executes, and a response you simply never received is replayed.
  3. On 429, sleep for retryAfter seconds, then retry with the same key.
  4. On any other 4xx, stop — the request itself is wrong, and retrying will not change the outcome.

This is exactly the policy the TypeScript SDK implements out of the box: pass idempotencyKey: true and it generates the UUID, reuses it across retries, honors Retry-After, and backs off exponentially on transient failures.