Implementation Guide
A concise guide to implementing webhooks, including setup, testing, and monitoring.
Webhook Server Setup (Express.js)
const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// Configuration
const config = {
expectedApiKey: process.env.AMWAL_API_KEY_FINGERPRINT,
privateKeyPath: process.env.AMWAL_PRIVATE_KEY_PATH,
webhooksEnabled: process.env.AMWAL_WEBHOOKS_ENABLED === 'true'
};
// Middleware for raw body
app.use('/webhooks/amwal', express.raw({ type: 'application/json' }));
app.use(express.json());
// Signature Verification
function verifySignature(payload, signature, privateKeyPath) {
try {
const privateKey = fs.readFileSync(privateKeyPath, 'utf8');
const publicKey = crypto.createPublicKey(privateKey);
const signatureBuffer = Buffer.from(signature, 'base64');
const verifier = crypto.createVerify('SHA256');
verifier.update(payload);
return verifier.verify(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_AUTO
},
signatureBuffer
);
} catch (error) {
console.error('Signature verification error:', error);
return false;
}
}
// Webhook Handlers
const webhookHandlers = {
'order.created': async (data) => {
console.log('Order created:', data.id);
return { success: true, message: 'Order created' };
},
'order.success': async (data) => {
console.log('Payment confirmed:', data.id);
return { success: true, message: 'Payment confirmed' };
},
'order.failed': async (data) => {
console.log('Payment failure:', data.id);
return { success: true, message: 'Payment failure recorded' };
},
'order.updated': async (data) => {
console.log('Order updated:', data.id);
return { success: true, message: 'Order updated' };
}
};
// Webhook Endpoint
app.post('/webhooks/amwal', async (req, res) => {
if (!config.webhooksEnabled) {
return res.status(503).json({ error: 'Webhooks disabled' });
}
const signature = req.headers['x-signature'];
const apiKey = req.headers['x-api-key'];
if (!signature || !apiKey || apiKey !== config.expectedApiKey) {
return res.status(401).json({ error: 'Invalid headers' });
}
const rawPayload = req.body.toString('utf8');
if (!verifySignature(rawPayload, signature, config.privateKeyPath)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const webhook = JSON.parse(rawPayload);
const handler = webhookHandlers[webhook.event_type];
if (!handler) {
return res.status(400).json({ error: 'Unknown event type' });
}
const result = await handler(webhook.data);
res.status(200).json({ status: 'success', ...result });
});
// Health Check
app.get('/health', (req, res) => {
res.json({ status: 'healthy', webhooks_enabled: config.webhooksEnabled });
});
// Start Server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
module.exports = app;Environment Setup
// .env file
AMWAL_WEBHOOKS_ENABLED=true
AMWAL_API_KEY_FINGERPRINT=8a7d42f1c4e6ba957beec92f2cad51d0b3ec4f8c9a1529e8f35e53a1de1a8b3b
AMWAL_PRIVATE_KEY_PATH=/etc/amwal/private_key.pem
AMWAL_WEBHOOK_URL=https://your-domain.com/webhooks/amwal
YOUR_API_TOKEN=your_system_api_token
PORT=3000Webhook Log Schema
CREATE TABLE amwal_webhook_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
event_type VARCHAR(50) NOT NULL,
payload TEXT NOT NULL,
api_key_fingerprint VARCHAR(100),
signature_verified BOOLEAN DEFAULT FALSE,
order_id VARCHAR(100),
magento_order_id VARCHAR(100),
success BOOLEAN DEFAULT FALSE,
message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_event_type (event_type),
INDEX idx_order_id (order_id),
INDEX idx_created_at (created_at),
INDEX idx_success (success)
);Webhook Queue
const Queue = require('bull');
class WebhookQueue {
constructor() {
this.queue = new Queue('amwal-webhooks', {
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
}
});
this.setupProcessors();
}
async addWebhook(webhook) {
return await this.queue.add('process-webhook', webhook, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
removeOnComplete: true
});
}
setupProcessors() {
this.queue.process('process-webhook', async (job) => {
const webhook = job.data;
const handler = webhookHandlers[webhook.event_type];
if (!handler) throw new Error(`Unknown event type: ${webhook.event_type}`);
return await handler(webhook.data);
});
this.queue.on('completed', (job, result) => {
console.log(`Webhook ${job.data.id} completed:`, result);
});
this.queue.on('failed', (job, error) => {
console.error(`Webhook ${job.data.id} failed:`, error);
});
}
}
const webhookQueue = new WebhookQueue();
app.post('/webhooks/amwal', async (req, res) => {
const rawPayload = req.body.toString('utf8');
const webhook = JSON.parse(rawPayload);
await webhookQueue.addWebhook(webhook);
res.status(200).json({ status: 'queued', webhook_id: webhook.id });
});Testing & Monitoring
Create Test Webhook
async function createTestWebhook() {
const response = await fetch(
'https://backend.sa.amwal.tech/api/create-webhook-and-apikey/',
{
method: 'POST',
headers: {
'Authorization': 'YOUR_MERCHANT_SECRET_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://webhook.site/YOUR-UNIQUE-URL',
description: 'Test webhook',
event_type_names: ['order.created', 'order.success', 'order.failed'],
api_key_name: 'Test Key',
api_key_scopes: ['trigger_events']
})
}
);
const data = await response.json();
console.log('Test webhook created:', data);
return data;
}Trigger Test Events
async function triggerTestOrderSuccess() {
const response = await fetch(
'https://backend.sa.amwal.tech/api/trigger-event/',
{
method: 'POST',
headers: {
'X-API-Key': 'YOUR_TEST_API_FINGERPRINT',
'Content-Type': 'application/json'
},
body: JSON.stringify({
event_type: 'order.success',
payload: {
id: 'test_order_12345',
ref_id: 'TEST-ORDER-001',
amount: 100.00,
currency: 'SAR',
status: 'completed'
}
})
}
);
const result = await response.json();
console.log('Test event triggered:', result);
return result;
}
async function triggerTestOrderFailed() {
const response = await fetch(
'https://backend.sa.amwal.tech/api/trigger-event/',
{
method: 'POST',
headers: {
'X-API-Key': 'YOUR_TEST_API_FINGERPRINT',
'Content-Type': 'application/json'
},
body: JSON.stringify({
event_type: 'order.failed',
payload: {
id: 'test_order_12346',
ref_id: 'TEST-ORDER-002',
amount: 50.00,
currency: 'SAR',
status: 'failed'
}
})
}
);
const result = await response.json();
console.log('Test event triggered:', result);
return result;
}Monitor Integration Health
async function monitorRecentDeliveries() {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const response = await fetch(
'https://backend.sa.amwal.tech/api/delivery-attempts/',
{
headers: {
'X-API-Key': 'YOUR_API_KEY_FINGERPRINT'
}
}
);
const data = await response.json();
const recentDeliveries = data.results.filter(
attempt => new Date(attempt.created_at) > new Date(oneHourAgo)
);
const stats = recentDeliveries.map(attempt => ({
webhook_url: attempt.webhook_url,
event_type: attempt.event_type,
status: attempt.status,
response_code: attempt.response_code,
attempts: attempt.attempts,
error_message: attempt.error_message
}));
console.log('Recent deliveries (last hour):', stats);
return stats;
}
async function calculateSuccessRate(limit = 100) {
const response = await fetch(
`https://backend.sa.amwal.tech/api/delivery-attempts/?limit=${limit}`,
{
headers: {
'X-API-Key': 'YOUR_API_KEY_FINGERPRINT'
}
}
);
const data = await response.json();
const results = data.results;
const statusCounts = results.reduce((acc, attempt) => {
acc[attempt.status] = (acc[attempt.status] || 0) + 1;
return acc;
}, {});
const total = results.length;
const successful = statusCounts.success || 0;
const successRate = (successful / total * 100).toFixed(2);
console.log('Delivery Statistics:');
console.log('Status counts:', statusCounts);
console.log(`Success rate: ${successRate}%`);
return { statusCounts, successRate, total };
}Health Check Script
#!/usr/bin/env node
const https = require('https');
const config = {
apiKeyFingerprint: process.env.AMWAL_API_KEY_FINGERPRINT,
webhookEndpoint: process.env.AMWAL_WEBHOOK_URL
};
async function checkWebhookHealth() {
console.log('=== Webhook Health Check ===');
// Test endpoint
try {
const response = await fetch(config.webhookEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ test: true })
});
console.log(`Endpoint accessible (HTTP ${response.status})`);
} catch (error) {
console.log(`Endpoint not accessible: ${error.message}`);
}
// Check SSL
try {
const url = new URL(config.webhookEndpoint);
const cert = await getSSLCertificate(url.hostname);
const validTo = new Date(cert.valid_to);
const now = new Date();
const daysUntilExpiry = Math.floor((validTo - now) / (1000 * 60 * 60 * 24));
console.log(`SSL valid until: ${cert.valid_to} (${daysUntilExpiry} days)`);
} catch (error) {
console.log(`SSL check failed: ${error.message}`);
}
// Check deliveries
try {
const response = await fetch(
'https://backend.sa.amwal.tech/api/delivery-attempts/?limit=50',
{
headers: {
'X-API-Key': config.apiKeyFingerprint
}
}
);
const data = await response.json();
const recentDeliveries = data.results.filter(
attempt => new Date(attempt.created_at) > new Date(Date.now() - 24 * 60 * 60 * 1000)
);
console.log('Recent delivery status:', recentDeliveries.length);
} catch (error) {
console.log(`Delivery check failed: ${error.message}`);
}
console.log('=== Health Check Complete ===');
}
function getSSLCertificate(hostname) {
return new Promise((resolve, reject) => {
const options = {
hostname: hostname,
port: 443,
method: 'GET'
};
const req = https.request(options, (res) => {
const cert = res.socket.getPeerCertificate();
resolve(cert);
});
req.on('error', reject);
req.end();
});
}
checkWebhookHealth().catch(console.error);Run the script:
chmod +x health_check.js
./health_check.jsAdvanced Configuration
Environment Variables
require('dotenv').config();
const config = {
webhooks: {
enabled: process.env.AMWAL_WEBHOOK_ENABLED === 'true',
apiKeyFingerprint: process.env.AMWAL_API_KEY_FINGERPRINT,
privateKeyPath: process.env.AMWAL_PRIVATE_KEY_PATH,
url: process.env.AMWAL_WEBHOOK_URL,
events: process.env.AMWAL_WEBHOOK_EVENTS?.split(',') || [],
signatureVerification: process.env.AMWAL_SIGNATURE_VERIFICATION !== 'false'
},
logging: {
level: process.env.AMWAL_LOG_LEVEL || 'INFO',
file: process.env.AMWAL_LOG_FILE || '/var/log/amwal_webhooks.log'
}
};
module.exports = config;Example .env file:
AMWAL_WEBHOOK_ENABLED=true
AMWAL_API_KEY_FINGERPRINT=8a7d42f1c4e6ba957beec92f2cad51d0b3ec4f8c9a1529e8f35e53a1de1a8b3b
AMWAL_PRIVATE_KEY_PATH=/etc/amwal/private_key.pem
AMWAL_WEBHOOK_URL=https://your-domain.com/webhooks/amwal
AMWAL_WEBHOOK_EVENTS=order.created,order.success,order.failed
AMWAL_LOG_LEVEL=INFO
AMWAL_SIGNATURE_VERIFICATION=trueUpdated about 15 hours ago