REST APIs
    Get Started with PayPal REST APIs
    Authentication
    Postman Guide
    API requests
    API responses
    Core Resources
    Overview
    API Integration
    Release Notes
    Add Tracking
    Catalog Products
    Disputes
    Identity
    Invoicing
    Orders
    Partner Referrals
    Payment Experience
    Payment Method Tokens
    Payments
    Payouts
    Referenced Payouts
    Subscriptions
    Transaction Search
    Webhooks Management
    Webhooks
    Overview
    Webhook event names
    Webhooks Events dashboard
    Webhooks simulator
    Integration
    Sandbox
    Overview
    Accounts
    Bulk Accounts
    Card testing
    Codespaces
    PayPal for Visual Studio Code
    Negative Testing
    Go Live
    Production Environment
    PayPal Application Guidelines
    PayPal Security Guidelines
    Rate Limiting Guidelines
    Idempotency
    Troubleshooting
    Not authorized
    Resource not found
    Unprocessable entity
    Validation error
    Reference
    Currency Codes
    Country Codes
    State & Province Codes
    Locale codes
    Deprecated Resources
    Deprecated resources
    Billing Agreements
    Billing Plans
    Invoicing v1
    Orders v1
    Partner Referrals v1
    Payments v1

Integrate webhooks

This guide covers technical details for implementing a webhook listener and verifying messages received.

Subscribing a listener URL

There are two ways to subscribe a URL to webhook events:

  • In Application management, under each App's settings
  • Programmatically using the Webhooks Management API

When you subscribe a listener URL, it will have a Webhook ID. Record the ID as it is needed for verification of all webhook messages sent to that URL.

Your listener should be an app or script deployed at the corresponding URL that listens on HTTPS port 443. For webhook message deliveries to be successful, the listener must respond with an HTTP 2xx success status every time one is posted. Otherwise—for example if there is no response, PayPal cannot connect, or if the server responds with a 404 or 500 error—PayPal will retry each delivery up to 25 times over 3 days.

Test the listener with mock events

You may use the Webhooks simulator to generate mock events. These do not belong to any App and can be generated on demand purely for development and testing purposes, to validate the functionality of a listener and its ability to receive and verify a webhook cryptographically. (Note: Postback verification to the verify-webhook-signature endpoint is not supported for mock events.) For message signature verification purposes, the ID of mock webhooks is the string: WEBHOOK_ID.

Message verification

If your system is programmatically capable, self cryptographic verification is the preferred method as it will be faster and avoid extra API dependency and latency. The alternative (not usable for mock test webhooks) is to post the same information back to PayPal and have it perform the verification for you.

Self verification method

Form the following original message string for a signature check:

1transmissionId|timeStamp|webhookId|crc32

The fields in the string are:

FieldDescription
transmissionIdThe unique ID of the transmission, from the paypal-transmission-id HTTP header.
timeStampThe date and time when the message was transmitted, from the paypal-transmission-time HTTP header.
webhookIdThe ID of the webhook from when the listener URL subscribed to events. This does not come in the header or body of the message but rather where the webhook listener URL's event subscriptions are configured, such as Application management
crc32The Cyclic Redundancy Check (CRC32) checksum for the body of the HTTP payload, in decimal form. You must use the original raw body to calculate this; do not parse the body to an array/object and then re-stringify it.
Next, utilize the public key in the certificate file specified by the header paypal-cert-url. This file should automatically be downloaded and cached for future use. Use it to validate the signature given by the header paypal-transmission-sig for the original message string.

Sample code for a listener that does the above:

1import "dotenv/config";
2import express from "express";
3
4import crypto from "crypto"
5import crc32 from "buffer-crc32"
6
7import fs from "fs/promises"
8import fetch from "node-fetch"
9
10// Note: PayPal only delivers webhooks to port 443 (HTTPS).
11// Development ports can be used in a forwarding configuration, set 443 if this is front-facing.
12const { LISTEN_PORT = 8888, LISTEN_PATH="/", CACHE_DIR = ".", WEBHOOK_ID = "<from when the listener URL was subscribed>" } = process.env;
13
14async function downloadAndCache(url, cacheKey) {
15 if(!cacheKey) {
16 cacheKey = url.replace(/\W+/g, '-')
17 }
18 const filePath = `${CACHE_DIR}/${cacheKey}`;
19
20 // Check if cached file exists
21 const cachedData = await fs.readFile(filePath, 'utf-8').catch(() => null);
22 if (cachedData) {
23 return cachedData;
24 }
25
26 // Download the file if not cached
27 const response = await fetch(url);
28 const data = await response.text()
29 await fs.writeFile(filePath, data);
30
31 return data;
32}
33
34const app = express();
35
36app.post(LISTEN_PATH, express.raw({type: 'application/json'}), async (request, response) => {
37 const headers = request.headers;
38 const event = request.body;
39 const data = JSON.parse(event)
40
41 console.log(`headers`, headers);
42 console.log(`parsed json`, JSON.stringify(data, null, 2));
43 console.log(`raw event: ${event}`);
44
45 const isSignatureValid = await verifySignature(event, headers);
46
47 if (isSignatureValid) {
48 console.log('Signature is valid.');
49
50 // Successful receipt of webhook, do something with the webhook data here to process it, e.g. write to database
51 console.log(`Received event`, JSON.stringify(data, null, 2));
52
53 } else {
54 console.log(`Signature is not valid for ${data?.id} ${headers?.['correlation-id']}`);
55 // Reject processing the webhook event. May wish to log all headers+data for debug purposes.
56 }
57
58 // Return a 200 response to mark successful webhook delivery
59 response.sendStatus(200);
60});
61
62async function verifySignature(event, headers) {
63 const transmissionId = headers['paypal-transmission-id']
64 const timeStamp = headers['paypal-transmission-time']
65 const crc = parseInt("0x" + crc32(event).toString('hex')); // hex crc32 of raw event data, parsed to decimal form
66
67 const message = `${transmissionId}|${timeStamp}|${WEBHOOK_ID}|${crc}`
68 console.log(`Original signed message ${message}`);
69
70 const certPem = await downloadAndCache(headers['paypal-cert-url']);
71
72 // Create buffer from base64-encoded signature
73 const signatureBuffer = Buffer.from(headers['paypal-transmission-sig'], 'base64');
74
75 // Create a verification object
76 const verifier = crypto.createVerify('SHA256');
77
78 // Add the original message to the verifier
79 verifier.update(message);
80
81 return verifier.verify(certPem, signatureBuffer);
82}
83
84app.listen(LISTEN_PORT, () => {
85 console.log(`Node server listening at http://localhost:${LISTEN_PORT}/`);
86});

Postback method

Alternatively to the above, rather than verifying the signature yourself you can post the event back to the verify-webhook-signature REST API endpoint. Example:
1curl -X POST https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature \
2 -H "Content-Type: application/json" \
3 -H "Authorization: Bearer ACCESS-TOKEN" \
4 -d '{
5 "transmission_id": "db49fb10-1343-11ef-ac58-e32457403f67",
6 "transmission_time": "2024-05-16T05:19:23Z",
7 "cert_url": "https://api.sandbox.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-ab66f33d",
8 "auth_algo": "SHA256withRSA",
9 "transmission_sig": "ab2tJk1VCFm4EqdSuKqezr38rTdY3JeRQlw94V1xYAIQVgLScAFn2XLRAhtyo/1jp8RZPO80hLpWfbdvvwyxK1u6L6NX/bnr0vA1wpfkwbqDT/Z01YP2VvJiYQdhpf/yXosoIKwP+37pYdAhwCsIu2D5fJAaDXpM8A06SpZJkR4f5+k566PDVPP7RCED9Xny4Yo0WyVaFORkiDJkBLGhUUIoO7Jv5JP5RQmFqVxhwSAaf4YYy/2r21r5HMQgAIgtS7oHYRea43Ze03Zz0EWqJu/hvKBzKHqhcvUPF64hVg8Nk2yqjnW7ZHgBWoHil2SJ1xQzaq2vDCXv5gWDn5+TZA==",
10 "webhook_id": "0NH55953DH663215D",
11 "webhook_event": {"id":"WH-3F562076HD293871E-75F399086E414290U","event_version":"1.0","create_time":"2024-05-16T05:19:19.355Z","resource_type":"capture","resource_version":"2.0","event_type":"PAYMENT.CAPTURE.COMPLETED","summary":"Payment completed for $ 500.0 USD","resource":{"payee":{"email_address":"receivingbusiness@example.com","merchant_id":"QDGTZ7B92B9QT"},"amount":{"value":"500.00","currency_code":"USD"},"seller_protection":{"dispute_categories":["ITEM_NOT_RECEIVED","UNAUTHORIZED_TRANSACTION"],"status":"ELIGIBLE"},"supplementary_data":{"related_ids":{"order_id":"9P99943869582473S"}},"update_time":"2024-05-16T05:19:15Z","create_time":"2024-05-16T05:19:15Z","final_capture":true,"seller_receivable_breakdown":{"paypal_fee":{"value":"25.44","currency_code":"USD"},"gross_amount":{"value":"500.00","currency_code":"USD"},"net_amount":{"value":"474.56","currency_code":"USD"}},"links":[{"method":"GET","rel":"self","href":"https://api.sandbox.paypal.com/v2/payments/captures/3Y662965014333303"},{"method":"POST","rel":"refund","href":"https://api.sandbox.paypal.com/v2/payments/captures/3Y662965014333303/refund"},{"method":"GET","rel":"up","href":"https://api.sandbox.paypal.com/v2/checkout/orders/9P99943869582473S"}],"id":"3Y662965014333303","status":"COMPLETED"},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-3F562076HD293871E-75F399086E414290U","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-3F562076HD293871E-75F399086E414290U/resend","rel":"resend","method":"POST"}]}
12}'

Note: It is essential that the webhook_event data be posted back exactly as it was received, with no deviations in formatting or content of any kind. Parsing to an array/object followed by re-serializing back to a JSON string can fail verification.

Test with sandbox events

Once a listener URL is able to receive mock events successfully, the next step is to subscribe it to a sandbox REST App's events in Application management and generate actual test events with an integration that uses the app (for example, make a subscription payment resulting in a PAYMENT.SALE.COMPLETED event). Then you can proceed with having the listener do something useful with the message data (for instance, write to a database).

Troubleshooting delivery problems

If PayPal cannot connect to a listener URL and receives no HTTP status code response, the most common reasons are if incoming HTTPS connections on port 443 are being blocked by a firewall or other rule, or if the domain webhooks are being sent to is marked as malware, phishing, or as involved in some other abuse by url filtering services. One place where you can check the status of a domain is: urlfiltering.paloaltonetworks.com. (If your domain is marked as abusive or having malware, take action to secure it and request recategorization there, or use a different domain to receive webhooks from PayPal)

If a status code is being returned but is non-2xx, such as a 404 or 500, this indicates a problem with the listener app/script or its webserver configuration and must be troubleshooted there.

See also

  • Webhooks overview
  • Webhook event names
  • Webhooks API reference
Reference
PayPal.com
Privacy
Support
Legal
Contact