Claude Skills Guide

Claude Code Paddle Billing Integration Setup Guide

Integrating billing into your application doesn’t have to be a nightmare. Paddle, a merchant of record platform, simplifies subscription management and payment processing. Combined with Claude Code’s AI-assisted development capabilities, you can set up a robust billing system in hours rather than days.

This guide walks you through integrating Paddle billing using Claude Code, covering everything from initial setup to handling webhooks and managing subscriptions.

Defining a Paddle Billing Skill

Before writing integration code, you can define a Claude skill that encapsulates the entire billing workflow. Skills give Claude a focused persona and explicit instructions for handling events:

---
name: paddle-billing
description: Handles Paddle billing events and manages subscription workflows
---

# Paddle Billing Workflow Handler

You handle incoming Paddle webhook events and execute appropriate billing workflows. When you receive an event:

1. Parse the event payload to identify the event type
2. Log the event for audit purposes
3. Execute the appropriate workflow based on event type
4. Update local records if needed

## Event Types

Handle these Paddle event types:
- subscription_created: New subscription activated
- subscription_updated: Subscription modified
- subscription_cancelled: Subscription terminated
- subscription_payment_succeeded: Payment received
- subscription_payment_failed: Payment declined
- invoice_created: New invoice generated

## Processing Events

When processing an event:
1. Extract the subscription_id and customer_id
2. Look up the customer in your database
3. Execute business logic based on event type
4. Return a summary of actions taken

With the skill in place, Claude Code uses it as context when you ask billing-related questions, keeping generated code consistent with your workflow design.

Why Paddle + Claude Code?

Paddle handles the complexity of global tax compliance, invoice generation, and subscription management. When you pair it with Claude Code, you get AI assistance that understands your codebase and can generate boilerplate code, debug issues, and suggest improvements.

Claude Code excels at:

Prerequisites

Before starting, ensure you have:

Step 1: Initialize Your Project with Claude Code

Start by asking Claude Code to help set up your billing module structure:

You: Help me set up a Paddle billing integration. I need webhook handlers, a subscription service, and API endpoints for managing subscriptions.

Claude Code will analyze your project structure and generate the appropriate files. For a Node.js/Express project, expect something like:

// src/billing/paddle-client.ts
import paddle from '@paddle/paddle-node-sdk';

export const paddleClient = new paddle.Client(
  process.env.PADDLE_API_KEY,
  { environment: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox' }
);

Step 2: Configure Environment Variables

Never hardcode your Paddle API keys. Create a .env.example file and ask Claude Code to update your configuration:

You: Add Paddle environment configuration to my existing config module.

Your .env should include:

PADDLE_API_KEY=your_sandbox_api_key
PADDLE_WEBHOOK_SECRET=your_webhook_secret
PADDLE_VENDOR_ID=your_vendor_id

Step 3: Implement Webhook Handlers

Webhooks are critical for billing integrations. When a subscription is created, updated, or cancelled, Paddle sends events to your server. Claude Code can generate robust webhook handlers:

// src/billing/webhooks.ts
import { Request, Response } from 'express';
import { paddleClient } from './paddle-client';
import { prisma } from '../db/client';

export async function handlePaddleWebhook(req: Request, res: Response) {
  const event = req.body;
  
  // Verify webhook signature
  const signature = req.headers['paddle-signature'] as string;
  if (!paddleClient.verifyWebhookSignature(signature, req.body, process.env.PADDLE_WEBHOOK_SECRET!)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  switch (event.event_type) {
    case 'subscription.created':
      await handleSubscriptionCreated(event);
      break;
    case 'subscription.updated':
      await handleSubscriptionUpdated(event);
      break;
    case 'subscription.canceled':
      await handleSubscriptionCanceled(event);
      break;
    case 'transaction.completed':
      await handleTransactionCompleted(event);
      break;
  }

  res.status(200).json({ received: true });
}

async function handleSubscriptionCreated(event: any) {
  const { customer_id, subscription_id, items } = event.data;
  
  await prisma.user.updateMany({
    where: { paddleCustomerId: customer_id },
    data: { 
      subscriptionId: subscription_id,
      subscriptionStatus: 'active',
      planId: items[0].price.product_id
    }
  });
}

If your backend uses Python, Claude Code can generate an equivalent signature verification function using the standard hmac library:

# process_subscription_event.py
import os
import hmac
import hashlib

def verify_webhook_signature(payload: str, signature: str) -> bool:
    """Verify that the webhook came from Paddle"""
    secret = os.environ.get('PADDLE_WEBHOOK_SECRET')
    expected = hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

The Python and TypeScript implementations follow the same HMAC-SHA256 pattern — always use compare_digest (or its equivalent) to prevent timing attacks.

Step 4: Create Subscription Management APIs

Claude Code can generate the RESTful endpoints for subscription management:

// src/api/subscriptions.ts
router.get('/subscriptions', requireAuth, async (req, res) => {
  const user = req.user;
  
  const subscriptions = await paddleClient.subscriptions.list({
    customerId: user.paddleCustomerId
  });

  res.json(subscriptions);
});

router.post('/subscriptions', requireAuth, async (req, res) => {
  const { priceId, paymentMethodId } = req.body;
  
  const subscription = await paddleClient.subscriptions.create({
    customerId: req.user.paddleCustomerId,
    items: [{ priceId }],
    paymentMethod: paymentMethodId,
    currency: 'USD',
    recurring: true
  });

  res.status(201).json(subscription);
});

router.post('/subscriptions/:id/cancel', requireAuth, async (req, res) => {
  const { id } = req.params;
  const { immediate } = req.body;

  const result = await paddleClient.subscriptions.cancel(id, {
    immediate: immediate ?? false
  });

  res.json(result);
});

Step 5: Handle Edge Cases

Billing systems have many edge cases. Ask Claude Code to help identify and handle them:

You: What edge cases should I handle for subscription billing? Add proper error handling and retry logic.

Key edge cases to consider:

Here’s how Claude Code might suggest handling failed payments:

async function handlePaymentFailed(event: any) {
  const { subscription_id, customer_id } = event.data;
  
  const user = await prisma.user.findFirst({
    where: { paddleCustomerId: customer_id }
  });

  if (!user) return;

  // Send notification email
  await emailService.sendPaymentFailed({
    to: user.email,
    subscriptionId: subscription_id
  });

  // Update status but don't cancel yet
  await prisma.user.update({
    where: { id: user.id },
    data: { subscriptionStatus: 'past_due' }
  });
}

For more nuanced retry scheduling, Claude Code can generate a strategy function that adjusts timing based on the reason for failure:

def get_retry_schedule(failure_reason: str) -> list:
    """Determine retry schedule based on failure type"""
    if failure_reason == 'insufficient_balance':
        # Gentle retry for temporary issues
        return ['3 days', '7 days', '14 days']
    elif failure_reason == 'card_expired':
        # Urgent - card needs immediate update
        return ['immediate']
    else:
        return ['1 day', '3 days', '7 days']

This failure-aware retry schedule prevents unnecessary retries on cards that clearly need updating while giving customers with temporary balance issues enough time to resolve them.

Invoice Lifecycle Handling

Invoices flow through several statuses (duepaid or overdue). Each status requires a different action. Ask Claude Code to generate a complete invoice event handler:

def process_invoice_webhook(event: dict) -> dict:
    """Process invoice events from Paddle"""
    invoice = event['data']
    invoice_id = invoice['id']
    status = invoice['status']

    if status == 'paid':
        # Payment succeeded - fulfill the order
        fulfill_order(invoice)
        send_receipt(invoice)
        return {'action': 'order_fulfilled'}

    elif status == 'due':
        # Invoice generated but not yet paid
        send_payment_reminder(invoice)
        return {'action': 'reminder_sent'}

    elif status == 'overdue':
        # Handle overdue invoice
        handle_overdue_invoice(invoice)
        return {'action': 'overdue_handled'}

    return {'action': 'no_action_needed'}

Step 6: Testing Your Integration

Claude Code excels at generating test fixtures. Ask for help:

You: Create test fixtures for Paddle webhook events and write tests for the webhook handlers.
// tests/fixtures/paddle-events.ts
export const subscriptionCreatedEvent = {
  event_type: 'subscription.created',
  data: {
    customer_id: 'cus_123',
    subscription_id: 'sub_456',
    items: [{ price: { product_id: 'pro_annual' } }],
    status: 'active'
  }
};

export const subscriptionCanceledEvent = {
  event_type: 'subscription.canceled',
  data: {
    subscription_id: 'sub_456',
    status: 'canceled'
  }
};

Testing Webhooks Locally with ngrok and paddle-cli

Unit tests cover logic, but you also need to validate that Paddle can actually reach your local server. Use ngrok to expose your local port and paddle-cli to fire real sandbox events:

# Expose your local server to the internet
ngrok http 3000

# Configure Paddle sandbox to send webhooks to your ngrok URL, then trigger test events:
paddle-cli test-event subscription_created
paddle-cli test-event subscription_payment_failed

Make sure your webhook handler gracefully survives these edge cases during local testing:

Use these fixtures to test your webhook handlers:

describe('Paddle Webhooks', () => {
  it('should create subscription on subscription.created', async () => {
    const req = { 
      body: subscriptionCreatedEvent,
      headers: { 'paddle-signature': 'valid_signature' }
    } as any;
    
    await handlePaddleWebhook(req, mockRes);
    
    expect(prisma.user.updateMany).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ subscriptionStatus: 'active' })
      })
    );
  });
});

Best Practices

  1. Always verify webhook signatures - Never trust incoming requests without validation
  2. Idempotency is key - Design handlers to handle duplicate events gracefully
  3. Log everything - Maintain audit trails for billing events
  4. Use webhooks for state, not API calls - Store event data and use it to update your database
  5. Test in sandbox - Always fully test in Paddle’s sandbox environment before production

Conclusion

Building a Paddle billing integration with Claude Code significantly accelerates development. The AI assistant helps generate type-safe code, identifies potential issues, and creates comprehensive tests. By following this guide and using Claude Code’s capabilities, you’ll have a production-ready billing system that handles subscriptions, webhooks, and payment failures elegantly.

Remember to thoroughly test your integration in Paddle’s sandbox environment before deploying to production. With proper error handling and webhook processing, your billing system will be robust and reliable.

Built by theluckystrike — More at zovo.one