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.

Something here wrong or missing? It is generated from the running API — tell us and we will fix the source.