Skip to main content

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.