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.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)

HeaderValue
X-Webhook-IDUUID of the webhook configuration. Constant across events, signed, but not a dedupe key
X-Webhook-TimestampUnix seconds when this attempt was signed (changes on every retry)
X-Webhook-SignatureOne or more space-separated v1,<base64 HMAC-SHA256> entries
X-Webhook-Event-IDThe 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

EventFired when
message.deliveredThe message reached the recipient's device
message.failedThe message failed (payload.reason_code, payload.reason)
message.receivedA contact sent an inbound message (payload.text, payload.inbound_number)
templates.approved / templates.rejectedA template review finished
contact.opt_outA contact opted out (for example the STOP keyword)
channel.activatedA 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 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

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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →