Webhooks
Verifying webhooks
Check the signature so you only trust events from Falconzo.
Every event is signed with your endpoint’s signing secret (it starts with whsec_; find it on the endpoint’s page under Webhooks). Checking the signature proves the event came from Falconzo and wasn’t changed on the way.
The signature header
Falconzo-Signature: t=1762596904,v1=5f2b7c…
tis when it was signed, in Unix seconds.v1is the HMAC-SHA256, in hex, oft, a full stop, and the raw request body, made with your signing secret.
Check it
- Use the raw body exactly as received, before any JSON parsing.
- Compare with a constant-time comparison.
- Refuse events signed more than five minutes ago, so an old event can’t be replayed.
import crypto from 'node:crypto'
import express from 'express'
const app = express()
// The raw body: the signature covers the exact bytes sent.
app.post('/falconzo-events', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('Falconzo-Signature') ?? ''
const { t, v1 } = Object.fromEntries(header.split(',').map((part) => part.split('=')))
const expected = crypto
.createHmac('sha256', process.env.FALCONZO_WEBHOOK_SECRET)
.update(`${t}.${req.body}`)
.digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
const valid =
typeof v1 === 'string' &&
v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
if (!fresh || !valid) return res.status(400).end()
const event = JSON.parse(req.body)
// Save event.id to ignore repeats, then handle event.data.message
res.sendStatus(200)
})Changing the secret
“New secret” on the endpoint’s page replaces it at once: events from then on are signed with the new one. Update your server straight after.
