Engineering•2 min read

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.

How to Verify Piisend Webhook Signatures in Production

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.

javascript
1import crypto from 'crypto';
2
3export function verifyWebhook(rawBody, signature, secret) {
4 const expected = crypto
5 .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.

python
1import hashlib
2import hmac
3import os
4
5from flask import Flask, request
6
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.

Start sending

Ship transactional email in minutes

Create an API key, verify a domain, and send your first message with Piisend.