Sending Transactional Email Reliably
Transactional email — OTPs, receipts, password resets, alerts — is expected within seconds and must never duplicate or get lost. This guide covers reliable sending patterns, idempotency, and retries.
🚀 Quick Start
curl -X POST https://api.afriroute.ai/api/v1/email/send \
-H "Authorization: Bearer $AFRIROUTE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "[email protected]",
"from": "[email protected]",
"subject": "Your code",
"text": "Your OTP is 482913. Expires in 5 minutes."
}'
📨 Common Transactional Types
| Type | Priority | Notes |
|---|---|---|
| OTP / verification | Highest | Time-sensitive, plain text fine |
| Receipts | High | Include order detail, downloadable |
| Password reset | High | Single-use, expiring link |
| Notifications | Normal | Batchable, respect preferences |
🔁 Idempotency
Prevent duplicate sends on retries by passing an idempotency key. Re-sending with the same key returns the original result instead of sending twice.
await fetch('https://api.afriroute.ai/api/v1/email/send', {
method: 'POST',
headers: {
'Authorization': 'Bearer $AFRIROUTE_API_KEY',
'Content-Type': 'application/json',
'Idempotency-Key': `otp-${userId}-${requestId}`
},
body: JSON.stringify({ to: '[email protected]', template: 'otp', variables: { code } })
});
⏱️ Retries & Timeouts
Network blips happen. Retry safely with backoff — combined with idempotency, retries can't cause duplicates.
import requests, time
def send_with_retry(payload, key, attempts=3):
for i in range(attempts):
try:
r = requests.post(
'https://api.afriroute.ai/api/v1/email/send',
headers={'Authorization': 'Bearer $AFRIROUTE_API_KEY', 'Idempotency-Key': key},
json=payload, timeout=5)
if r.status_code < 500:
return r.json()
except requests.Timeout:
pass
time.sleep(2 ** i) # exponential backoff
raise RuntimeError('email send failed')
🔔 Confirming Delivery
Don't assume success from the API 200. Track delivery via webhooks for OTP and reset flows where it matters.
app.post('/webhooks/email', (req, res) => {
const { message_id, type } = req.body;
if (type === 'delivered') markDelivered(message_id);
if (type === 'hard_bounce') alertUserAltChannel(message_id); // fall back to SMS
res.sendStatus(200);
});
💡 Best Practices
- Always send an idempotency key for OTP and payment emails.
- Retry with backoff only on 5xx/timeouts, never on 4xx.
- Set short expirations on OTPs and reset links.
- Fall back to SMS when an OTP email bounces.
- Separate transactional and marketing streams/domains.
- Keep OTP emails plain and minimal for fastest delivery.
📚 Related Resources
Last Updated: May 2026