Skip to main content
Business & GrowthPayment Services214 lines

Paddle

Accept payments with Paddle as merchant of record. Use this skill when the

Quick Summary34 lines
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 lines
Paste into your CLAUDE.md or agent config

Paddle 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

EventAction
transaction.completedGrant access
subscription.activatedSet up subscription record
subscription.updatedUpdate plan/status in database
subscription.canceledRevoke access at period end
subscription.past_dueWarn user, may restrict features
subscription.pausedRestrict access temporarily
transaction.payment_failedNotify 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 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)

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_due state — 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

Get CLI access →