Skip to main content

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.