How to Verify Piisend Webhook Signatures in Production
Learn to secure your event-driven workflows by verifying Piisend webhook signatures. This guide covers HMAC verification, replay protection, and secret rotation with practical Node.js and Python examples.
Section 1
Securing Your Event-Driven Workflows with Piisend Webhooks
Piisend webhooks provide real-time notifications for critical delivery events like bounces, opens, and clicks, enabling robust event-driven workflows. To ensure the integrity and authenticity of these incoming notifications, verifying the webhook's signature is crucial. This prevents malicious actors from injecting fake events into your system, safeguarding your application's logic and data.
Section 2
Understanding HMAC Signature Verification
Piisend signs every webhook payload with HMAC-SHA256. The hex digest is sent in X-Webhook-Signature. Your endpoint must recompute that digest from the raw body and your signing secret, then compare it in constant time. A mismatch means the payload was tampered with or did not come from Piisend.
Section 3
Implementing Verification in Node.js
In Node.js, read the raw request body before JSON parsing. Recompute HMAC-SHA256 with your webhook secret and compare it to X-Webhook-Signature using a constant-time check. Reject mismatched signatures before you trust bounce or delivery events.
1import crypto from 'crypto';2 3export function verifyWebhook(rawBody, signature, secret) {4 const expected = crypto5 .createHmac('sha256', secret)6 .update(rawBody)7 .digest('hex');8 9 const a = Buffer.from(String(signature));10 const b = Buffer.from(expected);11 if (a.length !== b.length) return false;12 return crypto.timingSafeEqual(a, b);13}14 15app.post('/webhooks/piisend', express.raw({ type: 'application/json' }), (req, res) => {16 const signature = req.get('X-Webhook-Signature');17 const timestamp = req.get('X-Webhook-Timestamp');18 if (!verifyWebhook(req.body, signature, process.env.PIISEND_WEBHOOK_SECRET)) {19 return res.status(401).send('invalid signature');20 }21 22 const event = JSON.parse(req.body.toString('utf8'));23 console.log(event.type, timestamp);24 res.status(200).end();25});Section 4
Implementing Verification in Python
Python apps can verify the same HMAC with hmac.compare_digest. Always hash the raw bytes of the POST body, not a re-serialized dict. Check X-Webhook-Timestamp and reject deliveries that are too old to reduce replay risk.
1import hashlib2import hmac3import os4 5from flask import Flask, request6 7app = Flask(__name__)8 9def verify_webhook(body: bytes, signature: str, secret: str) -> bool:10 expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()11 return hmac.compare_digest(expected, signature or "")12 13@app.post("/webhooks/piisend")14def piisend_webhook():15 secret = os.environ["PIISEND_WEBHOOK_SECRET"]16 if not verify_webhook(17 request.get_data(),18 request.headers.get("X-Webhook-Signature", ""),19 secret,20 ):21 return ("invalid signature", 401)22 23 event = request.get_json(force=True)24 print(event["type"], request.headers.get("X-Webhook-Timestamp"))25 return ("", 200)Section 5
Protecting Against Replay Attacks and Secret Rotation
Beyond signature verification, consider replay protection by checking a timestamp within the webhook payload and rejecting requests that are too old. For enhanced security, regularly rotate your webhook secrets. Piisend allows you to configure multiple active secrets, enabling a smooth transition without downtime. This multi-layered approach strengthens the security of your event-driven workflows.