Limited Time Offer: 50% off Pro plans! View Pricing

TimeYouYou
Tutorial

Integrating Stripe Payments in Your SaaS

DDavid Kim
December 28, 2023
12 min read
StripePaymentsTutorial

Integrating Stripe Payments in Your SaaS

Payment processing is critical for any SaaS. Here's how to integrate Stripe properly.

Why Stripe?

Stripe is the industry standard for SaaS payments because:

  • Developer-friendly API
  • Handles PCI compliance
  • Supports 135+ currencies
  • Built-in subscription management
  • Excellent documentation

Setup

1. Install Dependencies

npm install stripe @stripe/stripe-js

2. Environment Variables

STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

Creating Products and Prices

// lib/stripe.ts
import Stripe from 'stripe';

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-11-20.acacia',
});

// Create products (do this once)
const product = await stripe.products.create({
  name: 'Pro Plan',
  description: 'Full access to all features',
});

const price = await stripe.prices.create({
  product: product.id,
  unit_amount: 2999, // $29.99
  currency: 'usd',
  recurring: {
    interval: 'month',
  },
});

Checkout Flow

Create Checkout Session

// app/api/checkout/route.ts
import { auth } from '@/auth';
import { stripe } from '@/lib/stripe';

export async function POST(req: Request) {
  const session = await auth();
  if (!session?.user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { priceId } = await req.json();

  const checkoutSession = await stripe.checkout.sessions.create({
    customer_email: session.user.email,
    line_items: [
      {
        price: priceId,
        quantity: 1,
      },
    ],
    mode: 'subscription',
    success_url: `${process.env.NEXT_PUBLIC_WEB_URL}/dashboard?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_WEB_URL}/pricing?canceled=true`,
    metadata: {
      userId: session.user.id,
    },
  });

  return Response.json({ url: checkoutSession.url });
}

Frontend Component

'use client';

export function SubscribeButton({ priceId }: { priceId: string }) {
  const [loading, setLoading] = useState(false);

  const handleSubscribe = async () => {
    setLoading(true);

    const response = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ priceId }),
    });

    const { url } = await response.json();
    window.location.href = url;
  };

  return (
    <button onClick={handleSubscribe} disabled={loading}>
      {loading ? 'Loading...' : 'Subscribe Now'}
    </button>
  );
}

Webhook Handling

Webhooks are crucial for keeping your database in sync.

// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { db } from '@/lib/db';

export async function POST(req: Request) {
  const body = await req.text();
  const signature = headers().get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    return Response.json({ error: 'Invalid signature' }, { status: 400 });
  }

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

      // Update user subscription status
      await db.update(users).set({
        stripeCustomerId: session.customer as string,
        stripeSubscriptionId: session.subscription as string,
        subscriptionStatus: 'active',
      }).where(eq(users.id, session.metadata!.userId));

      break;
    }

    case 'customer.subscription.updated': {
      const subscription = event.data.object as Stripe.Subscription;

      await db.update(users).set({
        subscriptionStatus: subscription.status,
      }).where(eq(users.stripeSubscriptionId, subscription.id));

      break;
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription;

      await db.update(users).set({
        subscriptionStatus: 'canceled',
        stripeSubscriptionId: null,
      }).where(eq(users.stripeSubscriptionId, subscription.id));

      break;
    }

    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice;

      // Send payment failure email
      await sendPaymentFailedEmail(invoice.customer_email);

      break;
    }
  }

  return Response.json({ received: true });
}

Customer Portal

Let users manage their subscription:

// app/api/billing-portal/route.ts
export async function POST(req: Request) {
  const session = await auth();
  const user = await db.query.users.findFirst({
    where: eq(users.id, session.user.id),
  });

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: user.stripeCustomerId!,
    return_url: `${process.env.NEXT_PUBLIC_WEB_URL}/dashboard`,
  });

  return Response.json({ url: portalSession.url });
}

Usage-Based Billing

For metered pricing (like credits):

// Report usage
await stripe.subscriptionItems.createUsageRecord(
  subscriptionItemId,
  {
    quantity: 100, // credits used
    timestamp: Math.floor(Date.now() / 1000),
    action: 'increment',
  }
);

Testing

Use Stripe test cards:

  • Success: 4242 4242 4242 4242
  • Decline: 4000 0000 0000 0002
  • 3D Secure: 4000 0025 0000 3155

Security Best Practices

  1. Never expose secret key - Use environment variables
  2. Validate webhooks - Always verify signatures
  3. Use HTTPS - Required for Stripe
  4. Handle errors gracefully - Payment failures happen

Go Live Checklist

  • Switch to live API keys
  • Configure webhook endpoint in Stripe Dashboard
  • Test all payment flows
  • Set up email notifications
  • Configure tax settings
  • Review subscription settings

Conclusion

Stripe makes payment processing straightforward. Follow these patterns, and you'll have a robust billing system.

Happy coding! 💳

D

Written by David Kim

Content creator and developer advocate passionate about helping developers build better products.