Paddle
Accept payments with Paddle as merchant of record. Use this skill when the
You are a payments specialist who integrates Paddle into projects. Paddle is a
merchant of record (MoR) — it handles tax collection, compliance, invoicing, and
payouts globally. You sell through Paddle, not directly to customers.
## Key Points
- Dashboard: `sandbox-vendors.paddle.com`
- API: `sandbox-api.paddle.com`
- Use test card numbers for checkout testing
- Use `customData` on all transactions and subscriptions to link to your users
- Always verify webhook signatures
- Handle `subscription.past_due` — give users a grace period to update payment
- Use `effectiveFrom: 'next_billing_period'` for cancellations to honor paid time
- Use Paddle's pricing preview API for localized pricing on your pricing page
- Test the full lifecycle in sandbox before going live
- Use Paddle Retain for subscription recovery (failed payment dunning)
- Granting access on checkout completion callback without webhook verification
- Not storing Paddle customer IDs in your database
## Quick Example
```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: process.env.PADDLE_ENV === 'sandbox' ? 'sandbox' : 'production',
});
```skilldb get payment-services-skills/PaddleFull skill: 214 linesPaddle Payment Integration
You are a payments specialist who integrates Paddle into projects. Paddle is a merchant of record (MoR) — it handles tax collection, compliance, invoicing, and payouts globally. You sell through Paddle, not directly to customers.
Core Philosophy
Merchant of record simplifies everything
Paddle is the legal seller. It handles VAT/GST collection, invoicing, refunds, and chargebacks. You don't need to register for tax in each country or handle PCI compliance. The trade-off is less control over the payment experience and Paddle's fee.
Products and prices live in Paddle
Unlike Stripe where you can create prices on the fly, Paddle products and prices are configured in the Paddle dashboard. Your code references price IDs, not amounts.
Webhooks are the integration backbone
Every state change in Paddle triggers a webhook. Your application must process webhooks to stay in sync — subscriptions, payments, refunds, and cancellations.
Setup
Install
npm install @paddle/paddle-node-sdk
Initialize
import { Paddle } from '@paddle/paddle-node-sdk';
const paddle = new Paddle(process.env.PADDLE_API_KEY, {
environment: process.env.PADDLE_ENV === 'sandbox' ? 'sandbox' : 'production',
});
Key Techniques
Checkout (Paddle.js overlay)
<!-- Include Paddle.js -->
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
<script>
Paddle.Initialize({
token: 'live_xxx', // Client-side token
eventCallback: function(data) {
if (data.name === 'checkout.completed') {
// Redirect to success page
}
}
});
</script>
// Open checkout
Paddle.Checkout.open({
items: [{ priceId: 'pri_xxx', quantity: 1 }],
customer: { email: 'user@example.com' },
customData: { userId: '123' },
settings: {
successUrl: 'https://yourdomain.com/success',
theme: 'dark',
},
});
Server-side transaction
const transaction = await paddle.transactions.create({
items: [{ priceId: 'pri_xxx', quantity: 1 }],
customerId: 'ctm_xxx',
customData: { userId: '123' },
});
Subscription management
// Get subscription
const sub = await paddle.subscriptions.get('sub_xxx');
// Update subscription (change plan)
await paddle.subscriptions.update('sub_xxx', {
items: [{ priceId: 'pri_new_xxx', quantity: 1 }],
prorationBillingMode: 'prorated_immediately',
});
// Cancel subscription
await paddle.subscriptions.cancel('sub_xxx', {
effectiveFrom: 'next_billing_period',
});
// Pause subscription
await paddle.subscriptions.pause('sub_xxx');
// Resume subscription
await paddle.subscriptions.resume('sub_xxx', {
effectiveFrom: 'immediately',
});
Customer management
// Create customer
const customer = await paddle.customers.create({
email: 'user@example.com',
name: 'Alice',
customData: { userId: '123' },
});
// List customer subscriptions
const subs = await paddle.subscriptions.list({
customerId: ['ctm_xxx'],
});
Pricing preview
// Get localized pricing for a customer
const preview = await paddle.pricingPreviews.preview({
items: [{ priceId: 'pri_xxx', quantity: 1 }],
address: { countryCode: 'DE' },
});
console.log(preview.details.lineItems[0].formattedTotals);
// { subtotal: '€29.00', tax: '€5.51', total: '€34.51' }
Webhook Processing
| Event | Action |
|---|---|
transaction.completed | Grant access |
subscription.activated | Set up subscription record |
subscription.updated | Update plan/status in database |
subscription.canceled | Revoke access at period end |
subscription.past_due | Warn user, may restrict features |
subscription.paused | Restrict access temporarily |
transaction.payment_failed | Notify user |
import { Paddle } from '@paddle/paddle-node-sdk';
const paddle = new Paddle(process.env.PADDLE_API_KEY);
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get('paddle-signature');
let event;
try {
event = paddle.webhooks.unmarshal(body, process.env.PADDLE_WEBHOOK_SECRET, signature);
} catch {
return new Response('Invalid signature', { status: 401 });
}
switch (event.eventType) {
case 'transaction.completed':
const txn = event.data;
const userId = txn.customData?.userId;
await grantAccess(userId, txn.subscriptionId);
break;
case 'subscription.canceled':
const sub = event.data;
await scheduleAccessRevocation(sub.customData?.userId, sub.currentBillingPeriod?.endsAt);
break;
case 'subscription.past_due':
await notifyPastDue(event.data.customData?.userId);
break;
}
return new Response('OK');
}
Sandbox Testing
Paddle provides a full sandbox environment:
- Dashboard:
sandbox-vendors.paddle.com - API:
sandbox-api.paddle.com - Use test card numbers for checkout testing
Best Practices
- Use
customDataon all transactions and subscriptions to link to your users - Always verify webhook signatures
- Handle
subscription.past_due— give users a grace period to update payment - Use
effectiveFrom: 'next_billing_period'for cancellations to honor paid time - Use Paddle's pricing preview API for localized pricing on your pricing page
- Test the full lifecycle in sandbox before going live
- Use Paddle Retain for subscription recovery (failed payment dunning)
Anti-Patterns
- Granting access on checkout completion callback without webhook verification
- Not storing Paddle customer IDs in your database
- Immediately revoking access on cancellation instead of honoring the paid period
- Creating prices programmatically — Paddle requires dashboard configuration
- Not handling the
past_duestate — users get confused by silent access loss - Ignoring Paddle's tax handling — it's the whole point of using a MoR
Install this skill directly: skilldb add payment-services-skills
Related Skills
Adyen
Accept payments with Adyen. Use this skill when the project needs to integrate
Braintree
Accept payments with Braintree (PayPal). Use this skill when the project needs
Checkout Com
Accept payments with Checkout.com. Use this skill when the project needs to
Coinbase Commerce
Accept cryptocurrency payments with Coinbase Commerce. Use this skill when the
Creem
Accept payments with Creem as merchant of record. Use this skill when the project
Klarna
Accept payments with Klarna. Use this skill when the project needs to integrate