Webhook Signature Generation

A webhook signature is a cryptographic hash used to verify the authenticity and integrity of webhook requests. It ensures that the request comes from a trusted source and has not been tampered with.

Cash App Afterpay uses webhooks to notify merchants and partners about disputes.

Webhooks need a signature for security and verification purposes. Webhook signatures help:

  • Prevent spoofing – They ensure only trusted sources can send webhooks
  • Detect tampering – If the payload is altered, the signature will not match
  • Add security – The signature works as an additional layer of protection alongside HTTPS.

Pre-work

Before you can receive and act on webhooks, you need to do the following:

  1. Set up a webhook endpoint and an associated URL. The endpoint must allow POST requests with content-type = application/json.
  2. Contact Afterpay merchant support and give them the following information:
    • The URL you set up for webhook notification in step 1
    • Your unique partner ID

After we receive this information, we share an HMAC (Hash Message Authentication Code) value with you and enable our systems for notification.

Don’t confuse the HMAC shared secret key with the HMAC value that is generated using the HMAC shared secret key.

Verify the webhook event payload

To verify a webhook event payload with the HMAC value, follow these steps:

  1. Retrieve the webhook event signature:
    • Retrieve the event signature from the X-Afterpay-Request-Signature header provided in the webhook.
  2. Construct raw signature in canonical form:
    • url: The destination URL for the webhook
    • time: The X-Afterpay-Request-Date header value, represented as a UNIX timestamp
    • payload: The raw JSON body of the webhook
    • Concatenate raw signature with format: ${url}\n${time}\n${payload}
  3. Generate HMAC-SHA-256 value:
    • Create HMAC value using the raw signature and the API secret corresponding to API key utilities to create the webhook
    • Use a constant-time cryptographic library to generate the signature to prevent timing attacks
  4. Compare the generated signature with the received signature:
    • Compare the computed signature against the X-Afterpay-Request-Signature header value
    • If both signatures match, then the request is verified as legitimate
    • If they don’t match, reject the request

Code sample:

1const express = require('express');
2const crypto = require('crypto');
3const app = express();
4
5app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));
6
7// Secret provided by Afterpay team.
8const SECRET = "WEBHOOK_URL_SECRET"
9
10function verifyWebhookSignature(req) {
11
12// Step 1: Retrieve the webhook event signature
13 const receivedSignature = req.headers['x-afterpay-request-signature'];
14
15 // Step 2: Construct the message
16 const url = req.headers['host'];
17 const time = req.headers['x-afterpay-request-date'];
18 const payload = req.rawBody;
19 const message = `${url}\n${time}\n${payload}`
20
21 // Step 3: Create HMAC SHA-256 value.
22 const expectedSignature = crypto.createHmac('sha256', SECRET)
23 .update(message)
24 .digest('base64');
25
26 // Step 4: Assert recieved signature with expected signature.
27 const receivedBuffer = Buffer.from(receivedSignature, 'hex');
28 const expectedBuffer = Buffer.from(expectedSignature, 'hex');
29 return crypto.timingSafeEqual(receivedBuffer, expectedBuffer);
30}
31
32app.post('/afterpay', (req, res) => {
33 console.log("Message received:", req.body);
34 if (!verifyWebhookSignature(req)) {
35 return res.status(403).send("Invalid signature");
36 }
37
38 console.log("✅ Webhook verified:", req.body);
39 res.status(200).send("Webhook received");
40});
41
42app.listen(80, () => {
43 console.log('Webhook server listening on port 80');
44});

Manual signature verification

You can verify the webhook signature manually using the command line. This can be helpful for debugging or verifying payloads without using a backend server.

$PAYLOAD='{"webhook_event_id": "b4df2187-4090-4845-be15-a73546107cbe", "webhook_event_type": "created", "dispute_id": "dp_KvGaECApCMdsH8earUSa2V", "merchant_reference": "08CF65ZSFNHVM"}'
>URL="${notification_uri}" # replace with your webhook endpoint URL
>SECRET=<your_hmac_secret_key> # replace with the HMAC secret key shared by Afterpay
>TIME=1741100821 # UNIX timestamp from X-Afterpay-Request-Date header
>
>MESSAGE="$URL\n$TIME\n$PAYLOAD"
>HMAC=$(printf "${MESSAGE}" | openssl dgst -hmac "${SECRET}" -sha256 -binary | base64)

The resulting HMAC value is the signature you should compare against the value in the X-Afterpay-Request-Signature header.

Sample request:

1POST ${notification_uri} HTTP/1.1
2Host: ${notification_base_url}
3X-Afterpay-Request-Date: 1664239810
4X-Afterpay-Request-Signature: ${HMAC}
5Content-Type: application/json
6
7{
8 "webhook_event_id": "b4df2187-4090-4845-be15-a73546107cbe",
9 "webhook_event_type": "created",
10 "dispute_id": "dp_KvGaECApCMdsH8earUSa2V",
11 "merchant_reference": "08CF65ZSFNHVM"
12}