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=3000

Webhook 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.js

Advanced 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=true