Skip to content
Beta · The spec may still change before general availability. The API domain api.hensa.example is a placeholder. See changes →

Verify signatures

Anyone can send a fake POST to your webhook URL claiming “order DH-1024 is paid”. The signature is the only way to know a request really comes from Hensa and hasn’t been altered.

Hensa-Signature: t=1762156992,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t – signing time (Unix seconds).
  • v1 – HMAC-SHA256(secret, "<t>.<raw body>"), hex. For 24 hours after a secret rotation there are two v1 values.
  1. Read the request’s raw body (bytes) – before parsing JSON.
  2. Extract t and every v1 value from the Hensa-Signature header.
  3. Reject if t is more than 5 minutes away from your server clock (replay protection).
  4. Compute HMAC-SHA256(secret, t + "." + body) and compare it in constant time with each v1. One match means valid.
verify-hensa-signature.js
import crypto from 'node:crypto'
/** Returns true when the request really comes from Hensa and is fresh (≤ 300 s). */
export function verifyHensaSignature(rawBody, header, secret, toleranceSec = 300) {
const parts = (header ?? '').split(',').map((p) => p.trim().split('='))
const t = parts.find(([k]) => k === 't')?.[1]
const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => v) // 2 signatures while a secret rotates
if (!t || signatures.length === 0) return false
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false // replay protection
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return signatures.some((s) =>
s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s, 'hex'), Buffer.from(expected, 'hex'))
)
}
Symptom Cause Fix
Signature always wrong The framework parsed and re-serialised the JSON – the bytes changed Use the raw body: Express express.raw(), Laravel $request->getContent(), Next.js await req.text()
Wrong after rotating the secret Still using the old secret Deploy the new secret; both signatures are valid for 24 hours
Sometimes right, sometimes wrong Server clock drift Enable time sync (NTP)
Comparing with == Vulnerable to timing attacks Use timingSafeEqual / hash_equals / hmac.compare_digest

Dashboard › Developer › Webhooks › Rotate secret (or POST /webhook_endpoints/{id}/rotate_secret). The old secret stays valid for 24 hours; meanwhile every event carries two v1 signatures – deploy the new secret whenever you like without missing an event.