SendMailOS
Guides

Webhooks

Receive and verify signed delivery, open, click, and bounce events.

Webhooks push email events to your server the moment they happen — no polling. Register an HTTPS endpoint with POST /v1/webhooks and subscribe to events from the catalog (or ["*"] for all).

The event payload

{
  "id": "evt_...",
  "type": "email.delivered",
  "created_at": "2026-07-22T10:00:00Z",
  "data": {
    "email_id": "...",
    "to": "[email protected]",
    "subject": "Hello from SendMailOS"
  }
}

Common types: email.sent, email.delivered, email.opened, email.clicked, email.bounced, email.complained, email.suppressed.

Verifying signatures

Deliveries carry svix-compatible headers, and the signing secret (whsec_…) is returned in full only when you create the endpoint. Always verify before trusting a payload.

HeaderPurpose
webhook-idUnique delivery id
webhook-timestampUnix seconds — reject if older than 5 minutes
webhook-signaturev1,<base64 HMAC-SHA256> over {id}.{timestamp}.{body}
Node — verify
import crypto from 'node:crypto';

function verify(secret, headers, rawBody) {
  const id = headers['webhook-id'];
  const ts = headers['webhook-timestamp'];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay window

  const signed = `${id}.${ts}.${rawBody}`;
  const key = Buffer.from(secret.split('_')[1], 'base64');
  const expected = crypto.createHmac('sha256', key).update(signed).digest('base64');

  return headers['webhook-signature']
    .split(' ')
    .some((sig) => sig.split(',')[1] === expected);
}

Verify against the raw request body, before any JSON parsing — re-serializing changes the bytes and breaks the signature.

Retries & deliveries

Failed deliveries retry at 1m, 5m, 30m, 2h, and 5h. An endpoint that fails for five days is disabled. Inspect and replay attempts with GET /v1/webhooks/{id}/deliveries and rotate the secret with POST /v1/webhooks/{id}/rotate-secret.

On this page