Idempotency For Transactional Email APIs
Networks fail, retries happen, and every retry is a chance to send the same receipt twice. Idempotency keys make that impossible when they are used well.
A transactional email API sits on the ugliest part of every checkout flow: right after money changes hands, right after a password is reset, right after an account is created. If the call to the email provider times out, the app almost always retries. Without idempotency, the retried call sends a second receipt or a second reset link, and the user files a support ticket.
The Pattern
- The client generates a unique key per logical send, typically a UUID v4 or a hash of the deterministic message identity.
- The client sends the key with the request, usually as a header:
Idempotency-Key: 9c1b7c4a-... - The server records the key with the response before returning. Any subsequent request with the same key returns the stored response and does not send again.
- Keys are scoped to the API key (or account) and are stored for a bounded window, typically 24 hours.
Stripe popularized this shape for payments (Idempotent Requests in their API docs); the same idea applies cleanly to email.
Choosing The Key
The easiest bug to write is a key that changes on every attempt. The client generates the key once per logical send, not once per attempt. In practice:
- Random UUID at the start of the flow. Simple, works everywhere, but requires you to persist it if the flow spans processes.
- Deterministic hash. For a receipt tied to order
ord_123, a key likesha256("receipt:ord_123")means every retry, from any process, collapses to the same key. This is the pattern to reach for.
Server-Side Behavior That Matters
- Record before you send. The key row is written in the same transaction as the enqueue. Otherwise a retry arriving during the send can slip through.
- Return the stored response. Not just a “409 Duplicate.” The client asked for a result; give it the same result it would have gotten if the original call had come back.
- Fail loudly on request drift. If a second call arrives with the same key but a different body, refuse it. Silently coalescing hides real bugs.
Client-Side Retry Hygiene
Retry only on 5xx, 408, 429, and network-level errors. Never retry 4xx. Cap retries at three or four with exponential backoff and jitter. If your queue worker crashes and restarts, the same idempotency key on the same job means a duplicate is impossible.
Send transactional email that lands.
MailRoundup is a transactional-only pipe for receipts, confirmations, password resets, and alerts. One endpoint, honest logs, no marketing traffic in the way.