Docs βΊ Webhooks
Verifying signatures
dragonfly-signature / Svix-compatible verification.
Every message is signed so you can prove it came from Dragonfly and wasn't altered or replayed.
Headers
| Header | Value |
|---|---|
dragonfly-id | Unique message id (msg_β¦). Use it to deduplicate. |
dragonfly-timestamp | Unix seconds when the message was sent. |
dragonfly-signature | v1,<base64 HMAC-SHA256>, possibly several space-separated during secret rotation. |
The same three values are also sent as svix-id, svix-timestamp and svix-signature. A verifier built for Nash (the Svix libraries) works unchanged.
Algorithm
- Build the signed content:
{dragonfly-id}.{dragonfly-timestamp}.{raw request body}. - Take your endpoint secret, strip the
whsec_prefix and base64-decode the rest. That is the HMAC key. - Compute
base64(HMAC_SHA256(key, signedContent)). - Compare against each
v1,-prefixed signature in the header, using a constant-time comparison. - Reject the message if the timestamp is more than 5 minutes from your clock.
Node.js
javascript
import crypto from 'node:crypto'
export function verifyDragonfly(rawBody, headers, secret) {
const id = headers['dragonfly-id'], ts = headers['dragonfly-timestamp'], sigHeader = headers['dragonfly-signature'] ?? ''
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest('base64')
return sigHeader.split(' ').some((s) => {
const [ver, sig] = s.split(',')
return ver === 'v1' && sig && sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
})
}Python
python
import base64, hashlib, hmac, time
def verify_dragonfly(raw_body: bytes, headers: dict, secret: str) -> bool:
msg_id, ts, sig_header = headers["dragonfly-id"], headers["dragonfly-timestamp"], headers.get("dragonfly-signature", "")
if abs(time.time() - int(ts)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
expected = base64.b64encode(hmac.new(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256).digest()).decode()
return any(part.split(",", 1)[0] == "v1" and hmac.compare_digest(part.split(",", 1)[1], expected)
for part in sig_header.split() if "," in part)Using the Svix library
javascript
import { Webhook } from 'svix'
const wh = new Webhook(process.env.DRAGONFLY_WEBHOOK_SECRET)
const event = wh.verify(rawBody, { 'svix-id': req.headers['svix-id'], 'svix-timestamp': req.headers['svix-timestamp'], 'svix-signature': req.headers['svix-signature'] })Important: verify against the raw body bytes. Parsing and re-serializing the JSON first changes the bytes and breaks the signature.
Something unclear or missing? Tell us.