Mailfully logo

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

ScenarioResult
First request with a key returns 2xxResponse cached for 24 hours
Same key + same body within 24 hoursThe original response is replayed verbatim: same status, same id, no new email
Same key + different body409 invalid_idempotent_request
Same key while the first request is still in flight409 concurrent_idempotent_requests
First request failed (4xx/5xx)The key is released; a retry re-runs the request
Idempotency store unreachable when the request arrives503 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

StatustypeWhen
400invalid_idempotency_keyHeader present but not 1-256 characters
409invalid_idempotent_requestKey reused with a different body
409concurrent_idempotent_requestsThe first request with this key is still in flight; wait for it to finish, then retry with the same key
503service_unavailableIdempotency store unreachable when the request arrives; nothing happened, so retry with the same key

For the full error format, see Errors.