How to Test Paddle Webhooks Locally
What Webhooks Does Paddle Send?
Paddle is a merchant of record for SaaS companies, handling payments, taxes, and subscriptions. Its webhook system (Paddle Billing) sends events like subscription.created, subscription.updated, subscription.canceled, subscription.paused, transaction.completed, transaction.payment_failed, customer.created, customer.updated, adjustment.created, and payout.paid.
Each webhook payload includes an event_type, event_id, occurred_at timestamp, notification_id, and a data object containing the relevant entity. Paddle signs webhooks with an h1= HMAC-SHA256 signature in the Paddle-Signature header, along with a timestamp (ts=) for replay protection.
Since Paddle handles the entire billing relationship as merchant of record, its webhooks are your primary mechanism for knowing about subscription lifecycle changes. Missing or mishandling these webhooks can mean users getting access they have not paid for, or losing access when they should still have it.
Configuring Paddle with ReqPour
In the Paddle Developer Console, go to Notifications and create a new notification destination. Enter your ReqPour URL as the endpoint URL. Select the events you want to subscribe to — for a typical SaaS app, start with subscription.created, subscription.updated, subscription.canceled, transaction.completed, and transaction.payment_failed.
Paddle provides a sandbox environment for testing. Switch to sandbox mode in the console, create test products and prices, and simulate customer purchases. Each event arrives at your ReqPour endpoint where you can inspect the full payload.
Start the relay to forward to your local server:
npx reqpour relay --to http://localhost:3000/api/webhooks/paddlePaddle's sandbox lets you simulate the full customer lifecycle — subscribe, upgrade, downgrade, cancel, and payment failures — generating webhooks at each step.
Handling Paddle Webhooks in Code
Here is a handler for Paddle Billing webhook events:
const crypto = require('crypto');function verifyPaddleSignature(req) {
const signature = req.headers['paddle-signature'];
const parts = Object.fromEntries(
signature.split(';').map(p => p.split('='))
);
const ts = parts.ts;
const h1 = parts.h1;
const payload = ${ts}:${JSON.stringify(req.body)};
const expected = crypto
.createHmac('sha256', process.env.PADDLE_WEBHOOK_SECRET)
.update(payload)
.digest('hex');
return h1 === expected;
}
app.post('/api/webhooks/paddle', express.json(), (req, res) => { if (!verifyPaddleSignature(req)) { return res.status(401).send('Invalid signature'); }
const { event_type, data } = req.body;
switch (event_type) { case 'subscription.created': console.log('New subscription:', data.id); await activateSubscription(data); break; case 'subscription.canceled': console.log('Subscription canceled:', data.id); await deactivateSubscription(data.id); break; case 'transaction.completed': console.log('Payment received:', data.id); break; }
res.status(200).send('OK'); }); ```
The Paddle-Signature format includes both a timestamp and HMAC hash separated by semicolons. Use the timestamp for replay protection — reject requests where the timestamp is more than a few minutes old.
Testing Subscription Flows
Paddle's sandbox is excellent for testing the complete subscription lifecycle. Create a checkout session, complete it with test card details, and watch the subscription.created and transaction.completed events arrive at your ReqPour endpoint.
Then test subscription updates — upgrade to a different plan and observe subscription.updated with the new price and billing details. Cancel the subscription and verify your handler processes subscription.canceled correctly, including any grace period logic.
ReqPour's request history becomes invaluable here. You can review the entire sequence of events that make up a subscription lifecycle. This helps you understand the order of events and the data available at each step, which is critical for building correct subscription management logic.
Paddle Development Tips
Paddle events include comprehensive data — the full subscription object with items, prices, billing details, and custom data. Unlike some providers, you rarely need to make API callbacks to get additional information. Use the ReqPour dashboard to explore the complete payload structure.
Always implement idempotent handlers using the event_id field. Paddle guarantees at-least-once delivery and will retry failed webhook deliveries with exponential backoff for up to 30 days.
For SaaS billing, pay special attention to the subscription.updated event. This fires for plan changes, quantity changes, payment method updates, and billing period changes. Check the data fields carefully to determine what actually changed — the ReqPour dashboard lets you compare consecutive subscription.updated events to see exactly which fields differ.
Related
Get started with ReqPour
Catch, inspect, and relay webhooks to localhost. Free to start, $3/mo for Pro.