Idempotency

Retry any create safely with an Idempotency-Key.

Networks fail after the server has done the work. Without a way to say "this is the same request again", a retry after a timeout sends a second email or creates a second webhook endpoint. Every create endpoint takes an Idempotency-Key header to prevent that:

curl https://sendandretain.com/api/v1/emails \
  -H "Authorization: Bearer aem_your_key" \
  -H "Idempotency-Key: welcome/user_42" \
  -H "Content-Type: application/json" \
  -d '{ "to": "[email protected]", "template": "welcome" }'

The rules

You sendYou get
A new keyThe request runs normally.
The same key and the same body, after the first succeededThe first response again, with Idempotency-Replay: true. Nothing runs twice.
The same key and a different body409 conflict. A changed request needs a new key.
The same key while the first request is still running409 conflict. Retry once it has finished.
The same key after the first request failedThe request runs again. Only successes are replayed.
  • Keys are 1–256 characters and kept for 24 hours, then released.
  • Keys are scoped to the project and the operation, so the same key on two different endpoints is two different requests.
  • "Same body" ignores JSON key order and whitespace.
  • Build keys from the thing you are creating — welcome/user_42, import/2026-10-05 — rather than a random value per attempt. A random value per attempt protects nothing.

Which endpoints take it

Sends — POST /emails and POST /emails/batch — plus every create: POST /contacts/import, POST /suppressions, POST /suppressions/import, POST /templates, POST /automations, POST /automations/{id}/duplicate, POST /webhooks, POST /domains, POST /senders and POST /segments.

Updates and deletes are idempotent by nature and take no key. POST /contacts is an upsert keyed on the email address. POST /events dedupes on the dedupe_key field in its body instead.

Sends

A replayed send also carries deduplicated: true in the body, and the key is forwarded to your provider:

{ "id": "msg_3aF9c1", "status": "queued", "deduplicated": true }

In a batch, give each entry its own idempotency_key, or send one header and each entry derives <key>:<index>. Retrying a partly-failed batch then replays the entries that worked and retries only the ones that did not.

Webhook endpoints

POST /webhooks returns the signing secret exactly once. A replay of it omits secret — we never store it in plaintext. If you lost it, rotate it with POST /webhooks/{id}/rotate-secret.

After a 500, a 502 or a timeout, retry with the same key. Retrying without one risks doing the work twice. The SDKs generate a key for every create when retries are on.

On this page