Webhooks Integration
When you receive webhook notifications from our API, it's crucial to verify the authenticity of the messages to ensure data integrity and prevent unauthorized access. We use HMAC (Hash-based Message Authentication Code) to generate a signature for each webhook payload, which you can verify using a shared secret and timestamp provided in the request headers.
Verifying the HMAC Signature
To verify the HMAC signature included in the webhook payload, follow these steps:
- Retrieve the
PayNow-Signature
header from the webhook request, containing the HMAC of the entire payload body and the timestamp. - Extract the Unix millisecond timestamp from the
PayNow-Timestamp
header. - Create a string by combining the timestamp from the header, followed by a dot (".") and then the actual payload string from the webhook request. Then create an HMAC signature using the resulting string and your signing secret.
- Compare the calculated signature with the signature provided in the
PayNow-Signature
header.
When comparing HMAC signatures, avoid using simple string comparison methods, as they may be susceptible to timing attacks. Instead, use constant-time comparison techniques to compare the calculated signature with the provided signature, ensuring that the comparison takes the same amount of time regardless of the comparison result.
Preventing Replay Attacks
To prevent replay attacks, ensure that you check the timestamp included in the request and ignore webhook notifications with older timestamps. Here's how to do it:
Compare the timestamp provided in the PayNow-Timestamp
header with the current time.
If the timestamp is significantly older than the current time (e.g., more than a few seconds or minutes), consider the request invalid and discard it.
We recommend ignoring webhook requests older than 5 minutes. Use Network Time Protocol (NTP) to make sure that your server's clock is accurate.
You may also store the event_id
in your database and ignore any duplicate requests with the same ID.
This is useful for ensuring that your server does not handle the same webhook twice.
Integration Implementation Examples
const express = require('express');
const crypto = require('crypto');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 8080;
const SIGNING_SECRET = process.env.SIGNING_SECRET;
const TOLERANCE_PERIOD = 5 * 60 * 1000; // 5 minutes in milliseconds
// Middleware to preserve raw body for signature verification
const rawBodySaver = function (req, res, buf, encoding) {
if (buf && buf.length) {
req.rawBody = buf.toString(encoding || 'utf8');
}
}
// Function to calculate HMAC signature
function calculateHMAC(payloadWithTimestamp) {
const hmac = crypto.createHmac('sha256', SIGNING_SECRET);
hmac.update(payloadWithTimestamp);
return hmac.digest('base64');
}
// Middleware to verify webhook signature and prevent replay attacks
function verifyWebhookSignature(req, res, next) {
const timestamp = req.headers['paynow-timestamp'];
const providedSignature = req.headers['paynow-signature'];
if (!timestamp || !providedSignature) {
return res.status(400).send('Missing required headers');
}
const timestampInt = parseInt(timestamp, 10);
if (isNaN(timestampInt)) {
return res.status(400).send('Invalid timestamp format');
}
const timestampTime = new Date(timestampInt);
const currentTime = new Date();
if (currentTime - timestampTime > TOLERANCE_PERIOD) {
return res.status(401).send('Timestamp out of tolerance');
}
const payloadWithTimestamp = `${timestamp}.${req.rawBody}`;
const expectedSignature = calculateHMAC(payloadWithTimestamp);
const signatureValid = crypto.timingSafeEqual(Buffer.from(providedSignature, 'base64'), Buffer.from(expectedSignature, 'base64'));
if (!signatureValid) {
return res.status(401).send('Invalid signature');
}
next();
}
app.use(express.json({ verify: rawBodySaver }));
app.post('/webhook', verifyWebhookSignature, (req, res) => {
const eventType = req.body.event_type;
switch (eventType) {
case 'ON_DELIVERY_ITEM_ADDED':
// handleOnDeliveryItemAdded(req.body);
break;
case 'ON_DELIVERY_ITEM_ACTIVATED':
// handleOnDeliveryItemActivated(req.body);
break;
case 'ON_DELIVERY_ITEM_USED':
// handleOnDeliveryItemUsed(req.body);
break;
case 'ON_DELIVERY_ITEM_REVOKED':
// handleOnDeliveryItemRevoked(req.body);
break;
default:
console.log(`Received unknown event type: ${eventType}`);
}
res.status(200).send('Webhook processed');
});
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
});