Webhooks
Parse provider delivery events into EmailEvent or DeliveryEvent shapes, with optional signature checks.
Use webhooks when a provider posts delivery updates (delivered, bounced, failed, and similar).
Email parsers return EmailEvent[]. SMS and WhatsApp parsers return DeliveryEvent[].
Map email events with toDeliveryEvent when you want one ingest pipeline.
The one rule
Verify the signature against the unmodified raw request body before you trust or act on the event.
Quick start
Import one provider subpath
Prefer sently/webhooks/<provider> so runtimes without a bundler do not load every parser.
import { parse, verifySignature } from "sently/webhooks/sndr";Verify, then parse
const rawBody = await request.text(); // exact bytes as a string
const signature = request.headers.get("x-sndr-signature") ?? "";
const ok = await verifySignature(
rawBody,
signature,
process.env.SNDR_WEBHOOK_SECRET!,
);
if (!ok) {
return new Response("bad signature", { status: 400 });
}
const events = parse(JSON.parse(rawBody));Handle normalized events
for (const event of events) {
if (event.type === "bounced" || event.type === "failed") {
// suppress or alert using event.messageId / event.recipient
}
}Provider entrypoints
| Import | Channel | Parse export | Verify export |
|---|---|---|---|
sently/webhooks/brevo | parse → EmailEvent[] | — | |
sently/webhooks/mailgun | parse | verifySignature, verifyPayload | |
sently/webhooks/postmark | parse | — | |
sently/webhooks/resend | parse | verifySignature | |
sently/webhooks/sendgrid | parse | — | |
sently/webhooks/ses | parse | — | |
sently/webhooks/sndr | parse | verifySignature | |
sently/webhooks/twilio-sms | SMS | parse → DeliveryEvent[] | verifySignature (X-Twilio-Signature) |
sently/webhooks/unifonic | SMS | parse → DeliveryEvent[] | — |
sently/webhooks/whatsapp-cloud | parse → DeliveryEvent[] | verifySignature (X-Hub-Signature-256) |
The convenience barrel sently/webhooks re-exports named helpers such as parseSndrWebhook, parseTwilioSmsWebhook, and toDeliveryEvent.
Bundlers tree-shake unused names; Node/Deno without a bundler evaluate every re-export.
SMS status callbacks
Twilio posts form fields (MessageSid, MessageStatus, To). Pass a parsed object or URLSearchParams:
import { parse } from "sently/webhooks/twilio-sms";
const events = parse(await request.formData().then((fd) => Object.fromEntries(fd)));SNDR signatures
SNDR signs with X-Sndr-Signature: t=<unix>,v1=<hex>.
The digest is HMAC-SHA256 over `${t}.${rawBody}`.
Consequence: Re-serializing JSON changes whitespace and key order and breaks verification — always use the raw body string.
Default timestamp tolerance is 300 seconds. Pass { toleranceSeconds: 0 } to disable the freshness check.
Troubleshooting
Most frameworks parse JSON before your handler runs. Read the raw body first (request.text(), express.raw(), or equivalent), verify, then JSON.parse.
Email: Brevo, Mailgun, Postmark, Resend, SendGrid, SES, SNDR.
SMS / WhatsApp: Twilio SMS, Unifonic, WhatsApp Cloud.
Import each from its sently/webhooks/<provider> subpath.
Learn more
- Webhook events —
EmailEvent/DeliveryEventfields - Exports — package entrypoints