Loading...
Loading...
Receive and verify Paddle webhooks. Use when setting up Paddle webhook handlers, debugging signature verification, or handling subscription events like subscription.created, subscription.canceled, or transaction.completed.
npx skill4agent add hookdeck/webhook-skills paddle-webhooksconst express = require('express');
const crypto = require('crypto');
const app = express();
// CRITICAL: Use express.raw() for webhook endpoint - Paddle needs raw body
app.post('/webhooks/paddle',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['paddle-signature'];
if (!signature) {
return res.status(400).send('Missing Paddle-Signature header');
}
// Verify signature
const isValid = verifyPaddleSignature(
req.body.toString(),
signature,
process.env.PADDLE_WEBHOOK_SECRET // From Paddle dashboard
);
if (!isValid) {
console.error('Paddle signature verification failed');
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
// Handle the event
switch (event.event_type) {
case 'subscription.created':
console.log('Subscription created:', event.data.id);
break;
case 'subscription.canceled':
console.log('Subscription canceled:', event.data.id);
break;
case 'transaction.completed':
console.log('Transaction completed:', event.data.id);
break;
default:
console.log('Unhandled event:', event.event_type);
}
// IMPORTANT: Respond within 5 seconds
res.json({ received: true });
}
);
function verifyPaddleSignature(payload, signature, secret) {
const parts = signature.split(';');
const ts = parts.find(p => p.startsWith('ts='))?.slice(3);
const signatures = parts
.filter(p => p.startsWith('h1='))
.map(p => p.slice(3));
if (!ts || signatures.length === 0) {
return false;
}
const signedPayload = `${ts}:${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Check if any signature matches (handles secret rotation)
return signatures.some(sig =>
crypto.timingSafeEqual(
Buffer.from(sig),
Buffer.from(expectedSignature)
)
);
}import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
webhook_secret = os.environ.get("PADDLE_WEBHOOK_SECRET")
@app.post("/webhooks/paddle")
async def paddle_webhook(request: Request):
payload = await request.body()
signature = request.headers.get("paddle-signature")
if not signature:
raise HTTPException(status_code=400, detail="Missing signature")
if not verify_paddle_signature(payload.decode(), signature, webhook_secret):
raise HTTPException(status_code=400, detail="Invalid signature")
event = await request.json()
# Handle event...
return {"received": True}
def verify_paddle_signature(payload, signature, secret):
parts = signature.split(';')
timestamp = None
signatures = []
for part in parts:
if part.startswith('ts='):
timestamp = part[3:]
elif part.startswith('h1='):
signatures.append(part[3:])
if not timestamp or not signatures:
return False
signed_payload = f"{timestamp}:{payload}"
expected = hmac.new(
secret.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
# Check if any signature matches (handles secret rotation)
return any(hmac.compare_digest(sig, expected) for sig in signatures)For complete working examples with tests, see:
- examples/express/ - Full Express implementation
- examples/nextjs/ - Next.js App Router implementation
- examples/fastapi/ - Python FastAPI implementation
| Event | Description |
|---|---|
| New subscription created |
| Subscription now active (first payment) |
| Subscription canceled |
| Subscription paused |
| Subscription resumed from pause |
| Transaction completed successfully |
| Payment attempt failed |
| New customer created |
| Customer details updated |
For full event reference, see Paddle Webhook Events
PADDLE_WEBHOOK_SECRET=pdl_ntfset_xxxxx_xxxxx # From notification destination settings# Install Hookdeck CLI for local webhook testing
brew install hookdeck/hookdeck/hookdeck
# Or via NPM
npm install -g hookdeck-cli
# Start tunnel (no account needed)
hookdeck listen 3000 --path /webhooks/paddle// Generated with: paddle-webhooks skill
// https://github.com/hookdeck/webhook-skills