Verify signatures
Every webhook carries an X-VoDialer-Signature header. Check it before you trust the message.
X-VoDialer-Signature: t=1791208991,v1=9c1f2ab6…The rule
Section titled “The rule”tis the time we signed the message, in seconds since 1970. It is fresh on every try.v1is a hex HMAC-SHA256. The key is your whole signing secret,whsec_included. The signed text ist, a dot, then the exact body:<t>.<body>.- Reject the message if
tis more than 5 minutes (300 seconds) from your clock, in either direction. - Compare the signatures in constant time.
- Use the raw bytes of the body. If you parse the JSON and write it back, the bytes change and the check fails.
Do this
Section titled “Do this”- Copy the signing secret when you create the webhook. See webhooks.
- Read the raw body before any JSON parser touches it.
- Run the check below. If it fails, answer
401and do nothing else. - Answer any 2xx code as soon as you have stored the message. Do slow work afterwards.
- Press Test on the connection to send a signed
vd.test.pingand watch your check pass.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyVoDialer( header: string, secret: string, rawBody: Buffer, toleranceSeconds = 300,): boolean { let t: number | null = null; const signatures: string[] = []; for (const part of header.split(',')) { const [key, ...rest] = part.trim().split('='); const value = rest.join('='); if (key === 't' && /^\d+$/.test(value)) t = Number(value); else if (key === 'v1') signatures.push(value); } if (t === null || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest(); return signatures.some((hex) => { const given = Buffer.from(hex, 'hex'); return given.length === expected.length && timingSafeEqual(given, expected); });}With Express, take the body as bytes:
app.post('/hooks/vodialer', express.raw({ type: 'application/json' }), (req, res) => { const header = req.header('x-vodialer-signature') ?? ''; if (!verifyVoDialer(header, process.env.VODIALER_SECRET!, req.body)) return res.sendStatus(401); const event = JSON.parse(req.body.toString('utf8')); // store event.id and the X-VoDialer-Delivery header, then answer res.sendStatus(204);});Python
Section titled “Python”import hashlibimport hmacimport time
def verify_vodialer(header: str, secret: str, raw_body: bytes, tolerance: int = 300) -> bool: t = None signatures = [] for part in header.split(","): key, sep, value = part.strip().partition("=") if not sep: continue if key == "t" and value.isascii() and value.isdigit(): t = int(value) elif key == "v1": signatures.append(value) if t is None or abs(time.time() - t) > tolerance: return False
message = f"{t}.".encode() + raw_body expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected.encode(), s.lower().encode()) for s in signatures)With Flask, request.get_data() returns the raw bytes.
Rotating the secret
Section titled “Rotating the secret”Open the connection’s menu and choose Make a new signing secret (New signing secret in the older layout). The new secret is shown once. The old one stops working at once.
Handle repeats
Section titled “Handle repeats”A message can arrive more than once, for example after a timeout on your side. X-VoDialer-Delivery is the same on every retry of one message, and id in the body is the event’s id. Keep one of them and skip a message you have already handled.