Webhooks
Verify a webhook
Check the signature, ignore repeats, and answer fast.
- Read the raw body. Do not parse and re-serialise it before checking the signature.
- Check the signature, and that the timestamp is within 5 minutes. This stops replays.
- Ignore a
webhook-idyou have already processed. - Answer
2xxwithin 10 seconds, and do slow work afterwards.
The signature is v1, followed by the base64 HMAC-SHA256 of webhook-id, a dot, webhook-timestamp, a dot and the raw body, keyed with the secret's bytes (the base64 after whsec_).
Node (Express)
// npm install standardwebhooks
import { Webhook } from "standardwebhooks"
const wh = new Webhook(process.env.PACTBASE_WEBHOOK_SECRET.replace("whsec_", ""))
app.post("/webhooks/pactbase", express.raw({ type: "application/json" }), (req, res) => {
let event
try {
event = wh.verify(req.body, req.headers) // throws on a bad signature or old timestamp
} catch {
return res.status(400).send("bad signature")
}
if (event.type === "consent.withdrawn") {
stopProcessing(event.data.data_principal.id, event.data.summary.withdrawn)
}
res.sendStatus(204)
})Python
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, body: bytes) -> bool:
msg_id, timestamp = headers["webhook-id"], headers["webhook-timestamp"]
if abs(time.time() - int(timestamp)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + body
expected = "v1," + base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
return any(hmac.compare_digest(expected, s) for s in headers["webhook-signature"].split(" "))Testing
Click Send test event on the endpoint card. The result shows the HTTP status and time your receiver answered with. The Deliveries list shows the exact body sent, so you can compare it with what your code received.
To receive webhooks in n8n without writing code, see n8n.