How to Test PayPal Webhooks Locally
What Webhooks Does PayPal Send?
PayPal has two notification systems: the legacy Instant Payment Notification (IPN) and the modern Webhooks API. The Webhooks API sends events like PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.DENIED, CHECKOUT.ORDER.APPROVED, BILLING.SUBSCRIPTION.CREATED, BILLING.SUBSCRIPTION.CANCELLED, CUSTOMER.DISPUTE.CREATED, and many more.
Webhook payloads are JSON with a consistent structure: id (unique event ID), event_type, resource_type, resource (the actual object), create_time, and links for related API resources. PayPal signs webhooks with a certificate-based system — the PayPal-Transmission-Sig, PayPal-Cert-Url, PayPal-Transmission-Id, and PayPal-Transmission-Time headers are used for verification.
The legacy IPN system sends form-encoded data and requires your server to POST the data back to PayPal for verification. If you are working with an older integration, you may still encounter IPN. New integrations should use the Webhooks API.
Configuring PayPal Webhooks with ReqPour
In the PayPal Developer Dashboard, go to your app and navigate to Webhooks. Click "Add Webhook" and enter your ReqPour URL as the webhook URL. Select the events you want to receive — for payment processing, choose at minimum PAYMENT.CAPTURE.COMPLETED and PAYMENT.CAPTURE.DENIED.
For sandbox testing, PayPal provides a webhook simulator at developer.paypal.com/developer/webhooksSimulator. Select an event type and send it to your ReqPour endpoint. This is a quick way to generate test payloads without making actual sandbox transactions.
Start the ReqPour relay to forward events to your local server:
npx reqpour relay --to http://localhost:3000/api/paypal/webhooksNow trigger a sandbox payment or use the webhook simulator. The event appears in the ReqPour dashboard and gets forwarded to your local handler.
Handling PayPal Webhook Events
Here is a handler that processes PayPal webhook events:
app.post('/api/paypal/webhooks', express.json(), async (req, res) => {
const { event_type, resource } = req.body;// Verify the webhook (see PayPal SDK docs) // In development, you can skip this and inspect // the headers in the ReqPour dashboard
switch (event_type) {
case 'PAYMENT.CAPTURE.COMPLETED':
const captureId = resource.id;
const amount = resource.amount.value;
const currency = resource.amount.currency_code;
console.log(Payment captured: ${captureId} - ${amount} ${currency});
await fulfillOrder(resource);
break;
case 'BILLING.SUBSCRIPTION.CANCELLED':
const subscriptionId = resource.id;
console.log(Subscription cancelled: ${subscriptionId});
await cancelSubscription(subscriptionId);
break;
case 'CUSTOMER.DISPUTE.CREATED':
console.log(Dispute created: ${resource.dispute_id});
await handleDispute(resource);
break;
}
res.status(200).send('OK'); }); ```
PayPal's event naming convention uses dot-separated uppercase names. The resource object structure varies by event type — use the ReqPour dashboard to inspect the exact shape of each event type you need to handle.
Verifying PayPal Webhook Signatures
PayPal's webhook verification is more complex than most providers. It uses a certificate-based system where you fetch the signing certificate from the URL in the PayPal-Cert-Url header, then verify the signature using the certificate, transmission ID, timestamp, webhook ID, and the CRC32 of the request body.
The PayPal SDK provides a verification method:
const paypal = require('@paypal/checkout-server-sdk');const webhookId = process.env.PAYPAL_WEBHOOK_ID;
const verification = { auth_algo: req.headers['paypal-auth-algo'], cert_url: req.headers['paypal-cert-url'], transmission_id: req.headers['paypal-transmission-id'], transmission_sig: req.headers['paypal-transmission-sig'], transmission_time: req.headers['paypal-transmission-time'], webhook_id: webhookId, webhook_event: req.body, };
// Use PayPal API to verify const response = await client.execute( new paypal.notification.NotificationVerifySignature(verification) ); ```
During development, you can use the ReqPour dashboard to inspect all these headers and ensure they are present before implementing full verification.
PayPal Development Tips
Always use PayPal's sandbox environment during development. Sandbox webhooks work identically to production but use test credentials and do not process real money. Your ReqPour endpoint works the same way for both sandbox and production webhooks.
PayPal retries webhook deliveries for up to 3 days with increasing intervals. During development, this means a failed webhook might retry unexpectedly. Check the PayPal-Transmission-Id header to identify retries and the request inspector in ReqPour to understand why your handler failed the first time.
Test edge cases using ReqPour's replay: capture a PAYMENT.CAPTURE.COMPLETED event, then replay it to verify idempotency. Modify your handler to handle scenarios like partial captures, multi-currency payments, and payments that are captured and then refunded.
Related
Get started with ReqPour
Catch, inspect, and relay webhooks to localhost. Free to start, $3/mo for Pro.