How-to
Idempotent requests
Use the Idempotency-Key header to retry send and forward requests safely. Key format, the 24-hour window, replays, 409 and 503 responses, and key strategies.
Networks fail. When a send request times out, you can’t tell whether Emailit received it, and sending it again might email your customer twice. An Idempotency-Key header makes the retry safe: Emailit processes the first request and returns the same response for any repeat with the same key.
How it works
Add an Idempotency-Key header to POST /emails or POST /emails/{id}/forward.
- First request. Emailit reserves the key for your workspace and processes the request.
- Success. Emailit stores the response for 24 hours. Any request with the same key in that window gets the stored response back with
200, and no new email is created. - Failure. If the request fails, for example with a
400or402, Emailit releases the key. Fix the problem and retry with the same key. - Overlap. If a second request arrives while the first is still running, it gets
409and nothing is sent. Retry shortly with the same key.
Keys are scoped to your workspace, so two workspaces can use the same key without conflict.
Key format
| Rule | Value |
|---|---|
| Length | 1 to 256 characters |
| Characters | Letters A–Z and a–z, digits 0–9, hyphen - and underscore _ |
| Scope | Per workspace |
| Window | 24 hours after the first successful response |
A key with other characters, such as : or /, is rejected with 400 Invalid Idempotency-Key.
Choose a key
Derive the key from the event that causes the email, so every retry path produces the same key:
| Example key | |
|---|---|
| Order receipt | order-1042-receipt |
| Password reset | password-reset-7f3c9a1e (the reset token’s ID) |
| Weekly digest | digest-user-881-2026-w40 |
| Background job | The job’s ID, or a UUID you generate when you enqueue the job and store with it |
Avoid keys that change between attempts, such as timestamps or a UUID generated inside the retry loop. They make every retry look like a new request.
Send with an idempotency key
The Node.js, Python and PHP examples retry on network errors and on 409, 429 and 5xx responses, reusing the same key each time. The cURL example uses curl’s built-in retry, which covers timeouts, 429 and most 5xx responses.
curl https://api.emailit.com/v2/emails \
-H "Authorization: Bearer $EMAILIT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-receipt" \
--retry 3 \
-d '{
"from": "Acme <orders@acme.com>",
"to": "ada@example.com",
"subject": "Receipt for order 1042",
"text": "Thanks for your order."
}'async function sendOnce(payload, key, attempts = 4) {
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
const res = await fetch('https://api.emailit.com/v2/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.EMAILIT_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': key,
},
body: JSON.stringify(payload),
});
if (res.ok) return res.json();
if (![409, 429].includes(res.status) && res.status < 500) {
throw new Error(`Send failed: ${res.status} ${await res.text()}`);
}
const wait = Number(res.headers.get('retry-after')) || attempt * 2;
await new Promise((r) => setTimeout(r, wait * 1000));
} catch (err) {
if (err.message.startsWith('Send failed') || attempt === attempts) throw err;
await new Promise((r) => setTimeout(r, attempt * 2000));
}
}
throw new Error('Send failed after retries');
}
const email = await sendOnce(
{
from: 'Acme <orders@acme.com>',
to: 'ada@example.com',
subject: 'Receipt for order 1042',
text: 'Thanks for your order.',
},
'order-1042-receipt',
);import os
import time
import requests
def send_once(payload, key, attempts=4):
for attempt in range(1, attempts + 1):
try:
res = requests.post(
"https://api.emailit.com/v2/emails",
headers={
"Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}",
"Idempotency-Key": key,
},
json=payload,
timeout=30,
)
except requests.RequestException:
if attempt == attempts:
raise
time.sleep(attempt * 2)
continue
if res.ok:
return res.json()
if res.status_code not in (409, 429) and res.status_code < 500:
res.raise_for_status()
time.sleep(int(res.headers.get("retry-after", attempt * 2)))
raise RuntimeError("Send failed after retries")
email = send_once(
{
"from": "Acme <orders@acme.com>",
"to": "ada@example.com",
"subject": "Receipt for order 1042",
"text": "Thanks for your order.",
},
"order-1042-receipt",
)function sendOnce(array $payload, string $key, int $attempts = 4): array
{
for ($attempt = 1; $attempt <= $attempts; $attempt++) {
$ch = curl_init('https://api.emailit.com/v2/emails');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('EMAILIT_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . $key,
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($body !== false && $status >= 200 && $status < 300) {
return json_decode($body, true);
}
if ($body !== false && !in_array($status, [409, 429]) && $status < 500) {
throw new RuntimeException("Send failed: $status $body");
}
sleep($attempt * 2);
}
throw new RuntimeException('Send failed after retries');
}
$email = sendOnce([
'from' => 'Acme <orders@acme.com>',
'to' => 'ada@example.com',
'subject' => 'Receipt for order 1042',
'text' => 'Thanks for your order.',
], 'order-1042-receipt');Responses
| Status | When | What to do |
|---|---|---|
200 |
First successful request, or a replay of it within 24 hours | Use the response. A replay has the same body, including the same id. |
400 Invalid Idempotency-Key |
The key is empty, too long or has invalid characters | Fix the key. |
409 Idempotency key in progress |
A request with the same key is still being processed | Wait a moment, then retry with the same key. |
503 Idempotency unavailable |
Emailit couldn’t reach its idempotency store, so it refused the request rather than risk a duplicate | Retry with the same key. |
| Any other error | The request failed and the key was released | Fix the cause and retry with the same key. |
Rate limits are checked before the key, so a retry can still get 429. Wait for the retry-after header and send the same key again.
Related
- Idempotency in the API reference
- Send an email
- Forward an email
- Rate limits
- Why are my emails sent twice?