Claude Code for Stripe Integration: Payments, Subscriptions, and Webhooks — Claude Skills 360 Blog
Blog / Development / Claude Code for Stripe Integration: Payments, Subscriptions, and Webhooks
Development

Claude Code for Stripe Integration: Payments, Subscriptions, and Webhooks

Published: June 6, 2026
Read time: 9 min read
By: Claude Skills 360

Stripe is the standard for payment processing, but getting it right requires attention to edge cases: webhook signature verification, idempotency keys, SCA compliance, and handling failed payments gracefully. Claude Code generates Stripe integration code that handles these requirements — not just happy path flows.

This guide covers Stripe with Claude Code: one-time payments, subscriptions, webhook handling, customer portal, and testing.

One-Time Payments with Checkout

Implement Stripe Checkout for a one-time purchase.
After payment, redirect to a success page and fulfill the order.
// Create Checkout Session
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2025-01-27.acacia',
  typescript: true,
});

// API route handler (Next.js, Express, etc.)
export async function createCheckoutSession(req: Request) {
  const { items, customerId } = await req.json();

  const session = await stripe.checkout.sessions.create({
    mode: 'payment',
    customer: customerId, // Pre-fill if existing customer
    line_items: items.map(item => ({
      price_data: {
        currency: 'usd',
        product_data: {
          name: item.name,
          description: item.description,
          images: [item.imageUrl],
          metadata: { productId: item.id },
        },
        unit_amount: item.priceCents, // Stripe uses cents
      },
      quantity: item.quantity,
    })),
    success_url: `${process.env.APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.APP_URL}/cart`,
    // Collect billing address for tax calculation
    billing_address_collection: 'auto',
    // Automatic tax (requires product tax codes)
    automatic_tax: { enabled: true },
    // Metadata for webhook processing
    metadata: {
      orderId: crypto.randomUUID(),
      userId: req.headers.get('x-user-id') ?? '',
    },
  });

  return Response.json({ url: session.url });
}
// Success page — verify payment before fulfilling
export async function verifyAndFulfill(sessionId: string) {
  const session = await stripe.checkout.sessions.retrieve(sessionId, {
    expand: ['payment_intent', 'line_items'],
  });

  if (session.payment_status !== 'paid') {
    throw new Error('Payment not completed');
  }

  // Check idempotency — don't fulfill twice
  const existingOrder = await db('orders').where('stripe_session_id', sessionId).first();
  if (existingOrder?.fulfilled_at) {
    return existingOrder; // Already fulfilled
  }

  // Fulfill order
  const order = await fulfillOrder({
    stripeSessionId: sessionId,
    userId: session.metadata?.userId,
    items: session.line_items?.data ?? [],
    amountCents: session.amount_total ?? 0,
  });

  return order;
}

CLAUDE.md for Stripe Projects

## Stripe Integration
- Live keys: STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY in env
- Webhook secret: STRIPE_WEBHOOK_SECRET (validate all webhooks)
- Test mode: use STRIPE_SECRET_KEY=sk_test_... locally
- Payment link (active $39): https://buy.stripe.com/... — never hardcode
- Idempotency keys: required for all payment create/finalize operations
- Always subscribe to: checkout.session.completed, payment_intent.payment_failed, 
  customer.subscription.updated, customer.subscription.deleted
- Error handling: catch Stripe errors, return 400 for card errors (expected), 500 for API errors

Webhook Handling

Set up a webhook handler that processes Stripe events.
It needs to verify the signature, handle each event type,
and be idempotent — replaying an event shouldn't create duplicate records.
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function handleWebhook(req: Request) {
  const body = await req.text(); // Raw body required for signature verification
  const signature = req.headers.get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!,
    );
  } catch (err) {
    // Invalid signature — reject
    return new Response(`Webhook signature verification failed: ${err}`, { status: 400 });
  }

  // Idempotency check — mark event as processing to prevent duplicates
  const alreadyProcessed = await db('stripe_events')
    .where('stripe_event_id', event.id)
    .first();

  if (alreadyProcessed) {
    return new Response('Already processed', { status: 200 });
  }

  // Insert event record before processing (prevents duplicate processing on crash/retry)
  await db('stripe_events').insert({
    stripe_event_id: event.id,
    event_type: event.type,
    processed_at: new Date(),
  });

  try {
    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object as Stripe.Checkout.Session;
        await onCheckoutCompleted(session);
        break;
      }

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

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

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

      default:
        // Log unknown events for monitoring — don't error
        console.log(`Unhandled Stripe event type: ${event.type}`);
    }

    return new Response('OK', { status: 200 });
    
  } catch (error) {
    // Mark as failed so it can be retried
    await db('stripe_events')
      .where('stripe_event_id', event.id)
      .update({ error: (error as Error).message });
    
    // Return 500 — Stripe will retry the webhook
    return new Response('Processing failed', { status: 500 });
  }
}

Subscriptions

Implement subscription billing with three tiers: Free, Pro ($29/mo), Enterprise ($99/mo).
Upgrades should take effect immediately, downgrades at period end.
// Create subscription for new customer
async function createSubscription(customerId: string, priceId: string) {
  const subscription = await stripe.subscriptions.create({
    customer: customerId,
    items: [{ price: priceId }],
    payment_behavior: 'default_incomplete',   // Don't charge until confirmed
    payment_settings: {
      save_default_payment_method: 'on_subscription',
    },
    expand: ['latest_invoice.payment_intent'],
  });

  return {
    subscriptionId: subscription.id,
    clientSecret: (subscription.latest_invoice as Stripe.Invoice)
      ?.payment_intent
      ? ((subscription.latest_invoice as Stripe.Invoice).payment_intent as Stripe.PaymentIntent).client_secret
      : null,
  };
}

// Upgrade — prorate immediately
async function upgradeSubscription(subscriptionId: string, newPriceId: string) {
  const subscription = await stripe.subscriptions.retrieve(subscriptionId);
  const currentItem = subscription.items.data[0];

  return stripe.subscriptions.update(subscriptionId, {
    items: [{
      id: currentItem.id,
      price: newPriceId,
    }],
    proration_behavior: 'always_invoice', // Charge immediately for the upgrade
  });
}

// Downgrade — take effect at period end
async function downgradeSubscription(subscriptionId: string, newPriceId: string) {
  const subscription = await stripe.subscriptions.retrieve(subscriptionId);
  const currentItem = subscription.items.data[0];

  return stripe.subscriptions.update(subscriptionId, {
    items: [{
      id: currentItem.id,
      price: newPriceId,
    }],
    proration_behavior: 'none',       // No immediate charge
    billing_cycle_anchor: 'unchanged', // Keep current period
  });
}

Customer Portal

Add Stripe's customer portal so users can manage their
own subscriptions, update payment methods, and view invoices.
// Create a portal session and redirect
export async function createPortalSession(customerId: string, returnUrl: string) {
  const portalSession = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: returnUrl,
    // Configure what customers can do in the portal
    // (configure in Stripe Dashboard → Customer portal settings)
  });

  return portalSession.url;
}

// In your route handler:
app.post('/customer-portal', authenticate, async (req, res) => {
  const user = await User.findById(req.user.id);
  
  if (!user.stripeCustomerId) {
    return res.status(400).json({ error: 'No Stripe customer found' });
  }

  const url = await createPortalSession(
    user.stripeCustomerId,
    `${process.env.APP_URL}/settings`,
  );

  res.json({ url });
});

The customer portal handles card updates, subscription cancellations, and invoice downloads — saving significant development time for billing UI.

Testing Stripe Locally

How do I test webhooks locally without deploying?
# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login and forward webhooks to local server
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# In another terminal — trigger specific events without paying:
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_failed

# Test cards (work in test mode):
# 4242424242424242 — Visa, succeeds
# 4000000000000002 — card declined
# 4000002500003155 — requires 3D Secure (SCA)
# 4000000000009995 — insufficient funds
// Integration test with Stripe test mode
describe('checkout webhook', () => {
  it('fulfills order after successful payment', async () => {
    // Create a real checkout session in test mode
    const session = await stripe.checkout.sessions.create({
      mode: 'payment',
      line_items: [{ price: 'price_test_xxx', quantity: 1 }],
      success_url: 'https://test.com/success',
      cancel_url: 'https://test.com/cancel',
    });

    // Simulate the webhook event
    const event = stripe.webhooks.generateTestHeaderString({
      payload: JSON.stringify({
        type: 'checkout.session.completed',
        data: { object: { ...session, payment_status: 'paid' } },
      }),
      secret: process.env.STRIPE_WEBHOOK_SECRET!,
    });

    const response = await request(app)
      .post('/api/webhooks/stripe')
      .set('stripe-signature', event.header)
      .send(event.payload);

    expect(response.status).toBe(200);
    // Verify fulfillment happened
    const order = await db('orders').where('stripe_session_id', session.id).first();
    expect(order).toBeDefined();
  });
});

For authentication and user management that integrates with Stripe customers, see the authentication guide. For observability into payment flows — tracking conversion rates, failure rates, and revenue metrics — see the observability guide. The Claude Skills 360 bundle includes Stripe integration skill sets for checkout flows, subscription management, and billing edge cases. Start with the free tier to try payment integration code generation.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free