Receiving Webhook Events

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

Receive webhook POST requests, validate the request headers and payload, and return the correct HTTP status so Amwal can deliver events reliably.

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).

Receive and Acknowledge a Webhook

Create a webhook endpoint that accepts POST requests, checks the required headers, validates the payload shape, and returns the correct status code:

const express = require('express');
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 3000;
const expectedApiKey = process.env.AMWAL_API_KEY_FINGERPRINT;

app.use(express.json());

function processEvent(webhook) {
  switch (webhook.event_type) {
    case 'order.created':
    case 'order.success':
    case 'order.failed':
    case 'order.updated':
      return {
        accepted: true,
        eventType: webhook.event_type,
        orderId: webhook?.data?.id || webhook?.id || null
      };
    default:
      return {
        accepted: false,
        reason: `Unknown event type: ${webhook.event_type}`
      };
  }
}

app.post('/webhooks/amwal', async (req, res) => {
  try {
    const apiKey = req.header('X-API-Key');
    const signature = req.header('X-Signature');
    const contentType = req.header('Content-Type') || '';

    if (!contentType.includes('application/json')) {
      return res.status(400).json({
        error: 'Invalid Content-Type. Use application/json.'
      });
    }

    if (!apiKey || apiKey !== expectedApiKey) {
      return res.status(401).json({
        error: 'Invalid X-API-Key header.'
      });
    }

    if (!signature) {
      return res.status(401).json({
        error: 'Missing X-Signature header.'
      });
    }

    const webhook = req.body;

    if (!webhook.event_type || !webhook.data) {
      return res.status(400).json({
        error: 'Invalid payload structure.'
      });
    }

    const result = processEvent(webhook);

    if (!result.accepted) {
      return res.status(400).json({
        error: result.reason
      });
    }

    console.log(`Received ${result.eventType}`, {
      order_id: result.orderId,
      webhook_id: webhook.id || null
    });

    return res.status(200).json({
      status: 'received',
      event_type: result.eventType,
      webhook_id: webhook.id || null
    });
  } catch (error) {
    console.error('Webhook processing failed:', error.message);
    return res.status(500).json({
      error: 'Temporary processing failure.'
    });
  }
});

app.listen(PORT, () => {
  console.log(`Webhook listener running on port ${PORT}`);
});

Expected success response:

{
  "status": "received",
  "event_type": "order.success",
  "webhook_id": "amwal_order_12345"
}

Expected error responses:

{
  "error": "Invalid X-API-Key header."
}
{
  "error": "Invalid payload structure."
}
{
  "error": "Temporary processing failure."
}

Use the signature verification flow in Verify Signature before you process the webhook payload.

Payload Structure

Webhook payloads follow this structure:

Order.Success Event

{
  "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"
    }
  }
}

Order.updated Event

Refund Payload Example

{
  "event_type": "order.updated",
  "data": {
    "address_details": {
      "id": "3e637800-bede-4f50-82ea-d0c996d8c2cf",
      "city": "الصوارى",
      "state": "جدة",
      "country": "SA",
      "street1": "Icon55 jeddah",
      "postcode": "37732"
    },
    "refunded_amount": "1.30",
    "amount": "0.87000",
    "amwal_fee": "1.03",
    "amwal_tax_vat": "0.15",
    "app_notified": false,
    "approval_type": null,
    "brand_percentage": "2.50",
    "brand_static_fee": "1.00",
    "card_last_4_digits": "",
    "card_holder": "",
    "card_payment_brand": "",
    "client_email": "[email protected]",
    "client_first_name": "M",
    "client_last_name": "Z",
    "client_phone_number": "+966542170857",
    "client_registered": true,
    "created_at": "2025-12-14T08:37:32.640759+03:00",
    "discount": "0.00000",
    "fees": "0.00000",
    "has_new_registration": false,
    "hyperpay_checkout_id": null,
    "hypersplit_id": null,
    "id": "48744b9b-ecf6-4ed4-a511-d85f7ec3c15c",
    "is_approved_by_client": null,
    "is_refundable": false,
    "merchant_business_name": "Amwal Shop",
    "merchant_country_code": "SA",
    "merchant_english_business_name": "Amwal Shop",
    "merchant_key": "prod-amwal-5563d261-8545-418b-b122-8767a2e59931",
    "merchant_payout": "0.11",
    "order_details": {
      "order_id": "5276",
      "order_url": "https://shop.amwal.tech/checkout/order-received/5276/?key=wc_order_7zIS8V0Fm5Ced"
    },
    "paymentBrand": "VISA",
    "payment_method": "Apple Pay",
    "payment_option": "Pay In Full",
    "ref_id": "amwalwc-payload693e4d1932fb2",
    "shipping": "0.30000",
    "shipping_details": {
      "id": "flat_rate:12",
      "tax": 0,
      "label": "Flat rate",
      "price": 0.3
    },
    "status": "refunded",
    "store_domain": "https://shop.amwal.tech/",
    "store_logo": "https://fra1.digitaloceanspaces.com/media-amwal/media/store_logo/481a2629-ca3.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=DO801HVVA4FFJDYGV84V%2F20260209%2Ffra1%2Fs3%2Faws4_request&X-Amz-Date=20260209T095947Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=e533a51291c4fc02b4a7daccc54ef49325901fda62777d8e55ee53c481f788d2",
    "taxes": "0.13000",
    "total_amount": "1.30",
    "convenience_fee": "0.00",
    "payment_amount": "1.30",
    "failure_reason": null,
    "type": "PRODUCTION",
    "gateway": 11,
    "gateway_type": 4,
    "payment_link_id": "",
    "payment_link_callback_url": "",
    "payment_link_description": "",
    "billing_address_required": false,
    "supports_stc_pay": false,
    "refund_tracker": [
      {
        "id": "3392",
        "status": "R",
        "amount": "1.30",
        "gateway_transaction_id": "2026000179",
        "gateway_key_type": "RefundReference",
        "rrn": "603613330708"
      }
    ],
    "merchant_domain": "https://shop.amwal.tech/",
    "card_type": "",
    "card_bank_issuer": "",
    "installment_fee": "0.00",
    "installment_fee_vat": "0.00",
    "murabha_fee": "0.00",
    "murabha_fee_vat": "0.00",
    "installment_duration": null,
    "branch_name": "-",
    "payment_link_lang": "",
    "payment_link_only_show_bank_installments": true,
    "payment_link_only_show_pay_in_full": true,
    "payment_link_show_shipping": false,
    "payment_link_passkey_enabled": true,
    "address_required": false,
    "funding_method": "credit",
    "issuer": "Celtic Bank",
    "number": "427106xxxxxx7643",
    "bank_name": "",
    "trx_lang": "en"
  }
}

Optional: Troubleshoot Request Handling

  • Return 200 only after your application has accepted the event for processing.
  • Return 400 when the payload is invalid or required fields are missing.
  • Return 401 when X-API-Key or X-Signature is missing or invalid.
  • Return 500 when your system cannot process the event and Amwal should retry.
  • Keep processing under 30 seconds. If your business logic takes longer, queue the job and acknowledge the webhook first.