Check signature

Verify the events that PayEngine sends to your webhook endpoints.

We always sign the webhook events it sends to your endpoints by including a signature in each event's X-PF-Signature header. This allows you to verify that the events were sent by us, not by a third party.

Before you can verify the signature, you need to create a webhook endpoint on your Developer Dashboard. From there you can find the respectively shared-secret key, that will be used for verification.

PayEngine generates a unique secret for each endpoint. If you use multiple endpoints, you must obtain a secret for each one you want to verify the signature on.

Verifying the signature

The X-PF-Signature header included in each signed event contains a timestamp and signature. The timestamp is prefixed by t=, and the signature is prefixed by s=.

x-pf-signature:
t=1616987734,
s=614c7ca17945e4038ec4af052585fe970120ea909f8582b7e953415395951de1

We generate the signature using a hash-based message authentication code (HMAC) with SHA-256. To verify the signature you can follow these steps.

Step 1: Extract the timestamp and signature from the header

Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.

The value for the prefix t corresponds to the timestamp, and s corresponds to the signature. You can discard all other elements.

Step 2: Prepare the signed_payload string

The signed_payload string is created by concatenating:

  • The timestamp (as a string)

  • The character .

  • The actual JSON payload (the request BODY)

Step 3: Determinate the expected signature

Compute an HMAC with the SHA256 hash function. Use the endpoint's signing secret as the key, and use the signed_payload string as the message.

Step 4: Compare the signatures

Compare the signature in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

To protect against timing attacks, use constant-time string comparison to compare the expected signature to the received signature.

Example code

app.post("/webhook", (req, res) => {
    const header = req.header("x-pf-signature");
    const body = req.body;
    const secret = process.env.PF_WEBHOOK_SECRET
    
    const details = header.split(",").reduce(
        (obj, item) => {
            const kv = item.split("=");
            if (kv[0] === "t") {
                obj.timestamp = kv[1];
            }
            if (kv[0] === "s") {
                obj.signature = kv[1];
            }
            return obj;
        }, { timestamp: -1, signature: "" }
    );
    if (!details || details.timestamp === -1) {
        throw new Error('Unable to extract timestamp and signature from header')
    }
    
    const payload = `${details.timestamp}.${JSON.stringify(body)}`
    const expectedSignature = crypto
        .createHmac('sha256', secret)
        .update(payload, 'utf-8')
        .digest('hex')
    
    if (details.signature !== expectedSignature) {
        throw new Exception('Signature mismatch')
    }
});

Last updated