Verify Signature

Verify the authenticity of incoming webhook payloads by validating the signature attached to each request. This ensures the payload was sent by Amwal and has not been tampered with in transit.

The verification uses RSA-PSS with SHA-256 — an asymmetric signing scheme where Amwal signs payloads with a private key, and you verify them using the corresponding public key.

Prerequisites

  1. Retrieve your RSA public key (PEM format) from the Create Webhook and API Key endpoint.
  2. Extract the signature from the x-signature header of each incoming webhook request.

Inputs

Verification requires three values: the RSA public key, the webhook payload, and the Base64-encoded signature. Replace the placeholders below with your actual values.

const publicKeyPem = `-----BEGIN PUBLIC KEY-----

-----END PUBLIC KEY-----`;

const payload = {
  "amount": "29.00000",
  "client_first_name": "fname ",
  "client_last_name": "lname",
  "payment_link_id": "9493559e-yyyyyyb",
  "payment_option": "Pay In Full",
  "status": "success",
  "transaction_id": "ccdaaba6-xxxxx5"
};

const signatureBase64 = "amwal - signature";

Verify the Signature

The verification process follows these steps:

  1. Parse the PEM key — Strip the PEM headers and decode the Base64 content into a binary key that the Web Crypto API can import.

  2. Import the public key — Import the binary key as an RSA-PSS key with SHA-256 hashing.

  3. Serialize the payload — Extract only the following fields from the incoming payload, sort the keys alphabetically, and serialize to a JSON string with a space after each : and ,.

    Required fields: amount, client_first_name, client_last_name, payment_link_id, payment_option, status, transaction_id

    {
      "amount": "29.00000",
      "client_first_name": "fname ",
      "client_last_name": "lname",
      "payment_link_id": "9493559e-yyyyyy",
      "payment_option": "Pay In Full",
      "status": "success",
      "transaction_id": "ccdaaba6-xxxxx"
    }
    ⚠️

    Include only the fields listed above — do not add, remove, or rename any fields. Serialize with UTF-8 encoding, consistent key ordering (alphabetical), and exact string values (including trailing spaces). Any difference in serialization will cause verification to fail.

  4. Decode the signature — Convert the Base64-encoded signature string into a binary array.

  5. Verify — Pass the key, signature, and serialized payload to crypto.subtle.verify using RSA-PSS.

async function verifySignature() {
  try {
    // A. Clean the PEM string to get raw Base64
    const pemContents = publicKeyPem
          .replace(/-----BEGIN PUBLIC KEY-----/g, "")
          .replace(/-----END PUBLIC KEY-----/g, "")
          .replace(/\s/g, "");

    // B. Convert PEM Base64 to ArrayBuffer
    const binaryDerString = window.atob(pemContents);
    const binaryDer = new Uint8Array(binaryDerString.length);
    for (let i = 0; i < binaryDerString.length; i++) {
            binaryDer[i] = binaryDerString.charCodeAt(i);
          }

    // C. Import the Public Key
    const cryptoKey = await window.crypto.subtle.importKey(
            "spki",
            binaryDer.buffer,
            { name: "RSA-PSS", hash: "SHA-256" },
            false,
            ["verify"]
          );

    const sortedKeys = Object.keys(payload).sort();
    const sortedObj = {};
    sortedKeys.forEach(key => sortedObj[key] = payload[key]);

    const jsonString = JSON.stringify(sortedObj, null, 0)
          .replace(/:/g, ": ")
          .replace(/,/g, ", ");

    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(jsonString);

    // E. Decode Signature Base64
    const sigBinary = Uint8Array.from(atob(signatureBase64), c => c.charCodeAt(0));

    // F. Verify using RSA-PSS
    const isValid = await window.crypto.subtle.verify(
            {
                name: "RSA-PSS",
                saltLength: 222, // SHA-256 PSS usually uses 32-byte salt, or max. 
              },
            cryptoKey,
            sigBinary,
            dataBuffer
          );

    if (isValid) {
            alert( "✅ Verification Successful!");

          } else {

              const dataBufferSimple = encoder.encode(JSON.stringify(sortedObj));
              const isValidSimple = await window.crypto.subtle.verify(
                { name: "RSA-PSS", saltLength: 32 },
                cryptoKey, sigBinary, dataBufferSimple
              );

              if (isValidSimple) {
                alert( "✅ Verification Successful (Simple JSON)!");

              } else {
                  alert( "❌ Verification Failed: Signature mismatch.");

                }
            }

  } catch (err) {
          alert( "❌ Error: " + err.message);
        }
}

Verification Outcomes

ResultMeaningAction
✅ Verification SuccessfulThe payload is authentic and unmodified.Process the webhook event.
❌ Signature mismatchThe signature does not match the payload. The request may be forged or the payload was altered.Reject the request. Do not process the event.