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 Pay uses webhooks for the following events:

Event TypeTrigger
customer.createdWhen a Cash App user approves a client’s Cash App Pay request for the first time.
customer.deletedWhen a customer account is deleted by Cash App.
customer.updatedWhen a customer updates their Cashtag.
customer_request.state.updatedWhen a customer request’s state is updated. This is typically caused by a customer approving or declining the request.
dispute.createdWhen a dispute is created. This happens when a customer files a dispute for a payment collected by a merchant.
dispute.state.updatedWhen a disputed status changes due to action taken by Block or the partner.
grant.createdWhen a grant is created. This happens when a customer request is approved.
grant.status.updatedWhen a grant’s status is updated. The status change can occur under these conditions:
• An API client-initiated action to create a payment or revoke a grant
• A customer revoking an extended-use grant
merchant.status.updatedWhen a merchant’s status is updated.
payment.status.updatedWhen a payment status is updated. The status change can occur under these conditions:
• An API client-initiated action to void or capture the payment
• A payment auto-voiding after 7 days of not being captured
refund.status.updatedWhen a refund status is updated. The status change can occur under these conditions:
• An API client-initiated action to void or capture the refund
• A refund auto-voiding after 7 days of not being captured

For all events, the webhook needs 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. Create a Webhook endpoint using the Create Webhook API:
    curl --location 'https://sandbox.api.cash.app/management/v1/webhook-endpoints' \
    --header 'Accept: application/json' \
    --header 'Authorization: Client CASH_APP_CLIENT_ID API_KEY' \
    --header 'X-Region: PDX' \
    --header 'X-Signature: sandbox:skip-signature-check' \
    --header 'Content-Type: application/json' \
    --data '{
    "webhook_endpoint": {
    "event_configurations": [
    {
    "event_type": "grant.created",
    "api_version": "v1"
    },
    {
    "event_type": "grant.status.updated",
    "api_version": "v1"
    }
    ],
    "delivery_timeout": 10000,
    "api_key_id": "KEY_*",
    "url": "<webhook_url>",
    "reference_id": "reference_id"
    },
    "idempotency_key": "idempotency_key"
    }'
  3. Ask your Cash App Pay Partner Engineering contact to allowlist the webhook URL.
  4. Verify that your webhook is approved by querying the Webhook Events API:
    curl --location 'https://sandbox.api.cash.app/management/v1/webhook-endpoints' \
    --header 'Authorization: Client CASH_APP_CLIENT_ID API_KEY' \
    --header 'Accept: application/json' \
    --header 'X-Region: PDX' \
    --header 'X-Signature: sandbox:skip-signature-check'

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-Signature header provided in the webhook.
  2. Construct raw signature in canonical form:
    • Get event method (e.g. POST)
    • Get the event handler path
    • Get the host
    • Construct headers with format: {lowercase(name)}:{strip(value)}\n
    • Hash event body
    • Concatenate raw signature with format:${method}\n${path}\n${headers}\n${bodyDigest}
  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-signature header value
    • If both signatures match, then the request is verified as legitimate
    • If they don’t match, reject the request
The API secret is only available when a Webhook is created through Cash App Pay APIs. If you lose the API secret, then update the Webhook with a new API key.

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// Your API credentials
8const CLIENT_ID = 'CAS-CI_*';
9const API_KEY = 'KEY_*'; // API key used to create the webhook.
10const API_SECRET = 'CASH_*'; // API secret generated when webhook was created.
11
12function verifyWebhookSignature(req) {
13
14 // Step 1: Retrieve signature from Webhook x-signature header.
15 const signatureHeader = req.headers['x-signature'];
16 if (!signatureHeader) return false;
17
18 // Extract received signature (removing "V1 " prefix)
19 const [version, receivedSignature] = signatureHeader.split(' ');
20 if (version !== 'V1' || !receivedSignature) return false;
21
22 // Step 2: Construct the raw signature in canonical form.
23 const method = 'POST';
24 const path = '/';
25 const host = req.headers['host'];
26 const headers = 'accept:*/*' + '\nauthorization:Client ' + CLIENT_ID + ' ' + API_KEY + '\ncontent-type:application/json; charset=utf-8' + '\nhost:' + host;
27 const bodyDigest = crypto
28 .createHash('sha256')
29 .update(req.rawBody, 'utf8')
30 .digest('hex');
31 const rawSignature = `${method}\n${path}\n${headers}\n${bodyDigest}`;
32
33 // Step 3: Create HMAC SHA-256 value.
34 const expectedSignature = crypto
35 .createHmac('sha256', API_SECRET)
36 .update(rawSignature)
37 .digest('hex');
38
39 // Step 4: Assert recieved signature with expected signature.
40 const receivedBuffer = Buffer.from(receivedSignature, 'hex');
41 const expectedBuffer = Buffer.from(expectedSignature, 'hex');
42 return crypto.timingSafeEqual(receivedBuffer, expectedBuffer);
43}
44
45// Webhook endpoint
46app.post('*', (req, res) => {
47 if (!verifyWebhookSignature(req)) {
48 return res.status(403).send("Invalid signature");
49 }
50
51 console.log("✅ Webhook verified:", req.body);
52 res.status(200).send("Webhook received");
53});
54
55app.listen(80, () => {
56 console.log('Webhook server listening on port 80');
57});