Notix
Learn

A retry should never mean a second email.

An idempotency key is a string you attach to a send request that names the logical thing you are sending: this order’s receipt, this signup’s welcome. Send it in the Idempotency-Key header on POST /v1/emails, and a retry with the same key returns the first attempt’s emailId instead of sending again. It costs one header and removes the most common way a transactional email goes out twice.

Why a retry can send twice.

A send request has two halves: your request reaching the API, and the API’s answer reaching you. When the second half fails, your code sees a timeout and cannot tell whether the email was created. Every sensible runtime retries at that point: an HTTP client with a retry policy, a Lambda invoked asynchronously, a queue consumer whose visibility timeout expired, a cron job that runs again after a crash. Each retry is a fresh request to the API, and without a key each one is a fresh send. The customer receives two receipts, and you cannot see why, because from your side only one send succeeded.

The fix is to make the request itself say which send it is. With a key, the API stores the outcome of the first attempt under that key, and any later request carrying the same key gets that outcome back. The retry is then safe by construction, not by luck.

How Notix handles the header.

The server keeps a hash of the canonical request body next to the key. The canonical form sorts object keys and drops undefined fields, so two requests that differ only in field order count as the same body.

  • Same key, same body: 200 with the original emailId. Nothing is sent again, and nothing is billed again.
  • Same key, different body: 409 with code NOT_UNIQUE. A key that has been attached to one message cannot quietly be attached to another.
  • Same key, first request still running: 409 NOT_UNIQUE with a message saying the request is in progress. Two workers that pick up the same job at the same moment cannot both send; the second waits and retries.

A key is remembered for 24 hours after the first successful send and may be up to 256 characters. The in-flight lock lasts 60 seconds. A request that fails before an email is created leaves nothing behind, so the same key can be tried again straight away.

Choosing a key.

The key has to be the same on every attempt for the same send, which means it has to be derived from something you already have, not generated when the request is built. Your own record id plus the message’s purpose is the reliable recipe: order-1234-receipt, signup-9f2c-welcome, invoice-2026-0917-reminder. The purpose matters because one order can legitimately produce several emails, and each needs its own key.

A UUID minted per attempt is the common mistake: it looks unique, and it is, which is exactly why the retry carries a different one and sends again. If you queue sends, put the key on the job when it is enqueued, so every worker that picks the job up sends the same one.

The same header everywhere.

POST /api/v1/emails with Idempotency-Key
curl https://app.usenotix.dev/api/v1/emails \
  -H "Authorization: Bearer $NOTIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1234-receipt" \
  -d '{
    "from": "receipts@acme.com",
    "to": "customer@example.com",
    "subject": "Your receipt for order 1234",
    "html": "<p>Thanks for your order.</p>"
  }'

# First call:  200 { "emailId": "em_8f3..." }   (sent)
# Same again:  200 { "emailId": "em_8f3..." }   (not sent again)
# Same key, different body:
#   409 { "error": { "code": "NOT_UNIQUE",
#          "message": "Idempotency-Key already used with a different payload" } }

POST /v1/emails/batch takes the header too; a replayed batch returns the same list of ids the first call produced. So does POST /v1/sms. The key is scoped to your team, so a send started from one API key and retried from another still resolves to one message. The SMTP relay has no equivalent, because SMTP has no header for it; if a retry must be safe, use the API. The full parameter description is in the API reference.

Questions, answered.

What is an idempotency key when sending email?
A string you choose and send in the Idempotency-Key header that names one logical send, such as the receipt for order 1234. If your request times out or your function is retried, you send the same key again; the API recognises it and returns the emailId from the first attempt instead of sending a second copy. Without a key, every request is a new send, and a retry after a timeout can mean the customer gets two receipts.
What does the API return when I reuse a key?
Three cases. Same key and the same body: 200 with the original emailId, nothing is sent again. Same key and a different body: 409 with code NOT_UNIQUE, so a changed payload under a reused key is caught rather than silently ignored. Same key while the first request is still being processed: also 409 NOT_UNIQUE with a message saying the request is in progress; wait briefly and retry with the same key.
How long is a key remembered?
The result for a key is kept for 24 hours from the first successful send. After that the same key is treated as new, so a retry a day later would send again. The in-flight lock that makes a concurrent duplicate answer 409 lasts 60 seconds, which is longer than any single send takes.
How should I choose the key?
Derive it from your own record and the purpose of the message, so the same event always produces the same key: order-1234-receipt, signup-9f2c-welcome, invoice-2026-0917-reminder. A random UUID minted per attempt defeats the point, because the retry would carry a different key. Keys are up to 256 characters and can be any string.
Does the key apply to batch sends and SMS?
Yes. POST /v1/emails/batch takes the same header, and a replay returns the same list of emailIds the first call produced, without re-sending any of them. POST /v1/sms takes it too, with the same 200-on-replay and 409-on-mismatch behaviour. The scheduled sends Notix makes on your behalf inside journeys use an internal key per run and step for the same reason.
Is the key scoped to my API key or to my team?
To your team. Two API keys belonging to the same team share the same idempotency space, so a key used from your web server and again from a worker refers to the same send. Two different teams can use the same string without colliding.

One header, no duplicate receipts.

Send your first email with an Idempotency-Key in five minutes, on the free plan.