From webapp-toolkit
Use when integrating Paddle payments, setting up subscriptions, configuring webhooks, or debugging billing issues. Covers sandbox testing and production deployment.
How this skill is triggered — by the user, by Claude, or both
Slash command
/webapp-toolkit:paddle-integrationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Paddle is a merchant of record for SaaS subscriptions. This skill covers the complete integration: sandbox setup, webhook handling, frontend checkout, and production deployment.
Paddle is a merchant of record for SaaS subscriptions. This skill covers the complete integration: sandbox setup, webhook handling, frontend checkout, and production deployment.
Core principle: Always start with sandbox credentials. Test the full flow before touching production.
# Backend (server-side only)
PADDLE_API_KEY=pdl_sdbx_apikey_... # Sandbox: pdl_sdbx_, Production: pdl_live_
PADDLE_WEBHOOK_SECRET=pdl_ntfset_... # From webhook creation
# Frontend (NEXT_PUBLIC_ prefix for Next.js)
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN=test_... # Sandbox: test_, Production: live_
NEXT_PUBLIC_PADDLE_PRICE_ID_PRO_MONTHLY=pri_01...
In Paddle Dashboard → Catalog:
pri_01...)# Create webhook pointing to your endpoint
curl -X POST "https://sandbox-api.paddle.com/webhooks" \
-H "Authorization: Bearer $PADDLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "App webhooks",
"destination": "https://your-domain.com/api/webhooks/paddle",
"subscribed_events": [
"subscription.created",
"subscription.updated",
"subscription.canceled",
"subscription.paused",
"subscription.resumed",
"transaction.completed",
"transaction.payment_failed"
],
"active": true
}'
Response includes secret_key - this is your PADDLE_WEBHOOK_SECRET.
# Add each variable (repeat for each environment as needed)
echo "pdl_sdbx_apikey_..." | vercel env add PADDLE_API_KEY development
echo "pdl_ntfset_..." | vercel env add PADDLE_WEBHOOK_SECRET development
echo "test_..." | vercel env add NEXT_PUBLIC_PADDLE_CLIENT_TOKEN development
echo "pri_01..." | vercel env add NEXT_PUBLIC_PADDLE_PRICE_ID_PRO_MONTHLY development
# Pull to local
vercel env pull .env.local
Add subscription fields to your users table:
// Drizzle schema example
export const users = pgTable("user", {
// ... existing fields
paddleCustomerId: text("paddleCustomerId"),
paddleSubscriptionId: text("paddleSubscriptionId"),
plan: text("plan").$type<"free" | "pro">().notNull().default("free"),
planStatus: text("planStatus").$type<
"active" | "canceled" | "past_due" | "paused" | "trialing"
>(),
currentPeriodEnd: timestamp("currentPeriodEnd", { mode: "date" }),
});
// app/api/webhooks/paddle/route.ts
import { Paddle, EventName } from "@paddle/paddle-node-sdk";
const paddle = new Paddle(process.env.PADDLE_API_KEY!);
export async function POST(request: Request) {
const signature = request.headers.get("paddle-signature");
const rawBody = await request.text();
// Verify signature
let event;
try {
event = paddle.webhooks.unmarshal(
rawBody,
process.env.PADDLE_WEBHOOK_SECRET!,
signature!
);
} catch {
return new Response("Invalid signature", { status: 401 });
}
// Handle events
switch (event.eventType) {
case EventName.SubscriptionCreated:
case EventName.SubscriptionUpdated:
// Update user's subscription status
const customerId = event.data.customerId;
const subscriptionId = event.data.id;
const status = event.data.status;
const currentPeriodEnd = event.data.currentBillingPeriod?.endsAt;
// ... update database
break;
case EventName.SubscriptionCanceled:
// Mark subscription as canceled (still active until period ends)
break;
case EventName.TransactionCompleted:
// Payment succeeded - good for logging/analytics
break;
case EventName.TransactionPaymentFailed:
// Payment failed - may want to notify user
break;
}
return new Response("OK");
}
// components/pricing-cards.tsx
"use client";
import { useEffect } from "react";
import { initializePaddle, Paddle } from "@paddle/paddle-js";
let paddleInstance: Paddle | null = null;
export function PricingCards({ userEmail }: { userEmail?: string }) {
useEffect(() => {
initializePaddle({
environment: "sandbox", // Change to "production" for live
token: process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN!,
}).then((paddle) => {
paddleInstance = paddle ?? null;
});
}, []);
const handleCheckout = () => {
paddleInstance?.Checkout.open({
items: [{ priceId: process.env.NEXT_PUBLIC_PADDLE_PRICE_ID_PRO_MONTHLY! }],
customer: userEmail ? { email: userEmail } : undefined,
customData: { userId: "user_123" }, // Passed to webhooks
});
};
return <button onClick={handleCheckout}>Subscribe</button>;
}
| Step | Sandbox | Production |
|---|---|---|
| Dashboard URL | sandbox-vendors.paddle.com | vendors.paddle.com |
| API URL | sandbox-api.paddle.com | api.paddle.com |
| API Key prefix | pdl_sdbx_ | pdl_live_ |
| Client token prefix | test_ | live_ |
| Paddle.js environment | "sandbox" | "production" |
Get production credentials from https://vendors.paddle.com (same locations as sandbox)
Create production webhook (same API call but to production URL):
curl -X POST "https://api.paddle.com/webhooks" \
-H "Authorization: Bearer $PADDLE_LIVE_API_KEY" \
...
echo "pdl_live_apikey_..." | vercel env add PADDLE_API_KEY production
echo "pdl_ntfset_..." | vercel env add PADDLE_WEBHOOK_SECRET production
echo "live_..." | vercel env add NEXT_PUBLIC_PADDLE_CLIENT_TOKEN production
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox"
| Mistake | Fix |
|---|---|
| Webhook returns 401 | Check PADDLE_WEBHOOK_SECRET matches the secret from webhook creation |
| Checkout doesn't open | Verify NEXT_PUBLIC_PADDLE_CLIENT_TOKEN is set and Paddle.js initialized |
| Wrong price in checkout | Confirm Price ID matches your Paddle dashboard |
| Events not received | Check webhook URL is publicly accessible, not localhost |
| Sandbox works, prod fails | Ensure you created a NEW webhook for production (different secret) |
Paddle sandbox accepts test cards:
4242 4242 4242 4242 (any future expiry, any CVC)4000 0000 0000 0002# List webhooks
curl "https://sandbox-api.paddle.com/webhooks" \
-H "Authorization: Bearer $PADDLE_API_KEY"
# Get webhook details
curl "https://sandbox-api.paddle.com/webhooks/ntfset_01..." \
-H "Authorization: Bearer $PADDLE_API_KEY"
# List subscriptions for a customer
curl "https://sandbox-api.paddle.com/subscriptions?customer_id=ctm_01..." \
-H "Authorization: Bearer $PADDLE_API_KEY"
# Cancel subscription
curl -X POST "https://sandbox-api.paddle.com/subscriptions/sub_01.../cancel" \
-H "Authorization: Bearer $PADDLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"effective_from": "next_billing_period"}'
# Backend SDK (webhook verification)
yarn add @paddle/paddle-node-sdk
# Frontend SDK (checkout)
yarn add @paddle/paddle-js
npx claudepluginhub zainrizvi/webapp-toolkit --plugin webapp-toolkitGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.