Webhooks
Security
Every webhook delivery includes a Svix-compatible signature so you can verify it originated from eusend.
Every webhook delivery includes a signature so you can verify it originated from eusend. The signature scheme is Svix-compatible.
Headers
| Parameter | Type | Description |
|---|---|---|
webhook-id | string | Unique delivery ID for this event. |
webhook-timestamp | string | Unix timestamp (seconds) of delivery time. |
webhook-signature | string | HMAC-SHA256 signature prefixed with v1,. |
Verification
The signature is computed as
HMAC-SHA256(secret, "{webhook-id}.{webhook-timestamp}.{body}"), then
base64-encoded and prefixed with v1,. The webhook-id and webhook-timestamp
values are the ones sent in the request headers (each delivery has a unique
webhook-id).
import { createHmac, timingSafeEqual } from 'crypto'
function verifyWebhook(req, secret) {
const id = req.headers['webhook-id']
const timestamp = req.headers['webhook-timestamp']
const signature = req.headers['webhook-signature']
const body = req.rawBody // must be the raw string, not parsed JSON
const signed = `${id}.${timestamp}.${body}`
const expected = 'v1,' + createHmac('sha256', secret).update(signed).digest('base64')
const received = Buffer.from(signature ?? '', 'utf8')
const digest = Buffer.from(expected, 'utf8')
// timingSafeEqual throws on a length mismatch, so check that first — a missing
// or truncated header is exactly the case that would otherwise crash the handler.
if (received.length !== digest.length || !timingSafeEqual(received, digest)) {
throw new Error('Invalid webhook signature')
}
return JSON.parse(body)
}✦
Always compare signatures with a constant-time comparison — a plain === leaks how much of the
signature matched. In Node.js that's crypto.timingSafeEqual(), guarded by the length check
above.