Files
english/.opencode/skills/payment-integration/references/paddle/webhooks.md
2026-04-12 01:06:31 +07:00

113 lines
2.6 KiB
Markdown

# 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