Idempotency
Use the Idempotency-Key header to retry sends safely without dispatching duplicate email.
Pass an Idempotency-Key header on a send. The same key with the same body returns the original response instead of sending again. (Replace the recipient with an address you own.)
curl -X POST https://api.mailfully.com/v1/emails \
-H "Authorization: Bearer mf_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234-shipped" \
-d '{
"from": "orders@mail.acme.com",
"to": "you@yourcompany.com",
"subject": "Your order shipped",
"html": "<p>Order #1234 is on its way.</p>"
}'Run that request twice and both calls return 202 with the same message id. One email goes out.
Where it applies
Idempotency is opt-in and honored only by POST /v1/emails and POST /v1/emails/batch. Without the header there is no deduplication, and other endpoints ignore it entirely.
In the Node SDK, only emails.send and emails.batch accept the { idempotencyKey } options argument.
Keys must be 1-256 characters; there is no charset restriction beyond that. A key outside those bounds fails with 400 invalid_idempotency_key before anything else runs. Keys are scoped to your org, so they cannot collide with another tenant's.
Replay semantics
| Scenario | Result |
|---|---|
| First request with a key returns 2xx | Response cached for 24 hours |
| Same key + same body within 24 hours | The original response is replayed verbatim: same status, same id, no new email |
| Same key + different body | 409 invalid_idempotent_request |
| Same key while the first request is still in flight | 409 concurrent_idempotent_requests |
| First request failed (4xx/5xx) | The key is released; a retry re-runs the request |
| Idempotency store unreachable when the request arrives | 503 service_unavailable, zero side effects |
Key order inside objects doesn't matter, at any depth; array order and values do. Change any value and the same key returns 409 invalid_idempotent_request.
Choosing keys
Derive the key from the business event that triggered the send, not from the attempt:
order-1234-shipped
Every retry of the same logical send then shares one key. For a deliberate resend, such as a manual resend or a second reminder, use a new key like order-1234-shipped-resend-1. A random UUID generated per call gives no protection, because every retry looks like a new request.
Cached responses expire after 24 hours. After that, the same key runs as a brand-new request.
Errors
| Status | type | When |
|---|---|---|
| 400 | invalid_idempotency_key | Header present but not 1-256 characters |
| 409 | invalid_idempotent_request | Key reused with a different body |
| 409 | concurrent_idempotent_requests | The first request with this key is still in flight; wait for it to finish, then retry with the same key |
| 503 | service_unavailable | Idempotency store unreachable when the request arrives; nothing happened, so retry with the same key |
For the full error format, see Errors.
