PactbasePactbase DocsDocs
Webhooks

Verify a webhook

Check the signature, ignore repeats, and answer fast.

  1. Read the raw body. Do not parse and re-serialise it before checking the signature.
  2. Check the signature, and that the timestamp is within 5 minutes. This stops replays.
  3. Ignore a webhook-id you have already processed.
  4. Answer 2xx within 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.

On this page