home / skills / doanchienthangdev / omgkit / payment-integration

This skill integrates Stripe, PayPal, and LemonSqueezy for secure checkout, subscriptions, webhooks, and PCI-compliant payment flows.

npx playbooks add skill doanchienthangdev/omgkit --skill payment-integration

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
19.6 KB
---
name: payment-integration
description: Enterprise payment processing with Stripe, PayPal, and LemonSqueezy including subscriptions, webhooks, and PCI compliance
category: integrations
triggers:
  - payment integration
  - stripe
  - paypal
  - lemonsqueezy
  - checkout
  - subscription billing
  - payment processing
---

# Payment Integration

Enterprise-grade **payment processing** with Stripe, PayPal, and LemonSqueezy. This skill covers checkout flows, subscription management, webhook handling, and PCI compliance patterns.

## Purpose

Implement secure, reliable payment systems:

- Process one-time and recurring payments
- Handle subscription lifecycle management
- Implement secure webhook processing
- Manage refunds and disputes
- Ensure PCI DSS compliance
- Support multiple payment methods

## Features

### 1. Stripe Integration

```typescript
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
  typescript: true,
});

// Create checkout session
async function createCheckoutSession(
  items: CartItem[],
  customerId?: string,
  metadata?: Record<string, string>
): Promise<Stripe.Checkout.Session> {
  const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = items.map(item => ({
    price_data: {
      currency: 'usd',
      product_data: {
        name: item.name,
        description: item.description,
        images: item.images,
        metadata: { productId: item.id },
      },
      unit_amount: Math.round(item.price * 100), // Convert to cents
    },
    quantity: item.quantity,
  }));

  return stripe.checkout.sessions.create({
    mode: 'payment',
    line_items: lineItems,
    customer: customerId,
    success_url: `${process.env.APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.APP_URL}/checkout/cancel`,
    metadata,
    payment_intent_data: {
      metadata,
    },
    shipping_address_collection: {
      allowed_countries: ['US', 'CA', 'GB'],
    },
    automatic_tax: { enabled: true },
  });
}

// Create subscription
async function createSubscription(
  customerId: string,
  priceId: string,
  options?: {
    trialDays?: number;
    couponId?: string;
    metadata?: Record<string, string>;
  }
): Promise<Stripe.Subscription> {
  const params: Stripe.SubscriptionCreateParams = {
    customer: customerId,
    items: [{ price: priceId }],
    payment_behavior: 'default_incomplete',
    payment_settings: {
      save_default_payment_method: 'on_subscription',
    },
    expand: ['latest_invoice.payment_intent'],
    metadata: options?.metadata,
  };

  if (options?.trialDays) {
    params.trial_period_days = options.trialDays;
  }

  if (options?.couponId) {
    params.coupon = options.couponId;
  }

  return stripe.subscriptions.create(params);
}

// Handle subscription changes
async function updateSubscription(
  subscriptionId: string,
  newPriceId: string,
  prorationBehavior: 'create_prorations' | 'none' | 'always_invoice' = 'create_prorations'
): Promise<Stripe.Subscription> {
  const subscription = await stripe.subscriptions.retrieve(subscriptionId);

  return stripe.subscriptions.update(subscriptionId, {
    items: [{
      id: subscription.items.data[0].id,
      price: newPriceId,
    }],
    proration_behavior: prorationBehavior,
  });
}

// Cancel subscription
async function cancelSubscription(
  subscriptionId: string,
  cancelImmediately: boolean = false
): Promise<Stripe.Subscription> {
  if (cancelImmediately) {
    return stripe.subscriptions.cancel(subscriptionId);
  }

  return stripe.subscriptions.update(subscriptionId, {
    cancel_at_period_end: true,
  });
}

// Process refund
async function processRefund(
  paymentIntentId: string,
  amount?: number, // Partial refund in cents
  reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer'
): Promise<Stripe.Refund> {
  return stripe.refunds.create({
    payment_intent: paymentIntentId,
    amount, // Omit for full refund
    reason,
  });
}
```

### 2. Webhook Handling

```typescript
import { buffer } from 'micro';
import type { NextApiRequest, NextApiResponse } from 'next';

// Webhook handler with signature verification
async function handleStripeWebhook(
  req: NextApiRequest,
  res: NextApiResponse
): Promise<void> {
  if (req.method !== 'POST') {
    res.setHeader('Allow', 'POST');
    res.status(405).end('Method Not Allowed');
    return;
  }

  const buf = await buffer(req);
  const sig = req.headers['stripe-signature'] as string;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      buf,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error('Webhook signature verification failed:', err);
    res.status(400).send(`Webhook Error: ${err.message}`);
    return;
  }

  // Handle events with idempotency
  const idempotencyKey = event.id;
  const processed = await isEventProcessed(idempotencyKey);

  if (processed) {
    res.status(200).json({ received: true, duplicate: true });
    return;
  }

  try {
    await processWebhookEvent(event);
    await markEventProcessed(idempotencyKey);
    res.status(200).json({ received: true });
  } catch (err) {
    console.error('Webhook processing error:', err);
    // Return 200 to prevent retries for business logic errors
    // Return 500 for transient errors that should be retried
    res.status(err.retryable ? 500 : 200).json({ error: err.message });
  }
}

// Event processor
async function processWebhookEvent(event: Stripe.Event): Promise<void> {
  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      await handleCheckoutComplete(session);
      break;
    }

    case 'customer.subscription.created':
    case 'customer.subscription.updated': {
      const subscription = event.data.object as Stripe.Subscription;
      await syncSubscriptionStatus(subscription);
      break;
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription;
      await handleSubscriptionCanceled(subscription);
      break;
    }

    case 'invoice.payment_succeeded': {
      const invoice = event.data.object as Stripe.Invoice;
      await handlePaymentSuccess(invoice);
      break;
    }

    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice;
      await handlePaymentFailure(invoice);
      break;
    }

    case 'charge.dispute.created': {
      const dispute = event.data.object as Stripe.Dispute;
      await handleDispute(dispute);
      break;
    }

    default:
      console.log(`Unhandled event type: ${event.type}`);
  }
}

// Subscription sync
async function syncSubscriptionStatus(subscription: Stripe.Subscription): Promise<void> {
  await db.subscription.upsert({
    where: { stripeSubscriptionId: subscription.id },
    update: {
      status: subscription.status,
      currentPeriodStart: new Date(subscription.current_period_start * 1000),
      currentPeriodEnd: new Date(subscription.current_period_end * 1000),
      cancelAtPeriodEnd: subscription.cancel_at_period_end,
      priceId: subscription.items.data[0].price.id,
    },
    create: {
      stripeSubscriptionId: subscription.id,
      stripeCustomerId: subscription.customer as string,
      status: subscription.status,
      currentPeriodStart: new Date(subscription.current_period_start * 1000),
      currentPeriodEnd: new Date(subscription.current_period_end * 1000),
      priceId: subscription.items.data[0].price.id,
    },
  });
}
```

### 3. PayPal Integration

```typescript
import { PayPalHttpClient, OrdersCreateRequest, OrdersCaptureRequest } from '@paypal/checkout-server-sdk';

// PayPal client setup
function getPayPalClient(): PayPalHttpClient {
  const environment = process.env.NODE_ENV === 'production'
    ? new paypal.core.LiveEnvironment(
        process.env.PAYPAL_CLIENT_ID!,
        process.env.PAYPAL_CLIENT_SECRET!
      )
    : new paypal.core.SandboxEnvironment(
        process.env.PAYPAL_CLIENT_ID!,
        process.env.PAYPAL_CLIENT_SECRET!
      );

  return new PayPalHttpClient(environment);
}

// Create PayPal order
async function createPayPalOrder(
  items: CartItem[],
  shippingCost: number = 0
): Promise<PayPalOrder> {
  const client = getPayPalClient();

  const itemTotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const total = itemTotal + shippingCost;

  const request = new OrdersCreateRequest();
  request.prefer('return=representation');
  request.requestBody({
    intent: 'CAPTURE',
    purchase_units: [{
      amount: {
        currency_code: 'USD',
        value: total.toFixed(2),
        breakdown: {
          item_total: { currency_code: 'USD', value: itemTotal.toFixed(2) },
          shipping: { currency_code: 'USD', value: shippingCost.toFixed(2) },
        },
      },
      items: items.map(item => ({
        name: item.name,
        unit_amount: { currency_code: 'USD', value: item.price.toFixed(2) },
        quantity: item.quantity.toString(),
        category: 'PHYSICAL_GOODS',
      })),
    }],
    application_context: {
      brand_name: process.env.APP_NAME,
      landing_page: 'BILLING',
      user_action: 'PAY_NOW',
      return_url: `${process.env.APP_URL}/checkout/paypal/success`,
      cancel_url: `${process.env.APP_URL}/checkout/paypal/cancel`,
    },
  });

  const response = await client.execute(request);
  return response.result;
}

// Capture PayPal payment
async function capturePayPalOrder(orderId: string): Promise<PayPalCapture> {
  const client = getPayPalClient();
  const request = new OrdersCaptureRequest(orderId);
  request.prefer('return=representation');

  const response = await client.execute(request);

  if (response.result.status === 'COMPLETED') {
    await handlePayPalPaymentComplete(response.result);
  }

  return response.result;
}
```

### 4. Subscription Management UI

```typescript
// Subscription management component
interface SubscriptionManagerProps {
  subscription: UserSubscription;
  availablePlans: Plan[];
}

export function SubscriptionManager({ subscription, availablePlans }: SubscriptionManagerProps) {
  const [isLoading, setIsLoading] = useState(false);

  async function handleUpgrade(newPriceId: string) {
    setIsLoading(true);
    try {
      await updateSubscription(subscription.id, newPriceId);
      toast.success('Subscription updated successfully');
    } catch (error) {
      toast.error('Failed to update subscription');
    } finally {
      setIsLoading(false);
    }
  }

  async function handleCancel() {
    if (!confirm('Are you sure you want to cancel your subscription?')) return;

    setIsLoading(true);
    try {
      await cancelSubscription(subscription.id);
      toast.success('Subscription will be canceled at the end of the billing period');
    } catch (error) {
      toast.error('Failed to cancel subscription');
    } finally {
      setIsLoading(false);
    }
  }

  async function handleReactivate() {
    setIsLoading(true);
    try {
      await reactivateSubscription(subscription.id);
      toast.success('Subscription reactivated');
    } catch (error) {
      toast.error('Failed to reactivate subscription');
    } finally {
      setIsLoading(false);
    }
  }

  return (
    <div className="subscription-manager">
      <div className="current-plan">
        <h3>Current Plan: {subscription.plan.name}</h3>
        <p>Status: {subscription.status}</p>
        <p>Renews: {format(subscription.currentPeriodEnd, 'MMM d, yyyy')}</p>
      </div>

      {subscription.cancelAtPeriodEnd && (
        <Alert variant="warning">
          Your subscription will end on {format(subscription.currentPeriodEnd, 'MMM d, yyyy')}
          <Button onClick={handleReactivate} disabled={isLoading}>
            Reactivate
          </Button>
        </Alert>
      )}

      <div className="available-plans">
        <h4>Available Plans</h4>
        {availablePlans.map(plan => (
          <PlanCard
            key={plan.id}
            plan={plan}
            isCurrent={plan.priceId === subscription.priceId}
            onSelect={() => handleUpgrade(plan.priceId)}
            disabled={isLoading}
          />
        ))}
      </div>

      {!subscription.cancelAtPeriodEnd && (
        <Button variant="destructive" onClick={handleCancel} disabled={isLoading}>
          Cancel Subscription
        </Button>
      )}
    </div>
  );
}
```

### 5. LemonSqueezy Integration

```typescript
import { lemonSqueezySetup, createCheckout, getSubscription } from '@lemonsqueezy/lemonsqueezy.js';

// Initialize LemonSqueezy
lemonSqueezySetup({ apiKey: process.env.LEMONSQUEEZY_API_KEY! });

// Create LemonSqueezy checkout
async function createLemonSqueezyCheckout(
  variantId: string,
  customerEmail: string,
  metadata?: Record<string, string>
): Promise<string> {
  const checkout = await createCheckout(
    process.env.LEMONSQUEEZY_STORE_ID!,
    variantId,
    {
      checkoutData: {
        email: customerEmail,
        custom: metadata,
      },
      checkoutOptions: {
        embed: false,
        logo: true,
        dark: false,
      },
      productOptions: {
        enabledVariants: [parseInt(variantId)],
        redirectUrl: `${process.env.APP_URL}/checkout/success`,
      },
    }
  );

  return checkout.data.attributes.url;
}

// LemonSqueezy webhook handler
async function handleLemonSqueezyWebhook(req: NextApiRequest): Promise<void> {
  const signature = req.headers['x-signature'] as string;
  const payload = JSON.stringify(req.body);

  // Verify signature
  const hmac = crypto.createHmac('sha256', process.env.LEMONSQUEEZY_WEBHOOK_SECRET!);
  hmac.update(payload);
  const expectedSignature = hmac.digest('hex');

  if (signature !== expectedSignature) {
    throw new Error('Invalid webhook signature');
  }

  const { event_name, data } = req.body;

  switch (event_name) {
    case 'subscription_created':
      await handleLSSubscriptionCreated(data);
      break;
    case 'subscription_updated':
      await handleLSSubscriptionUpdated(data);
      break;
    case 'subscription_cancelled':
      await handleLSSubscriptionCancelled(data);
      break;
    case 'order_created':
      await handleLSOrderCreated(data);
      break;
  }
}
```

### 6. Payment Security

```typescript
// PCI-compliant payment handling patterns

// Never log sensitive payment data
const SENSITIVE_FIELDS = ['card_number', 'cvv', 'cvc', 'exp_month', 'exp_year'];

function sanitizeForLogging(data: Record<string, any>): Record<string, any> {
  const sanitized = { ...data };

  for (const field of SENSITIVE_FIELDS) {
    if (field in sanitized) {
      sanitized[field] = '[REDACTED]';
    }
  }

  return sanitized;
}

// Secure API endpoint
async function createPaymentIntent(req: NextApiRequest, res: NextApiResponse) {
  // Validate request
  const { amount, currency, customerId } = req.body;

  if (!amount || amount < 50) { // Minimum $0.50
    return res.status(400).json({ error: 'Invalid amount' });
  }

  // Use idempotency key
  const idempotencyKey = req.headers['idempotency-key'] as string;

  if (!idempotencyKey) {
    return res.status(400).json({ error: 'Idempotency key required' });
  }

  try {
    const paymentIntent = await stripe.paymentIntents.create(
      {
        amount,
        currency: currency || 'usd',
        customer: customerId,
        automatic_payment_methods: { enabled: true },
        metadata: {
          userId: req.user.id,
        },
      },
      { idempotencyKey }
    );

    // Only return necessary data
    res.json({
      clientSecret: paymentIntent.client_secret,
      paymentIntentId: paymentIntent.id,
    });
  } catch (error) {
    console.error('Payment error:', sanitizeForLogging(error));

    if (error.type === 'StripeCardError') {
      return res.status(400).json({ error: error.message });
    }

    res.status(500).json({ error: 'Payment processing failed' });
  }
}

// Webhook security middleware
function verifyWebhookSignature(
  payload: Buffer,
  signature: string,
  secret: string
): boolean {
  try {
    stripe.webhooks.constructEvent(payload, signature, secret);
    return true;
  } catch {
    return false;
  }
}
```

## Use Cases

### 1. E-commerce Checkout

```typescript
// Complete e-commerce checkout flow
async function processCheckout(cart: Cart, user: User): Promise<CheckoutResult> {
  // Calculate totals
  const subtotal = calculateSubtotal(cart.items);
  const tax = await calculateTax(subtotal, user.address);
  const shipping = await calculateShipping(cart.items, user.address);
  const total = subtotal + tax + shipping;

  // Create Stripe checkout
  const session = await createCheckoutSession(cart.items, user.stripeCustomerId, {
    orderId: generateOrderId(),
    userId: user.id,
  });

  // Create pending order
  await db.order.create({
    data: {
      userId: user.id,
      status: 'pending',
      subtotal,
      tax,
      shipping,
      total,
      stripeSessionId: session.id,
      items: {
        create: cart.items.map(item => ({
          productId: item.productId,
          quantity: item.quantity,
          price: item.price,
        })),
      },
    },
  });

  return {
    checkoutUrl: session.url,
    sessionId: session.id,
  };
}
```

### 2. SaaS Subscription

```typescript
// SaaS subscription management
async function handlePlanChange(
  userId: string,
  newPlanId: string
): Promise<PlanChangeResult> {
  const user = await db.user.findUnique({
    where: { id: userId },
    include: { subscription: true },
  });

  if (!user.subscription) {
    // New subscription
    const session = await createSubscriptionCheckout(user, newPlanId);
    return { action: 'redirect', url: session.url };
  }

  const currentPlan = await getPlan(user.subscription.priceId);
  const newPlan = await getPlan(newPlanId);

  if (newPlan.price > currentPlan.price) {
    // Upgrade - charge prorated amount
    await updateSubscription(user.subscription.stripeSubscriptionId, newPlanId, 'create_prorations');
    return { action: 'upgraded', newPlan };
  } else {
    // Downgrade - apply at period end
    await updateSubscription(user.subscription.stripeSubscriptionId, newPlanId, 'none');
    return { action: 'scheduled', effectiveDate: user.subscription.currentPeriodEnd };
  }
}
```

## Best Practices

### Do's

- **Use idempotency keys** - Prevent duplicate charges
- **Verify webhook signatures** - Always validate webhook authenticity
- **Handle all payment states** - Success, failure, pending, disputed
- **Store payment references** - Keep Stripe IDs for reconciliation
- **Test with sandbox** - Use test mode during development
- **Monitor for fraud** - Implement Stripe Radar or equivalent

### Don'ts

- Never log full card numbers
- Never store CVV/CVC codes
- Never handle raw card data (use Stripe.js/Elements)
- Never skip signature verification
- Never trust client-side amounts
- Never expose secret keys in frontend

### Security Checklist

```markdown
## Payment Security Checklist

### PCI Compliance
- [ ] No raw card data on server
- [ ] Using Stripe.js/Elements for card collection
- [ ] Webhook signature verification enabled
- [ ] HTTPS only for all payment endpoints

### Data Handling
- [ ] Sensitive fields excluded from logs
- [ ] Customer IDs used instead of card details
- [ ] Idempotency keys for all mutations
- [ ] Payment references stored securely

### Fraud Prevention
- [ ] Stripe Radar enabled
- [ ] Address verification (AVS)
- [ ] 3D Secure enabled
- [ ] Velocity checks implemented
```

## Related Skills

- **backend-development** - Server-side payment handling
- **security** - PCI compliance and secure handling
- **oauth** - Customer authentication
- **api-architecture** - Payment API design

## Reference Resources

- [Stripe Documentation](https://stripe.com/docs)
- [PayPal Developer](https://developer.paypal.com/)
- [LemonSqueezy Docs](https://docs.lemonsqueezy.com/)
- [PCI DSS Standards](https://www.pcisecuritystandards.org/)

Overview

This skill implements enterprise-grade payment processing with Stripe, PayPal, and LemonSqueezy, covering one-time payments, subscriptions, webhooks, refunds, and PCI compliance patterns. It includes checkout flows, subscription lifecycle management, webhook signature verification, and UI components for subscription management. The code samples and patterns are ready to integrate into a JavaScript/TypeScript application.

How this skill works

The skill provides helpers to create checkout sessions and subscriptions, update or cancel subscriptions, and process refunds via Stripe. It includes robust webhook handlers that verify signatures, enforce idempotency, and route events to business logic. PayPal order creation and capture flows are supported via the PayPal SDK, and LemonSqueezy checkout and webhook handling are provided with HMAC verification. Subscription UI components show current plan, upgrades, cancellations, and reactivation flows.

When to use it

  • You need a unified payment layer supporting Stripe, PayPal, and LemonSqueezy.
  • You must implement subscriptions with trials, proration, coupons, and lifecycle sync.
  • You require secure webhook processing with signature verification and idempotency.
  • You need client-facing subscription management UI for upgrades, cancellation, and reactivation.
  • You must meet PCI DSS guidance by minimizing server exposure to raw card data.

Best practices

  • Always verify webhook signatures and track processed event IDs to ensure idempotency.
  • Use hosted checkout sessions where possible to reduce PCI scope and complexity.
  • Save default payment methods on subscriptions to improve retry success rates.
  • Handle transient webhook errors with 500 to allow retries and return 200 for business errors.
  • Store subscription state in your database using upsert patterns to keep billing data consistent.

Example use cases

  • Create a Stripe checkout session for one-time purchases with automatic tax and shipping collection.
  • Create and manage recurring subscriptions with trial periods, coupon support, and proration on plan changes.
  • Process PayPal orders and capture payments after buyer approval in a server-side flow.
  • Handle LemonSqueezy embedded or redirect checkouts and verify webhook HMAC signatures.
  • Build a customer-facing subscription management page to upgrade, cancel, or reactivate plans.

FAQ

How do webhooks avoid duplicate processing?

The handlers verify signatures, derive an idempotency key from the event ID, check a processed-events store, and skip or acknowledge duplicates.

How can I reduce PCI scope?

Prefer hosted checkout solutions (Stripe Checkout, PayPal Checkout, LemonSqueezy) and never log raw card data on your servers.