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.

Email

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=signature
X-Webhook-TimestampUnix timestamp (seconds)
X-Webhook-EventEvent type (e.g. email.delivered)
X-Webhook-IdUnique event ID (for deduplication)
X-Webhook-Version2

Signature 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.