init
This commit is contained in:
116
.opencode/skills/payment-integration/references/paddle/api.md
Normal file
116
.opencode/skills/payment-integration/references/paddle/api.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Paddle API Reference
|
||||
|
||||
Base URL: `https://api.paddle.com` (prod) | `https://sandbox-api.paddle.com` (sandbox)
|
||||
|
||||
## Products
|
||||
|
||||
```bash
|
||||
# Create product
|
||||
POST /products
|
||||
{
|
||||
"name": "Pro Plan",
|
||||
"tax_category": "standard",
|
||||
"description": "Professional subscription"
|
||||
}
|
||||
|
||||
# List products
|
||||
GET /products?status=active
|
||||
```
|
||||
|
||||
## Prices
|
||||
|
||||
```bash
|
||||
# Create price
|
||||
POST /prices
|
||||
{
|
||||
"product_id": "pro_xxx",
|
||||
"description": "Monthly subscription",
|
||||
"unit_price": { "amount": "1999", "currency_code": "USD" },
|
||||
"billing_cycle": { "interval": "month", "frequency": 1 }
|
||||
}
|
||||
|
||||
# One-time price
|
||||
POST /prices
|
||||
{
|
||||
"product_id": "pro_xxx",
|
||||
"unit_price": { "amount": "4999", "currency_code": "USD" }
|
||||
}
|
||||
```
|
||||
|
||||
## Transactions
|
||||
|
||||
```bash
|
||||
# Create transaction (checkout)
|
||||
POST /transactions
|
||||
{
|
||||
"items": [{ "price_id": "pri_xxx", "quantity": 1 }],
|
||||
"customer_id": "ctm_xxx" # optional
|
||||
}
|
||||
|
||||
# Get transaction
|
||||
GET /transactions/{txn_id}
|
||||
```
|
||||
|
||||
## Customers
|
||||
|
||||
```bash
|
||||
# Create customer
|
||||
POST /customers
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"name": "John Doe"
|
||||
}
|
||||
|
||||
# Get customer portal session
|
||||
POST /customers/{ctm_id}/portal-sessions
|
||||
```
|
||||
|
||||
## Subscriptions
|
||||
|
||||
```bash
|
||||
# Get subscription
|
||||
GET /subscriptions/{sub_id}
|
||||
|
||||
# Update subscription
|
||||
PATCH /subscriptions/{sub_id}
|
||||
{
|
||||
"items": [{ "price_id": "pri_new", "quantity": 1 }],
|
||||
"proration_billing_mode": "prorated_immediately"
|
||||
}
|
||||
|
||||
# Cancel subscription
|
||||
POST /subscriptions/{sub_id}/cancel
|
||||
{
|
||||
"effective_from": "next_billing_period"
|
||||
}
|
||||
|
||||
# Pause subscription
|
||||
POST /subscriptions/{sub_id}/pause
|
||||
{
|
||||
"effective_from": "next_billing_period"
|
||||
}
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"data": { ... },
|
||||
"meta": {
|
||||
"request_id": "xxx",
|
||||
"pagination": { "per_page": 50, "next": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"type": "request_error",
|
||||
"code": "entity_not_found",
|
||||
"detail": "Product not found"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,130 @@
|
||||
# Paddle Best Practices
|
||||
|
||||
Production patterns for reliable integration.
|
||||
|
||||
## Webhook Handling
|
||||
|
||||
```typescript
|
||||
// 1. Verify signature first
|
||||
// 2. Check idempotency
|
||||
// 3. Process async
|
||||
// 4. Return 200 immediately
|
||||
|
||||
const processedEvents = new Set(); // Use Redis in production
|
||||
|
||||
app.post('/webhooks/paddle', async (req, res) => {
|
||||
const signature = req.headers['paddle-signature'];
|
||||
|
||||
// Verify
|
||||
const event = paddle.webhooks.unmarshal(
|
||||
req.rawBody,
|
||||
process.env.PADDLE_WEBHOOK_SECRET,
|
||||
signature
|
||||
);
|
||||
|
||||
// Idempotency
|
||||
if (processedEvents.has(event.eventId)) {
|
||||
return res.status(200).send('Already processed');
|
||||
}
|
||||
|
||||
// Acknowledge immediately
|
||||
res.status(200).send('OK');
|
||||
|
||||
// Process async
|
||||
await queue.add('paddle-webhook', event);
|
||||
});
|
||||
```
|
||||
|
||||
## Subscription Status Sync
|
||||
|
||||
```typescript
|
||||
// Always verify subscription status server-side
|
||||
async function checkAccess(userId: string): Promise<boolean> {
|
||||
const user = await db.users.findOne({ id: userId });
|
||||
if (!user.paddleSubscriptionId) return false;
|
||||
|
||||
const sub = await paddle.subscriptions.get(user.paddleSubscriptionId);
|
||||
return ['active', 'trialing'].includes(sub.status);
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Data for User Linking
|
||||
|
||||
```typescript
|
||||
// Pass user_id in checkout
|
||||
paddle.Checkout.open({
|
||||
items: [{ priceId: 'pri_xxx', quantity: 1 }],
|
||||
customData: { user_id: currentUser.id }
|
||||
});
|
||||
|
||||
// Retrieve in webhook
|
||||
app.post('/webhooks/paddle', async (req, res) => {
|
||||
const event = paddle.webhooks.unmarshal(...);
|
||||
|
||||
if (event.eventType === 'subscription.created') {
|
||||
const userId = event.data.customData?.user_id;
|
||||
await db.users.update(userId, {
|
||||
paddleSubscriptionId: event.data.id,
|
||||
paddleCustomerId: event.data.customerId
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Error Recovery
|
||||
|
||||
```typescript
|
||||
// Handle past_due subscriptions
|
||||
async function handlePastDue(subscriptionId: string) {
|
||||
// Get customer portal for payment update
|
||||
const sub = await paddle.subscriptions.get(subscriptionId);
|
||||
const portal = await paddle.customers.createPortalSession(sub.customerId);
|
||||
|
||||
// Email customer with portal link
|
||||
await sendEmail(sub.customer.email, {
|
||||
subject: 'Update your payment method',
|
||||
link: portal.urls.general.overview
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Testing with Sandbox
|
||||
|
||||
```typescript
|
||||
// Use sandbox environment
|
||||
const paddle = new Paddle(process.env.PADDLE_API_KEY, {
|
||||
environment: 'sandbox'
|
||||
});
|
||||
|
||||
// Sandbox card: 4242 4242 4242 4242
|
||||
// Any future expiry, any CVC
|
||||
```
|
||||
|
||||
## Price Localization
|
||||
|
||||
```typescript
|
||||
// Preview localized prices before checkout
|
||||
const preview = await paddle.PricePreview({
|
||||
items: [{ priceId: 'pri_xxx', quantity: 1 }],
|
||||
address: { countryCode: customerCountry }
|
||||
});
|
||||
|
||||
// Display localized price
|
||||
const formattedPrice = preview.data.details.totals.total;
|
||||
```
|
||||
|
||||
## Paddle Retain (Churn Prevention)
|
||||
|
||||
Features enabled in dashboard:
|
||||
- **Payment recovery**: Automated dunning emails
|
||||
- **Cancellation surveys**: Collect feedback + offer discounts
|
||||
- **Term optimization**: Auto-upgrade annual suggestions
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Webhook signatures verified
|
||||
- [ ] API keys in env vars, not code
|
||||
- [ ] Separate keys for sandbox/production
|
||||
- [ ] Idempotency implemented
|
||||
- [ ] Server-side status verification
|
||||
- [ ] Secure customer portal sessions
|
||||
@@ -0,0 +1,57 @@
|
||||
# Paddle Overview
|
||||
|
||||
Paddle Billing = merchant-of-record platform handling payments, tax compliance, localization, subscriptions globally.
|
||||
|
||||
## Authentication
|
||||
|
||||
```bash
|
||||
# API Key in Authorization header
|
||||
curl -X GET "https://api.paddle.com/products" \
|
||||
-H "Authorization: Bearer {api_key}"
|
||||
```
|
||||
|
||||
Environment:
|
||||
- Production: `api.paddle.com`
|
||||
- Sandbox: `sandbox-api.paddle.com`
|
||||
|
||||
## Core Concepts
|
||||
|
||||
| Concept | Description |
|
||||
|---------|-------------|
|
||||
| **Paddle ID** | Unique identifier for all entities (`pro_xxx`, `pri_xxx`, `txn_xxx`) |
|
||||
| **MoR** | Paddle is merchant-of-record, handles tax/compliance |
|
||||
| **Localization** | Auto currency/language based on customer location |
|
||||
|
||||
## SDK Installation
|
||||
|
||||
```bash
|
||||
# Node.js
|
||||
npm install @paddle/paddle-node-sdk
|
||||
|
||||
# Python
|
||||
pip install paddle-python-sdk
|
||||
|
||||
# PHP
|
||||
composer require paddle/paddle-php-sdk
|
||||
|
||||
# Go
|
||||
go get github.com/PaddleHQ/paddle-go-sdk
|
||||
```
|
||||
|
||||
## Entity Prefixes
|
||||
|
||||
| Entity | Prefix | Example |
|
||||
|--------|--------|---------|
|
||||
| Product | `pro_` | `pro_01gsz4vmqbjk3x4vvtafffd540` |
|
||||
| Price | `pri_` | `pri_01gsz8z1q1n00f12qt82y31smh` |
|
||||
| Customer | `ctm_` | `ctm_01grnn4zta5a1mf02jjze7y2ys` |
|
||||
| Subscription | `sub_` | `sub_01gv2z5ht1mk2y6bsgv2mjryyn` |
|
||||
| Transaction | `txn_` | `txn_01gv2z5ht1mk2y6bsgv2mjryyn` |
|
||||
|
||||
## Quick Links
|
||||
|
||||
- API Reference: https://developer.paddle.com/api-reference/overview
|
||||
- Paddle.js: `references/paddle/paddle-js.md`
|
||||
- Webhooks: `references/paddle/webhooks.md`
|
||||
- Subscriptions: `references/paddle/subscriptions.md`
|
||||
- External llms.txt: https://developer.paddle.com/llms.txt
|
||||
@@ -0,0 +1,106 @@
|
||||
# Paddle.js v2
|
||||
|
||||
Client-side library for checkout and pricing.
|
||||
|
||||
## Installation
|
||||
|
||||
```html
|
||||
<!-- CDN -->
|
||||
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
|
||||
```
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install @paddle/paddle-js
|
||||
```
|
||||
|
||||
## Initialization
|
||||
|
||||
```typescript
|
||||
import { initializePaddle } from '@paddle/paddle-js';
|
||||
|
||||
const paddle = await initializePaddle({
|
||||
environment: 'sandbox', // 'production'
|
||||
token: 'live_xxx', // client-side token
|
||||
eventCallback: (event) => {
|
||||
if (event.name === 'checkout.completed') {
|
||||
console.log('Payment successful', event.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Checkout Methods
|
||||
|
||||
### Overlay Checkout (Modal)
|
||||
|
||||
```typescript
|
||||
paddle.Checkout.open({
|
||||
items: [{ priceId: 'pri_xxx', quantity: 1 }],
|
||||
customer: { email: 'user@example.com' },
|
||||
customData: { user_id: '123' },
|
||||
successUrl: 'https://example.com/success',
|
||||
});
|
||||
```
|
||||
|
||||
### Inline Checkout (Embedded)
|
||||
|
||||
```html
|
||||
<div class="paddle-checkout-container"></div>
|
||||
```
|
||||
|
||||
```typescript
|
||||
paddle.Checkout.open({
|
||||
items: [{ priceId: 'pri_xxx', quantity: 1 }],
|
||||
settings: {
|
||||
displayMode: 'inline',
|
||||
frameTarget: 'paddle-checkout-container',
|
||||
frameStyle: 'width: 100%; min-width: 312px; background-color: transparent;'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### HTML Data Attributes
|
||||
|
||||
```html
|
||||
<a
|
||||
href="#"
|
||||
data-paddle-product="pri_xxx"
|
||||
data-paddle-quantity="1"
|
||||
data-paddle-email="user@example.com"
|
||||
>Buy Now</a>
|
||||
```
|
||||
|
||||
## Price Preview
|
||||
|
||||
```typescript
|
||||
const preview = await paddle.PricePreview({
|
||||
items: [{ priceId: 'pri_xxx', quantity: 1 }],
|
||||
address: { countryCode: 'US' }
|
||||
});
|
||||
|
||||
console.log(preview.data.details.totals.total); // "19.99"
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
| `checkout.loaded` | Checkout frame loaded |
|
||||
| `checkout.customer.created` | New customer created |
|
||||
| `checkout.payment.initiated` | Payment processing started |
|
||||
| `checkout.completed` | Payment successful |
|
||||
| `checkout.closed` | Checkout closed |
|
||||
| `checkout.error` | Payment failed |
|
||||
|
||||
## Update Checkout
|
||||
|
||||
```typescript
|
||||
// Update items after open
|
||||
paddle.Checkout.updateItems([
|
||||
{ priceId: 'pri_xxx', quantity: 2 }
|
||||
]);
|
||||
|
||||
// Close checkout
|
||||
paddle.Checkout.close();
|
||||
```
|
||||
131
.opencode/skills/payment-integration/references/paddle/sdk.md
Normal file
131
.opencode/skills/payment-integration/references/paddle/sdk.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Paddle SDKs
|
||||
|
||||
Official SDKs for server-side integration.
|
||||
|
||||
## Node.js
|
||||
|
||||
```bash
|
||||
npm install @paddle/paddle-node-sdk
|
||||
```
|
||||
|
||||
```typescript
|
||||
import Paddle from '@paddle/paddle-node-sdk';
|
||||
|
||||
const paddle = new Paddle(process.env.PADDLE_API_KEY, {
|
||||
environment: 'sandbox' // 'production'
|
||||
});
|
||||
|
||||
// Products
|
||||
const products = await paddle.products.list();
|
||||
const product = await paddle.products.create({
|
||||
name: 'Pro Plan',
|
||||
taxCategory: 'standard'
|
||||
});
|
||||
|
||||
// Prices
|
||||
const prices = await paddle.prices.list({ productId: 'pro_xxx' });
|
||||
const price = await paddle.prices.create({
|
||||
productId: 'pro_xxx',
|
||||
description: 'Monthly',
|
||||
unitPrice: { amount: '999', currencyCode: 'USD' },
|
||||
billingCycle: { interval: 'month', frequency: 1 }
|
||||
});
|
||||
|
||||
// Transactions
|
||||
const transaction = await paddle.transactions.create({
|
||||
items: [{ priceId: 'pri_xxx', quantity: 1 }]
|
||||
});
|
||||
|
||||
// Subscriptions
|
||||
const subscription = await paddle.subscriptions.get('sub_xxx');
|
||||
await paddle.subscriptions.update('sub_xxx', {
|
||||
items: [{ priceId: 'pri_new', quantity: 1 }]
|
||||
});
|
||||
await paddle.subscriptions.cancel('sub_xxx', { effectiveFrom: 'nextBillingPeriod' });
|
||||
|
||||
// Customers
|
||||
const customers = await paddle.customers.list({ email: 'user@example.com' });
|
||||
```
|
||||
|
||||
## Python
|
||||
|
||||
```bash
|
||||
pip install paddle-python-sdk
|
||||
```
|
||||
|
||||
```python
|
||||
from paddle_billing import Client, Environment
|
||||
|
||||
paddle = Client(
|
||||
api_key="your_api_key",
|
||||
options=Options(environment=Environment.SANDBOX)
|
||||
)
|
||||
|
||||
# Products
|
||||
products = paddle.products.list()
|
||||
product = paddle.products.create(
|
||||
name="Pro Plan",
|
||||
tax_category="standard"
|
||||
)
|
||||
|
||||
# Subscriptions
|
||||
subscription = paddle.subscriptions.get("sub_xxx")
|
||||
paddle.subscriptions.cancel(
|
||||
"sub_xxx",
|
||||
effective_from="next_billing_period"
|
||||
)
|
||||
```
|
||||
|
||||
## PHP
|
||||
|
||||
```bash
|
||||
composer require paddle/paddle-php-sdk
|
||||
```
|
||||
|
||||
```php
|
||||
use Paddle\SDK\Client;
|
||||
|
||||
$paddle = new Client('your_api_key');
|
||||
|
||||
// Products
|
||||
$products = $paddle->products->list();
|
||||
|
||||
// Subscriptions
|
||||
$subscription = $paddle->subscriptions->get('sub_xxx');
|
||||
$paddle->subscriptions->cancel('sub_xxx', [
|
||||
'effective_from' => 'next_billing_period'
|
||||
]);
|
||||
```
|
||||
|
||||
## Go
|
||||
|
||||
```bash
|
||||
go get github.com/PaddleHQ/paddle-go-sdk
|
||||
```
|
||||
|
||||
```go
|
||||
import paddle "github.com/PaddleHQ/paddle-go-sdk"
|
||||
|
||||
client, _ := paddle.New(
|
||||
os.Getenv("PADDLE_API_KEY"),
|
||||
paddle.WithBaseURL(paddle.SandboxBaseURL),
|
||||
)
|
||||
|
||||
// Products
|
||||
products, _ := client.ListProducts(ctx, nil)
|
||||
|
||||
// Subscriptions
|
||||
sub, _ := client.GetSubscription(ctx, "sub_xxx")
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await paddle.subscriptions.get('sub_invalid');
|
||||
} catch (error) {
|
||||
if (error.code === 'entity_not_found') {
|
||||
console.log('Subscription not found');
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,118 @@
|
||||
# Paddle Subscriptions
|
||||
|
||||
Full subscription lifecycle management.
|
||||
|
||||
## Create Subscription
|
||||
|
||||
Via checkout (customer initiates):
|
||||
```typescript
|
||||
paddle.Checkout.open({
|
||||
items: [{ priceId: 'pri_monthly', quantity: 1 }],
|
||||
customer: { email: 'user@example.com' }
|
||||
});
|
||||
```
|
||||
|
||||
## Subscription States
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| `trialing` | In trial period |
|
||||
| `active` | Actively billed |
|
||||
| `past_due` | Payment failed, retrying |
|
||||
| `paused` | Temporarily suspended |
|
||||
| `canceled` | Terminated |
|
||||
|
||||
## Upgrade/Downgrade
|
||||
|
||||
```typescript
|
||||
// API: Update subscription items
|
||||
PATCH /subscriptions/{sub_id}
|
||||
{
|
||||
"items": [{ "price_id": "pri_annual", "quantity": 1 }],
|
||||
"proration_billing_mode": "prorated_immediately"
|
||||
}
|
||||
```
|
||||
|
||||
Proration modes:
|
||||
- `prorated_immediately` - Charge/credit now
|
||||
- `prorated_next_billing_period` - Apply next cycle
|
||||
- `full_immediately` - Full new price now
|
||||
- `full_next_billing_period` - Full price next cycle
|
||||
- `do_not_bill` - No charge for change
|
||||
|
||||
## Multi-Item Subscriptions
|
||||
|
||||
```typescript
|
||||
// Add item to existing subscription
|
||||
PATCH /subscriptions/{sub_id}
|
||||
{
|
||||
"items": [
|
||||
{ "price_id": "pri_base", "quantity": 1 },
|
||||
{ "price_id": "pri_addon", "quantity": 5 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Trials
|
||||
|
||||
Set trial on price:
|
||||
```typescript
|
||||
POST /prices
|
||||
{
|
||||
"product_id": "pro_xxx",
|
||||
"unit_price": { "amount": "999", "currency_code": "USD" },
|
||||
"billing_cycle": { "interval": "month", "frequency": 1 },
|
||||
"trial_period": { "interval": "day", "frequency": 14 }
|
||||
}
|
||||
```
|
||||
|
||||
## Pause/Resume
|
||||
|
||||
```typescript
|
||||
// Pause at end of period
|
||||
POST /subscriptions/{sub_id}/pause
|
||||
{
|
||||
"effective_from": "next_billing_period"
|
||||
}
|
||||
|
||||
// Resume immediately
|
||||
POST /subscriptions/{sub_id}/resume
|
||||
{
|
||||
"effective_from": "immediately"
|
||||
}
|
||||
```
|
||||
|
||||
## Cancel
|
||||
|
||||
```typescript
|
||||
// Cancel at end of period
|
||||
POST /subscriptions/{sub_id}/cancel
|
||||
{
|
||||
"effective_from": "next_billing_period"
|
||||
}
|
||||
|
||||
// Cancel immediately
|
||||
POST /subscriptions/{sub_id}/cancel
|
||||
{
|
||||
"effective_from": "immediately"
|
||||
}
|
||||
```
|
||||
|
||||
## Customer Portal
|
||||
|
||||
Self-service subscription management:
|
||||
```typescript
|
||||
// Get portal URL
|
||||
POST /customers/{ctm_id}/portal-sessions
|
||||
|
||||
// Response
|
||||
{
|
||||
"data": {
|
||||
"id": "cps_xxx",
|
||||
"customer_id": "ctm_xxx",
|
||||
"urls": {
|
||||
"general": { "overview": "https://..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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