---
title: Webhooks
description: Parse provider delivery events into EmailEvent or DeliveryEvent shapes, with optional signature checks.
icon: Webhook
source: "src/webhooks.ts"
---

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.

<Callout title="The one rule">
  Verify the signature against the unmodified raw request body before you trust or act on the event.
</Callout>

## Quick start

<Steps>

<Step>
### Import one provider subpath

Prefer `sently/webhooks/<provider>` so runtimes without a bundler do not load every parser.

```ts
import { parse, verifySignature } from "sently/webhooks/sndr";
```

</Step>

<Step>
### Verify, then parse

```ts
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));
```

</Step>

<Step>
### Handle normalized events

```ts
for (const event of events) {
  if (event.type === "bounced" || event.type === "failed") {
    // suppress or alert using event.messageId / event.recipient
  }
}
```

</Step>

</Steps>

## Provider entrypoints

| Import | Channel | Parse export | Verify export |
| ------ | ------- | ------------ | ------------- |
| `sently/webhooks/brevo` | Email | `parse` → `EmailEvent[]` | — |
| `sently/webhooks/mailgun` | Email | `parse` | `verifySignature`, `verifyPayload` |
| `sently/webhooks/postmark` | Email | `parse` | — |
| `sently/webhooks/resend` | Email | `parse` | `verifySignature` |
| `sently/webhooks/sendgrid` | Email | `parse` | — |
| `sently/webhooks/ses` | Email | `parse` | — |
| `sently/webhooks/sndr` | Email | `parse` | `verifySignature` |
| `sently/webhooks/twilio-sms` | SMS | `parse` → `DeliveryEvent[]` | `verifySignature` (`X-Twilio-Signature`) |
| `sently/webhooks/unifonic` | SMS | `parse` → `DeliveryEvent[]` | — |
| `sently/webhooks/whatsapp-cloud` | WhatsApp | `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`:

```ts
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.

Normalized event types include queued → `deferred`, delivered, bounced, complained, opened, clicked, and failed / unsubscribed → `unknown`. See [SNDR](/docs/transports/sndr#webhooks).

Default timestamp tolerance is 300 seconds. Pass `{ toleranceSeconds: 0 }` to disable the freshness check.

## Troubleshooting

<Accordions>

<Accordion title="Signature verifies in docs but fails in production">

Most frameworks parse JSON before your handler runs. Read the raw body first (`request.text()`, `express.raw()`, or equivalent), verify, then `JSON.parse`.

</Accordion>

<Accordion title="Which providers are supported?">

Email: Brevo, Mailgun, Postmark, Resend, SendGrid, SES, SNDR.
SMS / WhatsApp: Twilio SMS, Unifonic, WhatsApp Cloud.
Import each from its `sently/webhooks/<provider>` subpath.

</Accordion>

</Accordions>

## Learn more

- [Webhook events](/docs/reference/webhook-events) — `EmailEvent` / `DeliveryEvent` fields
- [Exports](/docs/reference/exports) — package entrypoints

## Next

<Cards>
  <Card title="Webhook events" description="Normalized event fields." href="/docs/reference/webhook-events" />
  <Card title="Channel send result" description="Shared accepted mapping." href="/docs/reference/channel-result" />
  <Card title="Exports" description="Public package entrypoints." href="/docs/reference/exports" />
</Cards>
