Files
english/.opencode/skills/payment-integration/references/paddle/best-practices.md
2026-04-12 01:06:31 +07:00

3.1 KiB

Paddle Best Practices

Production patterns for reliable integration.

Webhook Handling

// 1. Verify signature first
// 2. Check idempotency
// 3. Process async
// 4. Return 200 immediately

const processedEvents = new Set(); // Use Redis in production

app.post('/webhooks/paddle', async (req, res) => {
  const signature = req.headers['paddle-signature'];

  // Verify
  const event = paddle.webhooks.unmarshal(
    req.rawBody,
    process.env.PADDLE_WEBHOOK_SECRET,
    signature
  );

  // Idempotency
  if (processedEvents.has(event.eventId)) {
    return res.status(200).send('Already processed');
  }

  // Acknowledge immediately
  res.status(200).send('OK');

  // Process async
  await queue.add('paddle-webhook', event);
});

Subscription Status Sync

// Always verify subscription status server-side
async function checkAccess(userId: string): Promise<boolean> {
  const user = await db.users.findOne({ id: userId });
  if (!user.paddleSubscriptionId) return false;

  const sub = await paddle.subscriptions.get(user.paddleSubscriptionId);
  return ['active', 'trialing'].includes(sub.status);
}

Custom Data for User Linking

// Pass user_id in checkout
paddle.Checkout.open({
  items: [{ priceId: 'pri_xxx', quantity: 1 }],
  customData: { user_id: currentUser.id }
});

// Retrieve in webhook
app.post('/webhooks/paddle', async (req, res) => {
  const event = paddle.webhooks.unmarshal(...);

  if (event.eventType === 'subscription.created') {
    const userId = event.data.customData?.user_id;
    await db.users.update(userId, {
      paddleSubscriptionId: event.data.id,
      paddleCustomerId: event.data.customerId
    });
  }
});

Error Recovery

// Handle past_due subscriptions
async function handlePastDue(subscriptionId: string) {
  // Get customer portal for payment update
  const sub = await paddle.subscriptions.get(subscriptionId);
  const portal = await paddle.customers.createPortalSession(sub.customerId);

  // Email customer with portal link
  await sendEmail(sub.customer.email, {
    subject: 'Update your payment method',
    link: portal.urls.general.overview
  });
}

Testing with Sandbox

// Use sandbox environment
const paddle = new Paddle(process.env.PADDLE_API_KEY, {
  environment: 'sandbox'
});

// Sandbox card: 4242 4242 4242 4242
// Any future expiry, any CVC

Price Localization

// Preview localized prices before checkout
const preview = await paddle.PricePreview({
  items: [{ priceId: 'pri_xxx', quantity: 1 }],
  address: { countryCode: customerCountry }
});

// Display localized price
const formattedPrice = preview.data.details.totals.total;

Paddle Retain (Churn Prevention)

Features enabled in dashboard:

  • Payment recovery: Automated dunning emails
  • Cancellation surveys: Collect feedback + offer discounts
  • Term optimization: Auto-upgrade annual suggestions

Security Checklist

  • Webhook signatures verified
  • API keys in env vars, not code
  • Separate keys for sandbox/production
  • Idempotency implemented
  • Server-side status verification
  • Secure customer portal sessions