Webhooks
Receive real-time HTTP callbacks when email events occur in your account.
When an event happens (email delivered, opened, bounced, etc.), SendMailOS sends an HTTP POST to your configured endpoint with the event payload. Configure endpoints in your Dashboard → Webhooks.
Event Types & Payloads
Select an event to see its description and example payload.
Subscriber
email.sent
Email accepted by the provider and queued for delivery.
{
"id": "ae204465-afeb-4f99-902a-223433ad67df",
"type": "email.sent",
"created_at": "2026-04-02T09:33:12.954Z",
"data": {
"id": "afca3804-9a4a-ba1a-2b5e-137ef28fbdd4",
"to": "[email protected]",
"event": "sent",
"subject": "Your order confirmation",
"from_email": "[email protected]",
"timestamp": "2026-04-02T09:33:12.845Z",
"subscriber_id": "3429b874-1ece-4d3f-ba20-91b288995adb",
"api_message_id": "4cce0aca-d4f3-4ce5-8435-1b7cd448b249",
"ses_message_id": "0107019d4d8a26ad-6b356db5-ce90-4008-...",
"delivery_run_id": "062e86e9-248d-4d92-ad70-8e351d52ef75"
}
}Request Headers
Every webhook request includes these headers for verification and routing:
X-Webhook-SignatureHMAC-SHA256: t=timestamp,v1=signatureX-Webhook-TimestampUnix timestamp (seconds)X-Webhook-EventEvent type (e.g. email.delivered)X-Webhook-IdUnique event ID (for deduplication)X-Webhook-Version2Signature Verification
Verify the X-Webhook-Signature header to confirm the request is from SendMailOS. The signature is computed as:
HMAC-SHA256(your_webhook_secret, "{timestamp}.{raw_json_body}")const crypto = require('crypto');
app.post('/webhooks/sendmailos', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const body = JSON.stringify(req.body);
// Parse signature: "t=123456,v1=abcdef..."
const parts = signature.split(',');
const t = parts.find(p => p.startsWith('t=')).slice(2);
const v1 = parts.find(p => p.startsWith('v1=')).slice(3);
// Compute expected signature
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(t + '.' + body)
.digest('hex');
if (v1 !== expected) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Check timestamp tolerance (5 min)
if (Math.abs(Date.now() / 1000 - parseInt(t)) > 300) {
return res.status(401).json({ error: 'Timestamp too old' });
}
const event = req.body;
console.log(event.type, event.data.to);
res.status(200).json({ received: true });
});Retry & Reliability
Failed deliveries are retried up to 8 times with exponential backoff and jitter. After all attempts are exhausted, the delivery is moved to a dead letter queue.
Retry Schedule
Exponential backoff: 10s × 3^(attempt-1), capped at 5 minutes
Attempt 1Immediate
Attempt 2~10 seconds
Attempt 3~30 seconds
Attempt 4~1.5 minutes
Attempt 5~4.5 minutes
Attempt 6~5 minutes (capped)
Attempt 7~5 minutes (capped)
Attempt 8~5 minutes (capped)
Circuit Breaker
If your endpoint fails 5 times consecutively, deliveries are paused for 5 minutes. This protects your server during outages.
Rate Limiting
Deliveries are rate-limited to 5 per second per endpoint. Large campaigns (e.g. 26k emails) are delivered as a steady stream, not a spike.
Dead Letter Queue
After 8 failed attempts, the delivery is dead-lettered. You can manually retry dead-lettered deliveries from the dashboard.
Timeout
Your endpoint must respond within 10 seconds. Return a 2xx status immediately and process the payload asynchronously if needed.
Best Practice: Always return
200 OK immediately, then process the event asynchronously. Use the X-Webhook-Id header for idempotency — you may receive the same event more than once during retries.