metadata.secret
. This secret is used to verify that incoming webhooks are authentic.
Each webhook request includes an X-Hook0-Signature
header containing:
t
field - the timestamph
field - list of headers included in the signaturev1
field - the signature
Implementation
Here’s an example of webhook signature verification:Node.js
Report incorrect code
Copy
Ask AI
const crypto = require('crypto');
/**
* Verify webhook signature and timestamp
* @param {string} payload - Raw request body as string
* @param {string} signatureHeader - X-Hook0-Signature header value
* @param {string} secret - Secret from metadata.secret in subscription creation
* @param {Object} headers - HTTP headers from webhook request
* @param {number} maxAgeMinutes - Max age for webhook (default: 5 minutes)
* @returns {boolean} true if webhook is authentic and within allowed time window
*/
function verifyWebhookSignature(payload, signatureHeader, secret, headers, maxAgeMinutes = 5) {
try {
// Parse signature header: t=timestamp,h=headers,v1=signature
const elements = signatureHeader.split(',');
const timestamp = elements.find(e => e.startsWith('t=')).split('=')[1];
const headerNames = elements.find(e => e.startsWith('h=')).split('=')[1];
const providedSignature = elements.find(e => e.startsWith('v1=')).split('=')[1];
// Build header values string
const headerNameList = headerNames.split(' ');
const headerValues = headerNameList.map(name => headers[name] || '').join('.');
// Build signed payload
const signedPayload = `${timestamp}.${headerNames}.${headerValues}.${payload}`;
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('hex');
// Compare signatures securely
const signaturesMatch = crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(providedSignature, 'hex')
);
// Verify timestamp to prevent replay attacks
const webhookTime = parseInt(timestamp) * 1000; // Convert to milliseconds
const currentTime = Date.now();
const ageMinutes = (currentTime - webhookTime) / (1000 * 60);
if (ageMinutes > maxAgeMinutes) {
console.error(`Webhook timestamp exceeds maximum age: ${ageMinutes.toFixed(1)} minutes > ${maxAgeMinutes} minutes`);
return false;
}
return signaturesMatch;
} catch (error) {
console.error('Webhook verification error:', error);
return false;
}
}
Node.js
Report incorrect code
Copy
Ask AI
const express = require("express");
const app = express();
// Important: Get raw body for signature verification
app.use(express.raw({ type: "application/json" }));
app.post("/webhook", (req, res) => {
const payload = req.body.toString(); // Raw string needed for signature
const signature = req.headers["x-hook0-signature"];
const secret = process.env.WEBHOOK_SECRET; // Store securely from metadata.secret
if (verifyWebhookSignature(payload, signature, secret, req.headers)) {
console.log("✅ Authentic webhook");
// Parse and process the event
const event = JSON.parse(payload);
// Handle your transaction event here
res.status(200).send("OK");
} else {
console.log("❌ Invalid webhook - rejected");
res.status(400).send("Invalid signature");
}
});