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.
| Header | Purpose |
|---|---|
webhook-id | Unique delivery id |
webhook-timestamp | Unix seconds — reject if older than 5 minutes |
webhook-signature | v1,<base64 HMAC-SHA256> over {id}.{timestamp}.{body} |
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.