Signature Verification
Verify webhook signatures with Node.js, Python, and curl examples.
How it works
Every webhook delivery is signed with HMAC-SHA256 using your webhook secret. The signature is computed over the string {timestamp}.{raw JSON body}.
Verification steps
- Extract the
X-Webhook-SignatureandX-Webhook-Timestampheaders - Concatenate:
"{timestamp}.{raw request body}" - Compute HMAC-SHA256 with your webhook secret
- Compare the computed signature with the header value
- Optionally check that the timestamp is within 5 minutes to prevent replay attacks
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(secret, payload, timestamp, signature) {
const data = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(data)
.digest('hex');
// Timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}
// Express middleware example
app.post('/webhooks/mexicop2p', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const payload = req.body.toString();
// Check timestamp is within 5 minutes
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return res.status(400).send('Timestamp too old');
}
if (!verifyWebhookSignature(process.env.WEBHOOK_SECRET, payload, timestamp, signature)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
console.log(`Received ${event.event} for order ${event.data.orderId}`);
// Process asynchronously
processWebhook(event);
res.status(200).send('ok');
});Python
import hashlib
import hmac
import time
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"
def verify_webhook_signature(secret, payload, timestamp, signature):
data = f"{timestamp}.{payload}"
expected = hmac.new(
secret.encode('utf-8'),
data.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhooks/mexicop2p', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature')
timestamp = request.headers.get('X-Webhook-Timestamp')
payload = request.get_data(as_text=True)
# Check timestamp is within 5 minutes
if abs(time.time() - int(timestamp)) > 300:
abort(400, 'Timestamp too old')
if not verify_webhook_signature(WEBHOOK_SECRET, payload, timestamp, signature):
abort(401, 'Invalid signature')
event = request.get_json()
print(f"Received {event['event']} for order {event['data']['orderId']}")
return 'ok', 200Testing with curl
Generate a test signature and send a webhook to your local server:
# Set your webhook secret
SECRET="your_webhook_secret"
TIMESTAMP=$(date +%s)
PAYLOAD='{"event":"order.completed","timestamp":"2025-06-15T12:35:00.000Z","data":{"orderId":"ord_test","status":"COMPLETED"}}'
# Compute HMAC-SHA256
SIGNATURE=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
# Send test webhook
curl -X POST http://localhost:3000/webhooks/mexicop2p \
-H "Content-Type: application/json" \
-H "X-Webhook-Id: del_test_001" \
-H "X-Webhook-Signature: $SIGNATURE" \
-H "X-Webhook-Timestamp: $TIMESTAMP" \
-d "$PAYLOAD"Security notes
- Store your webhook secret securely (environment variable, secrets manager)
- Always verify signatures before processing payloads
- Use the raw request body for signature verification (not parsed JSON)
- Implement timestamp checking to prevent replay attacks
- Use timing-safe comparison functions to prevent timing attacks