Skip to Content
Webhooks

Webhooks

Webhooks push email, SMS, and WhatsApp events to your application as they happen. Create an endpoint in the dashboard, pick the event types it receives, and Retransmit POSTs a signed JSON payload to your URL.

Event types

EventFired when
email.sentThe email was handed to the upstream provider.
email.deliveredThe recipient’s mail server accepted it.
email.delivery_delayedDelivery was temporarily deferred.
email.openedThe recipient opened the email.
email.clickedThe recipient clicked a link.
email.bouncedThe recipient’s server permanently rejected it.
email.complainedThe recipient marked it as spam.
email.rejectedThe email was rejected before sending.
email.failedThe provider send failed.
email.unsubscribedThe recipient unsubscribed from a marketing email. data.data.addresses lists the addresses.
sms.sentThe SMS was handed to the upstream provider.
sms.deliveredThe carrier reported successful delivery.
sms.undeliveredThe carrier reported the SMS as undelivered, expired, or rejected.
sms.failedThe provider send failed after retries, or no route was available.
whatsapp.sentWhatsApp accepted the message.
whatsapp.deliveredThe message reached the recipient’s device.
whatsapp.readThe recipient opened the message.
whatsapp.failedThe send failed after retries, or WhatsApp could not deliver it.
whatsapp.receivedA recipient sent you a message. See Receive messages.

Payload

{ "id": "whd_xxxxxxxxxxxx", "type": "email.delivered", "created_at": "2026-09-01T12:00:03.000Z", "data": { "emailId": "em_xxxxxxxxxxxx", "from": "Acme <hello@yourdomain.com>", "to": ["user@example.com"], "subject": "Your receipt", "createdAt": "2026-09-01T12:00:00.000Z" } }

Some events carry extra context in data.data. email.failed, sms.failed, and whatsapp.failed include { "message": "..." } with the provider error. WhatsApp events identify the message as data.messageId (wa_...).

Delivery behavior

  • Requests are POST with Content-Type: application/json.
  • Respond with a 2xx within 10 seconds. Anything else counts as a failed delivery.
  • Failed deliveries are retried up to 8 times with exponential backoff (10s, 20s, 40s, up to 1 hour). Each retry is re-signed with a fresh timestamp. After the last retry the delivery is dropped.
  • Deliveries can arrive more than once and out of order. Use the payload id for idempotency and created_at for ordering.
  • Return 200 first, then do the work.
  • Every attempt, including failures, is visible per endpoint in the dashboard.

Verifying signatures

Every delivery is signed. Two headers accompany each request:

HeaderValue
retransmit-timestampUnix timestamp (seconds) when the delivery was signed.
retransmit-signaturev1=<hex>. HMAC-SHA256 of `${timestamp}.${body}` using your endpoint’s secret (whsec_...).

The secret is shown once, when you create the endpoint or rotate its secret in the dashboard. Rotating signs every later delivery with the new secret.

verify-webhook.ts
import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyWebhookSignature( secret: string, timestamp: string, body: string, // the raw request body, exactly as received signature: string, // the header value without the `v1=` prefix ): boolean { const expected = createHmac("sha256", secret) .update(`${timestamp}.${body}`) .digest("hex"); const a = Buffer.from(expected, "hex"); const b = Buffer.from(signature, "hex"); return a.length === b.length && timingSafeEqual(a, b); }

Example handler (Hono, but the shape is the same anywhere):

webhook-handler.ts
app.post("/webhooks/retransmit", async (c) => { const body = await c.req.text(); // raw body, do not parse first const timestamp = c.req.header("retransmit-timestamp") ?? ""; const signature = (c.req.header("retransmit-signature") ?? "").replace(/^v1=/, ""); if (!verifyWebhookSignature(process.env.RETRANSMIT_WEBHOOK_SECRET!, timestamp, body, signature)) { return c.json({ error: "invalid signature" }, 401); } const event = JSON.parse(body); // handle event.type / event.data ... return c.json({ received: true }); });
Important

Compute the HMAC over the raw request body. Parsing and re-serializing JSON first can change key order or whitespace and break verification.

Tip

Reject deliveries whose retransmit-timestamp is more than a few minutes old to prevent replays.

Last updated on