Idempotency
A request that times out has an unknown outcome: it may have sent, or it may not. Retrying without protection is how one email becomes two.
Pass an Idempotency-Key header and the retry is safe:
curl -X POST https://app.mailstein.com/api/v1/emails \
-H "Authorization: Bearer $MAILSTEIN_API_KEY" \
-H "Idempotency-Key: order-1234-confirmation" \
-H "Content-Type: application/json" \
-d '{"from":"hello@yourdomain.com","to":"someone@example.com","subject":"Your order","html":"<p>Thanks.</p>"}'
Supported on POST /v1/emails and POST /v1/emails/batch. The key can be up
to 256 characters.
What happens on a repeat
| Situation | Result |
|---|---|
| Same key, same body | The stored response is returned. Nothing is sent again. |
| Same key, different body | Rejected. The key already stands for a different request. |
| Original still in flight | Rejected — retry in a moment. |
The body is compared canonically, so key order and whitespace do not matter.
Choosing a key
Use something derived from the thing you are emailing about, not something random:
order-1234-confirmation
password-reset-user-88-1786313285
invoice-2026-08-payment-received
A random UUID generated at the call site defeats the purpose — your retry generates a new one and sends a second email. The key has to be stable across retries of the same logical send, which means deriving it from the event rather than the attempt.
When you do not need one
Reads are naturally safe to repeat. So is PUT on a contact — it is an upsert.
Deletes are safe to repeat too: deleting something already deleted answers
404, which is the right answer and harmless.
The operations that need a key are the ones that create something every time they succeed: sending email, and creating campaigns.