eusend
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

ParameterTypeDescription
webhook-idstringUnique delivery ID for this event.
webhook-timestampstringUnix timestamp (seconds) of delivery time.
webhook-signaturestringHMAC-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).

node.js verification
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.