Agent skill
Sent Webhooks Skill
Receive and verify Sent (sent.dm) webhooks, a multi-channel messaging API (SMS, WhatsApp, RCS, voice) that signs deliveries Standard Webhooks / Svix style on X-Webhook-* headers. Use when setting up a Sent webhook endpoint, getting the whsec_ signing secret, debugging X-Webhook-Signature verification, deduping on X-Webhook-Event-ID, or handling events like message.delivered, message.failed, message.received, templates.approved and contact.opt_out.
Install this skill
npx skills add hookdeck/webhook-skills --skill sent-dm-webhooks
Sent 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.failedormessage.receivedevents? - 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.
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 and widen or disable its window.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Envelope
{ "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.
Delivery Semantics
- Return
2xxwithin the webhook's timeout (5-120 s, default 30 s). Non-2xx, timeouts and connection failures are retriedretry_counttimes (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
2xxeven for event types you don't handle. - Response contract:
400missing signature headers,401bad signature or stale timestamp,503no signing secret configured. - Delivery is at-least-once. Dedupe on
X-Webhook-Event-ID.
Environment Variables
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
# 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 - Event families, payload fields, headers, retries
- references/setup.md - Create a webhook in the dashboard or API, signing secret, testing
- references/verification.md - Library and manual verification, gotchas, debugging