Skip to content
Developers

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…

  • t is when it was signed, in Unix seconds.
  • v1 is the HMAC-SHA256, in hex, of t, 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.