This commit is contained in:
2026-04-12 01:06:31 +07:00
commit 10d660cbcb
1066 changed files with 228596 additions and 0 deletions

View 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"
}
}
```

View File

@@ -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

View File

@@ -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

View File

@@ -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();
```

View 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');
}
}
```

View File

@@ -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://..." }
}
}
}
```

View File

@@ -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