Securely Receive and Verify Webhooks
Learn how to securely receive and verify webhook events from NextAPI to ensure real-time updates for your payment processing system.
Overview
Webhooks are essential for:
- Real-time payment status updates
- Transaction completion notifications
- Wallet balance changes
- Fraud alerts and security events
- Automated workflow triggers
Webhook Security Architecture
NextAPI → Webhook Event → Signature Verification → Event Processing → Business Logic
Getting Started
Prerequisites
- NextAPI API keys
- Webhook endpoint URL (publicly accessible)
- Understanding of HTTP servers and event handling
- SSL certificate for your endpoint
Implementation Steps
1. Set Up Webhook Endpoint
const express = require('express');
const crypto = require('crypto');
const bodyParser = require('body-parser');
const app = express();
// Use raw body parser for signature verification
app.use('/webhooks/nextpay', bodyParser.raw({ type: 'application/json' }));
app.post('/webhooks/nextpay', (req, res) => {
try {
// Verify webhook signature
const signature = req.headers['x-nextpay-signature'];
const isValid = verifyWebhookSignature(req.body, signature);
if (!isValid) {
console.error('Invalid webhook signature');
return res.status(401).send('Unauthorized');
}
// Parse event data
const event = JSON.parse(req.body);
// Process event
processWebhookEvent(event);
// Acknowledge receipt
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing error:', error);
res.status(400).send('Bad Request');
}
});
function verifyWebhookSignature(payload, signature) {
const webhookSecret = 'YOUR_WEBHOOK_SECRET';
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
2. Event Processing Logic
class WebhookProcessor {
constructor() {
this.eventHandlers = {
'payment.completed': this.handlePaymentCompleted.bind(this),
'payment.failed': this.handlePaymentFailed.bind(this),
'wallet.credited': this.handleWalletCredited.bind(this),
'wallet.debited': this.handleWalletDebited.bind(this),
'payout.batch.completed': this.handlePayoutBatchCompleted.bind(this),
'fraud.alert': this.handleFraudAlert.bind(this)
};
}
async processWebhookEvent(event) {
console.log(`Processing webhook event: ${event.type}`);
try {
// Validate event structure
this.validateEvent(event);
// Get appropriate handler
const handler = this.eventHandlers[event.type];
if (!handler) {
console.warn(`No handler for event type: ${event.type}`);
return;
}
// Process event
await handler(event.data);
// Log successful processing
await this.logEventProcessing(event, 'SUCCESS');
} catch (error) {
console.error(`Failed to process event ${event.type}:`, error);
await this.logEventProcessing(event, 'FAILED', error);
throw error;
}
}
validateEvent(event) {
if (!event.type) {
throw new Error('Event type is required');
}
if (!event.data) {
throw new Error('Event data is required');
}
if (!event.timestamp) {
throw new Error('Event timestamp is required');
}
// Check for replay attacks (events older than 5 minutes)
const eventTime = new Date(event.timestamp);
const now = new Date();
const timeDiff = (now - eventTime) / 1000; // seconds
if (timeDiff > 300) { // 5 minutes
throw new Error('Event timestamp is too old');
}
}
async handlePaymentCompleted(data) {
console.log(`Payment completed: ${data.transactionId}`);
// Update order status
await this.updateOrderStatus(data.reference, 'PAID');
// Send confirmation to customer
await this.sendPaymentConfirmation(data.customerId, data);
// Update inventory
await this.updateInventory(data.items);
// Record in analytics
await this.recordSale(data);
}
async handlePaymentFailed(data) {
console.log(`Payment failed: ${data.transactionId}`);
// Update order status
await this.updateOrderStatus(data.reference, 'PAYMENT_FAILED');
// Notify customer
await this.sendPaymentFailureNotification(data.customerId, data);
// Retry logic if applicable
if (data.retryable) {
await this.schedulePaymentRetry(data);
}
}
async handleWalletCredited(data) {
console.log(`Wallet credited: ${data.walletId} - ${data.amount}`);
// Update wallet balance
await this.updateWalletBalance(data.walletId, data.amount);
// Send notification to user
await this.sendWalletCreditNotification(data.userId, data);
// Trigger any pending withdrawals
await this.processPendingWithdrawals(data.userId);
}
async handleWalletDebited(data) {
console.log(`Wallet debited: ${data.walletId} - ${data.amount}`);
// Update wallet balance
await this.updateWalletBalance(data.walletId, -data.amount);
// Send notification to user
await this.sendWalletDebitNotification(data.userId, data);
}
async handlePayoutBatchCompleted(data) {
console.log(`Payout batch completed: ${data.batchId}`);
// Update batch status
await this.updateBatchStatus(data.batchId, 'COMPLETED');
// Generate payroll report
const report = await this.generatePayrollReport(data);
// Send notification to finance team
await this.sendPayrollCompletionNotification(report);
}
async handleFraudAlert(data) {
console.warn(`Fraud alert: ${data.alertType} - ${data.userId}`);
// Flag user account
await this.flagUserAccount(data.userId, data.riskLevel);
// Block suspicious transactions
await this.blockPendingTransactions(data.userId);
// Notify security team
await this.sendSecurityAlert(data);
// Schedule review
await this.scheduleAccountReview(data.userId);
}
// Helper methods
async updateOrderStatus(reference, status) {
// Implement your order status update logic
console.log(`Updating order ${reference} to status: ${status}`);
}
async sendPaymentConfirmation(customerId, data) {
// Implement payment confirmation logic
console.log(`Sending payment confirmation to customer ${customerId}`);
}
async updateWalletBalance(walletId, amount) {
// Implement wallet balance update logic
console.log(`Updating wallet ${walletId} balance by ${amount}`);
}
async logEventProcessing(event, status, error = null) {
const logEntry = {
eventId: event.id,
eventType: event.type,
status: status,
processedAt: new Date().toISOString(),
error: error ? error.message : null
};
// Store in your logging system
console.log('Webhook event logged:', logEntry);
}
}
Advanced Webhook Features
1. Event Deduplication
class DeduplicatingWebhookProcessor extends WebhookProcessor {
constructor() {
super();
this.processedEvents = new Set();
}
async processWebhookEvent(event) {
// Check if event was already processed
if (this.processedEvents.has(event.id)) {
console.log(`Event ${event.id} already processed, skipping`);
return;
}
// Process event normally
await super.processWebhookEvent(event);
// Mark as processed
this.processedEvents.add(event.id);
// Clean up old events (keep last 10000)
if (this.processedEvents.size > 10000) {
const eventsArray = Array.from(this.processedEvents);
this.processedEvents = new Set(eventsArray.slice(-5000));
}
}
}
2. Retry Mechanism
class RetryableWebhookProcessor extends WebhookProcessor {
constructor(maxRetries = 3, retryDelay = 1000) {
super();
this.maxRetries = maxRetries;
this.retryDelay = retryDelay;
this.retryQueue = new Map();
}
async processWebhookEvent(event) {
try {
await super.processWebhookEvent(event);
} catch (error) {
const retryCount = this.retryQueue.get(event.id) || 0;
if (retryCount < this.maxRetries) {
console.log(`Retrying event ${event.id}, attempt ${retryCount + 1}`);
this.retryQueue.set(event.id, retryCount + 1);
// Schedule retry with exponential backoff
setTimeout(async () => {
try {
await super.processWebhookEvent(event);
this.retryQueue.delete(event.id);
} catch (retryError) {
console.error(`Retry failed for event ${event.id}:`, retryError);
}
}, this.retryDelay * Math.pow(2, retryCount));
} else {
console.error(`Max retries exceeded for event ${event.id}`);
await this.handlePermanentFailure(event, error);
}
}
}
async handlePermanentFailure(event, error) {
// Store failed event for manual review
await this.storeFailedEvent(event, error);
// Send alert to monitoring system
await this.sendFailureAlert(event, error);
}
}
3. Webhook Monitoring
class WebhookMonitor {
constructor() {
this.metrics = {
eventsReceived: 0,
eventsProcessed: 0,
eventsFailed: 0,
averageProcessingTime: 0
};
this.processingTimes = [];
}
recordEventReceived() {
this.metrics.eventsReceived++;
}
recordEventProcessed(processingTime) {
this.metrics.eventsProcessed++;
this.processingTimes.push(processingTime);
// Keep only last 100 processing times for average
if (this.processingTimes.length > 100) {
this.processingTimes = this.processingTimes.slice(-100);
}
this.metrics.averageProcessingTime =
this.processingTimes.reduce((sum, time) => sum + time, 0) / this.processingTimes.length;
}
recordEventFailed() {
this.metrics.eventsFailed++;
}
getHealthStatus() {
const successRate = this.metrics.eventsReceived > 0
? (this.metrics.eventsProcessed / this.metrics.eventsReceived) * 100
: 0;
return {
status: successRate > 95 ? 'HEALTHY' : successRate > 80 ? 'WARNING' : 'CRITICAL',
successRate: successRate.toFixed(2) + '%',
averageProcessingTime: this.metrics.averageProcessingTime.toFixed(2) + 'ms',
totalEvents: this.metrics.eventsReceived
};
}
}
Security Best Practices
1. HTTPS Only
// Force HTTPS for webhook endpoint
app.use('/webhooks/nextpay', (req, res, next) => {
if (req.protocol !== 'https') {
return res.status(403).send('HTTPS required');
}
next();
});
2. Rate Limiting
const rateLimit = require('express-rate-limit');
const webhookRateLimit = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100, // Limit each IP to 100 requests per minute
message: 'Too many webhook requests from this IP'
});
app.use('/webhooks/nextpay', webhookRateLimit);
3. IP Whitelisting
const allowedIPs = [
'192.168.1.100', // NextAPI webhook server IP
'192.168.1.101'
];
app.use('/webhooks/nextpay', (req, res, next) => {
const clientIP = req.ip || req.connection.remoteAddress;
if (!allowedIPs.includes(clientIP)) {
return res.status(403).send('IP not allowed');
}
next();
});
Testing Your Webhooks
1. Local Development with Ngrok
// Use ngrok to expose local server to internet
const ngrok = require('ngrok');
async function startWebhookServer() {
// Start your Express server
const server = app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
// Start ngrok
const url = await ngrok.connect(3000);
console.log(`Public webhook URL: ${url}/webhooks/nextpay`);
// Register webhook with NextAPI
await registerWebhook(`${url}/webhooks/nextpay`);
return { server, url };
}
2. Webhook Testing Suite
class WebhookTester {
constructor(webhookUrl) {
this.webhookUrl = webhookUrl;
}
async testPaymentCompleted() {
const testEvent = {
id: 'test_event_001',
type: 'payment.completed',
timestamp: new Date().toISOString(),
data: {
transactionId: 'test_tx_001',
reference: 'test_order_001',
amount: 1000,
customerId: 'test_customer_001',
status: 'COMPLETED'
}
};
return await this.sendTestEvent(testEvent);
}
async testPaymentFailed() {
const testEvent = {
id: 'test_event_002',
type: 'payment.failed',
timestamp: new Date().toISOString(),
data: {
transactionId: 'test_tx_002',
reference: 'test_order_002',
amount: 1000,
customerId: 'test_customer_002',
status: 'FAILED',
error: 'Insufficient funds'
}
};
return await this.sendTestEvent(testEvent);
}
async sendTestEvent(event) {
const webhookSecret = 'YOUR_WEBHOOK_SECRET';
const payload = JSON.stringify(event);
const signature = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
const response = await fetch(this.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-NextPay-Signature': signature
},
body: payload
});
return {
status: response.status,
success: response.ok
};
}
}
Deployment Considerations
1. Production Setup
// Production webhook server with clustering
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
// Fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork(); // Restart worker
});
} else {
// Workers can share any TCP connection
const server = app.listen(3000, () => {
console.log(`Worker ${process.pid} started`);
});
}
2. Health Check Endpoint
app.get('/health', (req, res) => {
const monitor = new WebhookMonitor();
const health = monitor.getHealthStatus();
res.status(health.status === 'HEALTHY' ? 200 : 503).json(health);
});
Conclusion
Implementing secure webhook processing ensures:
- ✅ Real-time event processing
- ✅ Secure signature verification
- ✅ Reliable event handling with retries
- ✅ Comprehensive monitoring and alerting
- ✅ Protection against replay attacks
- ✅ Scalable production-ready setup
Ready to implement webhooks? Start with our basic integration guide and add webhook processing for real-time updates.