# Sent Webhooks

[Sent](https://sent.dm) is a multi-channel messaging API (SMS, WhatsApp, RCS, voice). When a message changes status, a contact replies, a template is reviewed, a sender channel changes, a contact's consent changes, or a tracked link or call produces an event, Sent POSTs one signed JSON event to every webhook subscribed to it. Sender Profiles can receive clones of your webhook.

Sent signs "Svix-style", which is the Standard Webhooks scheme, but the headers are named `X-Webhook-*` instead of `webhook-*` (or Svix's `svix-*`).

## When to Use This Skill

* How do I receive Sent (sent.dm) webhooks?
* How do I verify a Sent `X-Webhook-Signature`?
* Where do I find my Sent webhook signing secret (`whsec_...`)?
* How do I handle `message.delivered`, `message.failed` or `message.received` events?
* Why does Sent webhook signature verification fail?
* How do I dedupe Sent webhook retries (`X-Webhook-Event-ID`)?

## Verification (core)

| Header | Value |
| --- | --- |
| `X-Webhook-ID` | UUID of the **webhook configuration**. Constant across events, signed, but not a dedupe key |
| `X-Webhook-Timestamp` | Unix seconds when this attempt was signed (changes on every retry) |
| `X-Webhook-Signature` | One or more space-separated `v1,<base64 HMAC-SHA256>` entries |
| `X-Webhook-Event-ID` | The event's own UUID, identical on every attempt. Dedupe on this |

Signed content is `{X-Webhook-ID}.{X-Webhook-Timestamp}.{raw body}`, HMAC-SHA256, base64, prefixed `v1,`. The key is the base64-**decoded** part of the secret after `whsec_`. The Sent SDKs do not verify inbound webhooks, so use the `standardwebhooks` library (npm and PyPI) and map Sent's headers onto the names it reads. Do not use the `svix` library, which reads `svix-*` headers.

```javascript
const { Webhook } = require('standardwebhooks');

// rawBody: exact request body string, NOT re-serialised JSON
function verifySentWebhook(rawBody, headers, secret = process.env.SENT_WEBHOOK_SECRET) {
  return new Webhook(secret).verify(rawBody, {
    'webhook-id': headers['x-webhook-id'],
    'webhook-timestamp': headers['x-webhook-timestamp'],
    'webhook-signature': headers['x-webhook-signature'],
  }); // returns the parsed event; throws → respond 401
}

```

Python: `from standardwebhooks.webhooks import Webhook`, then `Webhook(secret).verify(await request.body(), {"webhook-id": ..., "webhook-timestamp": ..., "webhook-signature": ...})` with the same header mapping. In Express, read the body with `express.raw({ type: '*/*' })` so any content type verifies.

**Timestamp window.** The library always rejects timestamps more than 5 minutes off. Sent signs each retry when it sends it, so Sent's own retries pass. Hookdeck retries and replays resend the **original** timestamp (and Hookdeck applies no window itself), so the library rejects them once they are over 5 minutes old. If you receive through Hookdeck and need late retries to pass, use the dependency-free verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/sent-dm-webhooks/references/verification.md) and widen or disable its window.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/sent-dm-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/sent-dm-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/sent-dm-webhooks/examples/fastapi/).

## Event Envelope

```json
{ "field": "message", "event": "message.delivered", "timestamp": "2025-10-31T10:10:42Z",
  "payload": { "message_id": "8ba7b830-...", "outbound_number": "+1987654321",
               "message_status": "DELIVERED", "channel": "sms", "body": "Hi Jane, ..." } }

```

Dispatch on the body's `event` field (`field` is the parent family). The `X-Webhook-Event-Type` header carries the same name. Some events also carry a `request_id`.

## Common Event Types

| Event | Fired when |
| --- | --- |
| `message.delivered` | The message reached the recipient's device |
| `message.failed` | The message failed (`payload.reason_code`, `payload.reason`) |
| `message.received` | A contact sent an inbound message (`payload.text`, `payload.inbound_number`) |
| `templates.approved` / `templates.rejected` | A template review finished |
| `contact.opt_out` | A contact opted out (for example the `STOP` keyword) |
| `channel.activated` | A sender channel registration became active |

Sent documents 38 event types in six families: `message.*` (queued, routed, sent, delivered, read, failed, scheduled, cancelled, filtered, blocked, received), `templates.*`, `channel.*`, `contact.*`, `link.*` and `call.*`. Handle the ones you need, log the rest and return `2xx`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/sent-dm-webhooks/references/overview.md).

## Delivery Semantics

* Return `2xx` within the webhook's timeout (5-120 s, default 30 s). Non-2xx, timeouts and connection failures are retried `retry_count` times (1-5, default 3), starting about a minute later and doubling up to 60 minutes apart.
* **10 consecutive failed events disable the webhook.** Acknowledge fast and process asynchronously.
* Return `2xx` even for event types you don't handle.
* Response contract: `400` missing signature headers, `401` bad signature or stale timestamp, `503` no signing secret configured.
* Delivery is at-least-once. Dedupe on `X-Webhook-Event-ID`.

## Environment Variables

```bash
SENT_WEBHOOK_SECRET=whsec_xxxxx   # the webhook's signing secret; handlers fail closed (503) without it

```

The create-webhook response returns the secret, and the dashboard shows it (Eye / Copy button) on the webhook's detail view.

## Local Development

```bash
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 sent-dm --path /webhooks/sent-dm

```

Register the printed URL as the endpoint of a Sent webhook, then use **Test Webhook** in the dashboard to send a sample event.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/sent-dm-webhooks/references/overview.md) - Event families, payload fields, headers, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/sent-dm-webhooks/references/setup.md) - Create a webhook in the dashboard or API, signing secret, testing
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/sent-dm-webhooks/references/verification.md) - Library and manual verification, gotchas, debugging