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 send | You get |
|---|---|
| A new key | The request runs normally. |
| The same key and the same body, after the first succeeded | The first response again, with Idempotency-Replay: true. Nothing runs twice. |
| The same key and a different body | 409 conflict. A changed request needs a new key. |
| The same key while the first request is still running | 409 conflict. Retry once it has finished. |
| The same key after the first request failed | The 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.