Receiving Webhook Events

Set up and verify webhook endpoints, including payload structure and signature verification.

Webhook Endpoint Requirements

Your webhook endpoint must:

  • Accept HTTP POST requests with JSON payloads.
  • Return HTTP 200 for successful processing within 30 seconds.
  • Return HTTP 4xx for permanent failures (no retries).
  • Return HTTP 5xx for temporary failures (will retry).
  • Use HTTPS - HTTP is not supported.
  • Validate signatures before processing (recommended).

Payload Structure

Webhook payloads follow this structure:

{
  "event_type": "order.success",
  "id": "amwal_order_12345",
  "timestamp": "2025-12-15T10:30:00Z",
  "data": {
    "id": "amwal_order_12345",
    "ref_id": "ORD-2025-001",
    "merchant_order_id": "ORD-2025-001",
    "amount": 99.99,
    "currency": "SAR",
    "status": "completed",
    "customer": {
      "id": "customer_123",
      "email": "[email protected]",
      "phone": "+966501234567",
      "first_name": "John",
      "last_name": "Doe"
    },
    "payment": {
      "method": "wallet",
      "reference": "pay_ref_12345",
      "transaction_id": "txn_67890",
      "gateway": "amwal"
    },
    "shipping": {
      "address": {
        "street": "123 Main St",
        "city": "Riyadh",
        "state": "Riyadh Province",
        "country": "SA",
        "postal_code": "11564"
      },
      "method": "standard"
    },
    "metadata": {
      "custom_field": "value",
      "integration_version": "1.0.0"
    }
  }
}

Signature Verification

Steps

  1. Extract signature from X-Signature header.
  2. Get raw payload - Use the exact request body.
  3. Decode signature - Base64 decode the signature.
  4. Load private key - Retrieve your stored private key.
  5. Extract public key - Derive from the private key.
  6. Verify signature - Use RSA-PSS with SHA-256.

Verification Using Node.js Crypto Module

const crypto = require('crypto');
const fs = require('fs');

function verifyWebhookSignature(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;
  }
}

const express = require('express');
const app = express();

app.use('/webhooks/amwal', express.raw({ type: 'application/json' }));

app.post('/webhooks/amwal', (req, res) => {
  const signature = req.headers['x-signature'];
  const apiKey = req.headers['x-api-key'];
  const rawPayload = req.body.toString('utf8');

  if (apiKey !== process.env.EXPECTED_API_KEY_FINGERPRINT) {
    return res.status(401).json({ error: 'Invalid API key' });
  }

  if (!verifyWebhookSignature(rawPayload, signature, process.env.PRIVATE_KEY_PATH)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const webhook = JSON.parse(rawPayload);
  console.log('Valid webhook received:', webhook.event_type);
  res.status(200).json({ status: 'success' });
});

Key Configuration

  • Store private keys securely.
  • Never expose private keys in logs or code.
  • Use the exact private key provided during webhook creation.