Idempotency
A network timeout tells you nothing about whether the request arrived. Retrying blindly can send the same email twice. An idempotency key makes the retry safe.
Send one on supported POST operations, including email sends:
Idempotency-Key: order-4821-welcome
If we have already processed that key, we return the original response instead of doing the work again.
What happens on a retry
curl https://api.epostix.com/v1/emails \
-H "Authorization: Bearer $EPOSTIX_API_KEY" \
-H "Idempotency-Key: order-4821-welcome" \
-H "Content-Type: application/json" \
-d '{"from":"hello@mail.yourdomain.com","to":["you@yourdomain.com"],"subject":"Welcome","html":"<p>Hi</p>"}'
Run that twice and you get the same id both times, one email, and on the second response:
Idempotent-Replayed: true
That header is how you distinguish a replay from a fresh send.
Keys are remembered for 24 hours, scoped to the workspace, API key, environment, HTTP
method and request path. The same key on POST /emails and POST /suppressions is unrelated.
Changing the API key also starts a different deduplication scope.
Keep the request body identical, including its serialized bytes. Credential-creating operations, such as creating API keys or webhook endpoints and rotating webhook secrets, are excluded from response caching. Do not assume a header makes every mutation retryable.
SDKs generate keys for supported send calls, but a key generated for one call does not survive a new call or worker restart. Supply your own stable key for background jobs.
The three answers you can get
The same response, with Idempotent-Replayed: true. The original request finished. You
are looking at its result.
idempotency_conflict, 409. The key has been used with a
different body. This nearly always means the key is not as unique as you thought. It is a
signal, not an obstacle: something is reusing a key across genuinely different sends.
idempotency_in_progress, 409 with Retry-After: 1.
An identical request is still running. This is what stops two racing retries both sending.
Wait and try again, and you will get the first request's response.
Choosing a key
Derive it from the thing you are reacting to, not from the moment you are reacting.
order-4821-welcome
invoice-99120-reminder-2
password-reset-user-7731-2026-08-19T13:04:11Z
A key built from your own identifiers survives a process restart, a queue redelivery and a retry from a different worker. A random UUID generated at call time does not: the retry generates a new one and sends a second email, which is exactly the failure you were trying to prevent.
Keys may be up to 255 characters.
When errors are and are not remembered
Responses below 500 that pass through the idempotency handler can be cached, including
validation failures. After fixing a definitively rejected request, use a new key for the
corrected request. Reusing the old one can replay the error or return idempotency_conflict.
A 5xx response is not cached. It can still follow a partial operation, so do not interpret it as proof that nothing happened. Keep the same key and inspect the message state when the outcome is uncertain. Idempotency has a 24-hour window; it is not a permanent exactly-once guarantee.
Where it is worth using
Anywhere a duplicate would be visible to a person: order confirmations, password resets, one-time codes, invoices. Anywhere your sender is a queue consumer, since redelivery is normal rather than exceptional.
Idempotency and duplicate detection solve different problems. Idempotency protects a single logical send from being repeated by retries. Duplicate detection catches a loop hammering one recipient with the same message.