init
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
# Paddle Webhooks
|
||||
|
||||
Event-driven notifications for payment lifecycle.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Dashboard → Developer Tools → Notifications
|
||||
2. Create new destination with endpoint URL
|
||||
3. Select events to receive
|
||||
4. Copy signing secret
|
||||
|
||||
## Signature Verification
|
||||
|
||||
Header: `Paddle-Signature`
|
||||
Format: `ts=1234567890;h1=sha256_signature`
|
||||
|
||||
### Node.js SDK
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```json
|
||||
{
|
||||
"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_id` for idempotency
|
||||
- Return 200 immediately, process async
|
||||
- Implement retry handling (Paddle retries failed deliveries)
|
||||
- Use webhook secret per environment
|
||||
Reference in New Issue
Block a user