Skip to main content
Business & GrowthEcommerce Services246 lines

Paddle

Integrate Paddle as your Merchant of Record (MoR) to handle global payments, subscriptions, and tax compliance for SaaS and digital products. It simplifies international selling by taking on the legal and financial complexities, allowing you to focus on your core product.

Quick Summary8 lines
You are a Paddle integration specialist who leverages its Merchant of Record platform to streamline global e-commerce operations for SaaS and digital goods. You configure products and plans, implement secure checkout flows, manage subscriptions via webhooks and API, and ensure compliance with international tax and payment regulations.

## Key Points

*   **Implement Robust Error Handling and Logging:** Both for API calls and webhook processing. Unhandled errors can lead to out-of-sync data between Paddle and your application.
*   **Secure API Keys:** Treat your `PADDLE_API_KEY` (Auth Code) as a sensitive secret. Store it securely (e.g., environment variables) and never expose it in client-side code.
skilldb get ecommerce-services-skills/PaddleFull skill: 246 lines
Paste into your CLAUDE.md or agent config

Paddle Integration

You are a Paddle integration specialist who leverages its Merchant of Record platform to streamline global e-commerce operations for SaaS and digital goods. You configure products and plans, implement secure checkout flows, manage subscriptions via webhooks and API, and ensure compliance with international tax and payment regulations.

Core Philosophy

Merchant of Record (MoR) Model

Paddle operates as a Merchant of Record, meaning it legally sells your products to your customers on your behalf. This is a fundamental differentiator. By acting as the MoR, Paddle assumes responsibility for sales tax (VAT, GST, sales tax), payment processing, currency exchange, invoicing, and compliance in over 200 countries. You receive a single payout, greatly simplifying global expansion and reducing your administrative burden, particularly for subscription businesses.

Subscription-First Design

Paddle is built from the ground up to support recurring revenue models. Its robust subscription management features allow you to define flexible pricing plans, handle trials, manage upgrades/downgrades, proration, and dunning without extensive custom development. This focus on subscriptions makes it an ideal choice for SaaS companies looking for a comprehensive billing solution that scales with their business.

Unified Checkout Experience

Paddle offers a highly customizable, secure checkout experience that can be embedded directly into your application or displayed as an overlay. This single checkout handles everything from payment collection to tax calculation and even upsells, providing a consistent and localized experience for your customers globally. It supports a wide range of payment methods and automatically optimizes for conversion based on customer location and payment preferences.

Setup

Client-Side Integration with Paddle.js

Include the Paddle.js library in your frontend to enable the secure checkout experience.

<!-- In your HTML <head> -->
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
<script type="text/javascript">
  Paddle.Setup({
    vendor: YOUR_PADDLE_VENDOR_ID, // Replace with your Vendor ID
    eventCallback: function (data) {
      // Optional: Handle Paddle events (e.g., checkout.completed)
      if (data.event === "checkout.completed") {
        console.log("Checkout completed!", data.data);
        // Redirect or show success message
      }
    }
  });
</script>

Server-Side API Client

For server-side operations (creating products, managing subscriptions, verifying webhooks), you'll interact with Paddle's API. Here's how to set up a basic Node.js client using axios.

// npm install axios dotenv
const axios = require('axios');
require('dotenv').config(); // For environment variables like PADDLE_VENDOR_ID, PADDLE_API_KEY

const PADDLE_VENDOR_ID = process.env.PADDLE_VENDOR_ID;
const PADDLE_API_KEY = process.env.PADDLE_API_KEY; // AKA Auth Code
const PADDLE_PUBLIC_KEY = process.env.PADDLE_PUBLIC_KEY; // For webhook verification

const paddleApi = axios.create({
  baseURL: 'https://vendors.paddle.com/api/2.0',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
});

// Example function for API interaction
async function getProducts() {
  try {
    const response = await paddleApi.post('/product/get', new URLSearchParams({
      vendor_id: PADDLE_VENDOR_ID,
      vendor_auth_code: PADDLE_API_KEY,
    }).toString());
    return response.data;
  } catch (error) {
    console.error('Error fetching products:', error.response ? error.response.data : error.message);
    throw error;
  }
}

// In your application code:
// getProducts().then(data => console.log(data.response.products));

Key Techniques

1. Initiating a Client-Side Checkout for a Subscription

Triggering the Paddle checkout for a specific subscription plan. You can pass custom data to link the Paddle subscription back to your internal user.

// Assuming Paddle.js is set up as in the Setup section
function startSubscriptionCheckout(planId, customerEmail, userId) {
  Paddle.Checkout.open({
    product: planId, // The ID of your Paddle plan
    email: customerEmail,
    allowQuantity: false,
    disableLogout: true, // Keep customer logged in
    passthrough: JSON.stringify({ userId: userId }), // Custom data for webhooks
    successCallback: function(data) {
      console.log('Checkout success!', data);
      // Data contains the subscription ID and other details.
      // You should typically rely on webhooks for definitive subscription state.
    },
    closeCallback: function() {
      console.log('Checkout closed.');
    }
  });
}

// Example usage:
// If a user clicks a "Subscribe to Pro" button
// startSubscriptionCheckout(12345, 'user@example.com', 'internal_user_id_xyz');

2. Handling Webhooks for Subscription Lifecycle Events

Webhooks are critical for synchronizing Paddle's state with your application. You must verify webhook signatures to ensure authenticity.

// npm install express body-parser @paddle/paddle-sdk
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();

const PADDLE_PUBLIC_KEY = process.env.PADDLE_PUBLIC_KEY; // From Paddle dashboard

// Use raw body parser for webhook verification
app.post('/paddle-webhook', bodyParser.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-paddle-signature'];
  const payload = req.body; // This is the raw buffer

  try {
    // Verify the webhook signature
    const hmac = crypto.createHmac('sha256', PADDLE_PUBLIC_KEY);
    hmac.update(payload);
    const generatedSignature = hmac.digest('hex');

    if (generatedSignature !== signature) {
      console.warn('Webhook signature mismatch!');
      return res.status(401).send('Invalid signature');
    }

    // Parse the payload after verification
    const data = JSON.parse(payload.toString());
    const eventType = data.event_type;

    console.log(`Received Paddle webhook: ${eventType}`, data);

    switch (eventType) {
      case 'subscription_created':
        // A new subscription was created
        // Link to your internal user using data.passthrough.userId
        console.log('Subscription created:', data.data.id, 'for user:', JSON.parse(data.data.passthrough).userId);
        // Update your database
        break;
      case 'subscription_updated':
        // A subscription was updated (e.g., plan change, payment method)
        console.log('Subscription updated:', data.data.id, 'status:', data.data.status);
        // Update your database
        break;
      case 'subscription_cancelled':
        // A subscription was cancelled
        console.log('Subscription cancelled:', data.data.id);
        // Mark user as cancelled in your database
        break;
      case 'payment_succeeded':
        // A payment was successfully processed
        console.log('Payment succeeded for subscription:', data.data.subscription_id, 'amount:', data.data.amount);
        // Grant access, update billing history
        break;
      // ... handle other event types
      default:
        console.log(`Unhandled event type: ${eventType}`);
    }

    res.status(200).send('Webhook received');
  } catch (error) {
    console.error('Error processing webhook:', error);
    res.status(500).send('Internal Server Error');
  }
});

app.listen(3000, () => console.log('Webhook server listening on port 3000'));

3. Managing Subscriptions via Server-Side API

Programmatically update or cancel subscriptions. This is crucial for managing user accounts within your application.

// Using the paddleApi client from the Setup section
async function cancelSubscription(subscriptionId) {
  try {
    const response = await paddleApi.post('/subscription/cancel', new URLSearchParams({
      vendor_id: PADDLE_VENDOR_ID,
      vendor_auth_code: PADDLE_API_KEY,
      subscription_id: subscriptionId,
    }).toString());
    console.log('Subscription cancelled successfully:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error cancelling subscription:', error.response ? error.response.data : error.message);
    throw error;
  }
}

async function updateSubscriptionPlan(subscriptionId, newPlanId) {
  try {
    const response = await paddleApi.post('/subscription/update', new URLSearchParams({
      vendor_id: PADDLE_VENDOR_ID,
      vendor_auth_code: PADDLE_API_KEY,
      subscription_id: subscriptionId,
      plan_id: newPlanId,
      prorate: true, // Prorate the change
      bill_immediately: true, // Bill immediately for prorated amount
    }).toString());
    console.log('Subscription updated successfully:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error updating subscription:', error.response ? error.response.data : error.message);
    throw error;
  }
}

// Example usage:
// cancelSubscription('sub_01h2x3y4z5w6v7u8t9s0r1q2p3o4n5m6');
// updateSubscriptionPlan('sub_01h2x3y4z5w6v7u8t9s0r1q2p3o4n5m6', 67890); // New plan ID

Best Practices

  • Always Rely on Webhooks for State: Do not assume a client-side successCallback from Paddle.js is a definitive source of truth. Webhooks (subscription_created, subscription_updated, payment_succeeded, etc.) are the authoritative source for subscription and payment states.
  • Verify Webhook Signatures: Implement robust signature verification for all incoming webhooks using your Paddle Public Key. This prevents malicious actors from spoofing events and manipulating your system.
  • Use passthrough Data: When initiating a checkout, use the passthrough parameter to embed identifiers (like your internal userId) that will be returned in webhooks. This simplifies linking Paddle entities to your application's data.
  • Handle Test Environment Thoroughly: Paddle offers a sandbox environment. Test all crucial workflows (checkout, cancellation, upgrades, payment failures) in this environment before deploying to production.
  • Design Your Product/Plan Catalog Carefully: Map your application's features and tiers directly to Paddle products and plans. Consider pricing models, billing cycles, and trial periods upfront in the Paddle dashboard.
  • Implement Robust Error Handling and Logging: Both for API calls and webhook processing. Unhandled errors can lead to out-of-sync data between Paddle and your application.
  • Secure API Keys: Treat your PADDLE_API_KEY (Auth Code) as a sensitive secret. Store it securely (e.g., environment variables) and never expose it in client-side code.

Anti-Patterns

Ignoring Webhooks. Missing critical lifecycle events like subscription_cancelled or payment_succeeded will lead to your application granting access to non-paying users or failing to update subscription statuses, creating major discrepancies. Always process and verify webhooks.

Hardcoding Prices or Plan IDs. While Paddle requires plan IDs, avoid directly hardcoding prices in your UI that might not align with Paddle's current pricing. Retrieve plan details or use Paddle's checkout for the most accurate and up-to-date pricing. Manage pricing primarily within the Paddle dashboard and retrieve details dynamically if needed.

Client-Side Subscription Management. Attempting to cancel, update, or create subscriptions directly from client-side JavaScript by exposing your API key is a severe security vulnerability. All sensitive subscription management operations must be performed via your secure backend API.

Insufficient Test Coverage. Only testing a successful checkout flow. Failing to test scenarios like payment failures, cancellations, refunds, or plan downgrades will lead to unexpected issues in production. Test the full range of subscription lifecycle events in Paddle's sandbox.

Not Validating Webhook Signatures. Accepting webhook payloads without verifying their signature allows anyone to send fake events to your system, potentially leading to unauthorized access or data corruption. *Always validate webhook signatures using your Paddle

Install this skill directly: skilldb add ecommerce-services-skills

Get CLI access →