2.6 KiB
2.6 KiB
Paddle Webhooks
Event-driven notifications for payment lifecycle.
Setup
- Dashboard → Developer Tools → Notifications
- Create new destination with endpoint URL
- Select events to receive
- Copy signing secret
Signature Verification
Header: Paddle-Signature
Format: ts=1234567890;h1=sha256_signature
Node.js SDK
import Paddle from '@paddle/paddle-node-sdk';
const paddle = new Paddle(process.env.PADDLE_API_KEY);
app.post('/webhooks/paddle', async (req, res) => {
const signature = req.headers['paddle-signature'];
const rawBody = req.body; // raw request body string
try {
const event = paddle.webhooks.unmarshal(
rawBody,
process.env.PADDLE_WEBHOOK_SECRET,
signature
);
await handleEvent(event);
res.status(200).send('OK');
} catch (err) {
res.status(400).send('Invalid signature');
}
});
Manual Verification
import crypto from 'crypto';
function verifyPaddleWebhook(
rawBody: string,
signature: string,
secret: string
): boolean {
const [tsPart, h1Part] = signature.split(';');
const ts = tsPart.replace('ts=', '');
const h1 = h1Part.replace('h1=', '');
const signedPayload = `${ts}:${rawBody}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(h1),
Buffer.from(expectedSig)
);
}
Key Events
| Event | Description |
|---|---|
transaction.completed |
Payment successful |
transaction.payment_failed |
Payment failed |
subscription.created |
New subscription |
subscription.updated |
Subscription changed |
subscription.canceled |
Subscription canceled |
subscription.past_due |
Payment overdue |
subscription.paused |
Subscription paused |
subscription.resumed |
Subscription resumed |
customer.created |
New customer |
customer.updated |
Customer updated |
Event Payload
{
"event_id": "evt_xxx",
"event_type": "subscription.created",
"occurred_at": "2024-01-15T10:00:00Z",
"notification_id": "ntf_xxx",
"data": {
"id": "sub_xxx",
"status": "active",
"customer_id": "ctm_xxx",
"items": [{ "price": { "id": "pri_xxx" }, "quantity": 1 }],
"billing_cycle": { "interval": "month", "frequency": 1 },
"current_billing_period": {
"starts_at": "2024-01-15",
"ends_at": "2024-02-15"
}
}
}
Best Practices
- Store
event_idfor idempotency - Return 200 immediately, process async
- Implement retry handling (Paddle retries failed deliveries)
- Use webhook secret per environment