const crypto = require('crypto');
const express = require('express');
const app = express();
// Middleware to parse JSON bodies
app.use(express.json());
// Utility function to validate signature
function validateSignature(payload, signature, privateKey) {
const hmac = crypto.createHmac('sha256', privateKey);
const expectedSignature = hmac.update(JSON.stringify(payload)).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Database model for processed webhooks (example using Mongoose)
const ProcessedWebhook = mongoose.model('ProcessedWebhook', {
transaction_id: { type: String, unique: true },
processed_at: { type: Date, default: Date.now }
});
// Webhook handler
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['opm-signature'];
const payload = req.body;
const privateKey = process.env.OPM_PRIVATE_KEY;
// 1. Validate signature
if (!validateSignature(payload, signature, privateKey)) {
console.error('Invalid signature for transaction:', payload.transaction_id);
return res.status(400).send('Invalid signature');
}
// 2. Check for duplicate webhook
const existingWebhook = await ProcessedWebhook.findOne({
transaction_id: payload.transaction_id
});
if (existingWebhook) {
console.log('Duplicate webhook received:', payload.transaction_id);
return res.status(200).send('Webhook already processed');
}
// 3. Process the webhook asynchronously
res.status(200).send('Webhook received');
// 4. Business logic
await processWebhookData(payload);
// 5. Mark webhook as processed
await ProcessedWebhook.create({
transaction_id: payload.transaction_id
});
} catch (error) {
console.error('Webhook processing error:', error);
// Still return 200 to acknowledge receipt
if (!res.headersSent) {
res.status(200).send('Webhook received with errors');
}
// Store failed webhook for retry
await storeFailedWebhook(payload, error);
}
});
// Business logic processing
async function processWebhookData(payload) {
const {
transaction_id,
reference,
meta_data,
status,
amount,
buyer_email,
buyer_name
} = payload;
// Update order status
await Orders.findOneAndUpdate(
{ reference },
{
payment_status: status,
payment_confirmed: true,
transaction_id
}
);
// Process meta data
if (meta_data?.order_id) {
await fulfillOrder(meta_data.order_id);
}
// Send confirmation email
await sendPaymentConfirmation({
email: buyer_email,
name: buyer_name,
amount,
reference
});
// Additional business logic...
}
// Failed webhook storage
async function storeFailedWebhook(payload, error) {
await FailedWebhooks.create({
transaction_id: payload.transaction_id,
payload: payload,
error: error.message,
created_at: new Date(),
retry_count: 0
});
}
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});