payments
development
saas
Stripe Integration Guide: Building Payment Flows for SaaS
Introduction
Integrating payments is one of the most critical aspects of building a SaaS product. Stripe has become the industry standard payment infrastructure, powering businesses of all sizes from startups to enterprises. This guide covers both one-time payments and subscription flows with Stripe, based on Stripe's official documentation.
Before You Start Coding
Before diving into implementation, several key considerations will save you time and prevent future refactoring. According to Stripe's planning guide, you should address these questions early:
1. Account Structure Planning
Decide whether you need a single Stripe account or multiple accounts:
- Single account: Simplest approach for most businesses
- Multiple accounts: Required for marketplaces, platforms with multiple merchants, or businesses operating in multiple legal entities
For more detailed guidance on account structure, see Stripe's account structure documentation.
2. Integration Security Requirements
Ensure your implementation follows security best practices:
- Use Stripe.js or Elements to avoid handling sensitive card data
- Set up proper TLS/SSL for all communications
- Understand PCI compliance requirements
- Implement proper server-side validation
Stripe provides a comprehensive integration security guide to help you maintain PCI compliance.
3. Choosing Between Checkout and Elements
Stripe offers two main ways to create payment forms, each with different tradeoffs:
Options | Stripe Checkout | Stripe.js and Elements |
---|---|---|
Description | Stripe Checkout is a secure, Stripe-hosted page that lets you collect payments quickly. It works across devices and can help increase conversion. | Elements is a set of prebuilt UI components for building your custom checkout flow. Stripe.js tokenizes sensitive payment details without letting them touch your server. |
Benefits | Simplified integration Up-to-date with available payment methods Optimized conversion Co-branded with your business logo and colors | Optimized conversion with dynamic inputs Simplified PCI compliance with SAQ A reporting Customizable styling to match your site |
Limitations | Temporarily redirects customers off your web domain Fewer options for customization | Increased integration time and effort Elements doesn't support all payment methods |
For this guide, we'll focus on Checkout for its simplicity, but the concepts apply to Elements implementations as well.
4. Business Model Selection
For subscription businesses, determine which model fits your needs according to Stripe's subscription integration guide:
- Pay up front: Collect payment details and charge before providing access
- Free trial: Collect payment details, offer free period, then begin charging
- Freemium: Provide limited access without payment details, charge for premium features
One-Time Payment Integration
Let's start with implementing a basic one-time payment flow using Stripe Checkout, Stripe's pre-built, hosted payment page.
Step 1: Set Up Your Stripe Account and Install Dependencies
First, create a Stripe account if you don't have one. Then install the necessary libraries:
# Install Stripe server-side library in Node.js
npm install stripe
# For client-side integration
npm install @stripe/stripe-js
Step 2: Create a Product and Price in Stripe Dashboard
Create your products and prices in the Stripe Dashboard, or programmatically:
// Server-side code to create a product and price
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Create a product
// Documentation: https://docs.stripe.com/api/products/create
const product = await stripe.products.create({
name: "Basic Plan",
description: "One-time purchase for basic access",
});
// Create a price for the product
// Documentation: https://docs.stripe.com/api/prices/create
const price = await stripe.prices.create({
product: product.id,
unit_amount: 1999, // $19.99 in cents
currency: "usd",
});
console.log(`Product created with ID: ${product.id}`);
console.log(`Price created with ID: ${price.id}`);
Make sure to store the price ID, as you'll need it for creating checkout sessions.
Step 3: Create a Checkout Session (Server-Side)
Following Stripe's Checkout documentation:
// Next.js API route for creating a checkout session
import { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
// Create the checkout session
// Documentation: https://docs.stripe.com/api/checkout/sessions/create
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: [
{
price: process.env.PRICE_ID, // Your actual Price ID from Step 2
quantity: 1,
},
],
mode: "payment", // One-time payment
success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.headers.origin}/canceled`,
// Optional: Prefill customer email
customer_email: req.body.email,
});
res.status(200).json({ sessionId: session.id, url: session.url });
} catch (error) {
console.error("Error creating checkout session:", error);
res.status(500).json({ error: "Failed to create checkout session" });
}
}
Step 4: Redirect to Checkout (Client-Side)
// React component to handle checkout
import React from "react";
import { loadStripe } from "@stripe/stripe-js";
// Initialize Stripe with your publishable key
// Documentation: https://docs.stripe.com/js/initializing
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
);
export default function CheckoutButton() {
const handleCheckout = async () => {
try {
// Call your backend to create the Checkout Session
const response = await fetch("/api/create-checkout-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "[email protected]", // Optional: prefill customer email
}),
});
const { url } = await response.json();
// Redirect to Checkout
window.location.href = url;
} catch (error) {
console.error("Error:", error);
}
};
return (
<button
onClick={handleCheckout}
className="bg-blue-600 text-white px-4 py-2 rounded"
>
Buy Now ($19.99)
</button>
);
}
Step 5: Handle Successful Payments with Webhooks
Setting up webhooks is critical for reliable payment processing. Follow Stripe's webhook guide:
// Next.js API route for Stripe webhooks
import { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
import { buffer } from "micro";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
export const config = {
api: {
bodyParser: false,
},
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).end();
}
const buf = await buffer(req);
const sig = req.headers["stripe-signature"];
let event;
try {
// Verify webhook signature and extract the event
// Documentation: https://docs.stripe.com/webhooks/signatures
event = stripe.webhooks.constructEvent(buf.toString(), sig, webhookSecret);
} catch (err) {
console.error(`Webhook signature verification failed: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle specific events
if (event.type === "checkout.session.completed") {
const session = event.data.object;
// Fulfill the order
await fulfillOrder(session);
}
res.status(200).json({ received: true });
}
async function fulfillOrder(session) {
// Implement your order fulfillment logic here
// e.g., update database, grant access, send confirmation email
console.log(`Order fulfilled for session ${session.id}`);
// Retrieve the session with line items to get product details
// Documentation: https://docs.stripe.com/api/checkout/sessions/retrieve
const checkoutSession = await stripe.checkout.sessions.retrieve(session.id, {
expand: ["line_items"],
});
// Now you can access product information
const lineItems = checkoutSession.line_items;
// Process the purchased items
}
Subscription Payment Integration
Now, let's implement a subscription-based payment flow using Stripe Checkout, following Stripe's subscription guide.
Step 1: Create Subscription Products and Prices
// Server-side code to create subscription products and prices
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Create a product for subscription
// Documentation: https://docs.stripe.com/api/products/create
const product = await stripe.products.create({
name: "Pro Plan",
description: "Monthly subscription for premium features",
});
// Create a recurring price
// Documentation: https://docs.stripe.com/api/prices/create
const price = await stripe.prices.create({
product: product.id,
unit_amount: 2999, // $29.99 in cents
currency: "usd",
recurring: {
interval: "month",
},
});
console.log(`Subscription product created with ID: ${product.id}`);
console.log(`Subscription price created with ID: ${price.id}`);
Step 2: Create a Subscription Checkout Session
// Next.js API route for creating a subscription checkout session
import { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const { priceId, customerId } = req.body;
try {
// Create the checkout session for subscription
// Documentation: https://docs.stripe.com/api/checkout/sessions/create
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: [
{
price: priceId, // Use the price ID from Step 1
quantity: 1,
},
],
mode: "subscription", // Set mode to subscription
success_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.headers.origin}/pricing`,
// If you have an existing customer, pass their ID
customer: customerId || undefined,
customer_email: customerId ? undefined : req.body.email,
// Enable automatic tax calculation if needed
// automatic_tax: { enabled: true },
// Allow promotion codes
allow_promotion_codes: true,
});
res.status(200).json({ sessionId: session.id, url: session.url });
} catch (error) {
console.error("Error creating subscription session:", error);
res.status(500).json({ error: "Failed to create subscription session" });
}
}
Step 3: Set Up Customer Portal for Subscription Management
Stripe's Customer Portal allows customers to manage their own subscriptions:
// Next.js API route for creating a customer portal session
import { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const { customerId } = req.body;
try {
// Create customer portal session
// Documentation: https://docs.stripe.com/api/customer_portal/sessions/create
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${req.headers.origin}/account`,
// Optional: Configure the portal features
// configuration: 'conf_xyz'
});
res.status(200).json({ url: session.url });
} catch (error) {
console.error("Error creating portal session:", error);
res.status(500).json({ error: "Failed to create portal session" });
}
}
Step 4: Handle Subscription Lifecycle Events
Following Stripe's webhook events guide:
// Handling subscription lifecycle events in webhook handler
// Add these cases to your existing webhook handler
async function handler(req: NextApiRequest, res: NextApiResponse) {
// ... existing webhook setup code ...
// Handle different event types
// Documentation: https://docs.stripe.com/webhooks/stripe-events
switch (event.type) {
case "customer.subscription.created":
// New subscription created
await handleSubscriptionCreated(event.data.object);
break;
case "customer.subscription.updated":
// Subscription updated (plan change, etc.)
await handleSubscriptionUpdated(event.data.object);
break;
case "customer.subscription.deleted":
// Subscription cancelled or expired
await handleSubscriptionCancelled(event.data.object);
break;
case "invoice.payment_failed":
// Failed payment for subscription
await handleFailedPayment(event.data.object);
break;
case "invoice.paid":
// Successful payment - extend access
await handleSuccessfulPayment(event.data.object);
break;
}
res.status(200).json({ received: true });
}
async function handleSubscriptionCreated(subscription) {
// Grant access to your service
// Update your database with new subscription status
console.log(`New subscription started: ${subscription.id}`);
// Retrieve the customer to get their email or custom metadata
const customer = await stripe.customers.retrieve(subscription.customer);
// Update your user records with subscription details
}
async function handleSubscriptionUpdated(subscription) {
// Update access level based on new subscription plan
console.log(`Subscription updated: ${subscription.id}`);
// Check new status and items to update user permissions
const newStatus = subscription.status;
const items = subscription.items.data;
// Update your database accordingly
}
async function handleSubscriptionCancelled(subscription) {
// Remove access or downgrade to free tier
console.log(`Subscription cancelled: ${subscription.id}`);
// Check if access should end immediately or at period end
const endDate = subscription.cancel_at || subscription.current_period_end;
}
async function handleFailedPayment(invoice) {
// Notify customer, retry payment, or temporarily restrict access
console.log(`Payment failed for invoice: ${invoice.id}`);
// Get customer information
const customer = await stripe.customers.retrieve(invoice.customer);
// Send email notification about failed payment
}
async function handleSuccessfulPayment(invoice) {
// Extend subscription access period
console.log(`Payment succeeded for invoice: ${invoice.id}`);
// Update subscription status in your database
}
Advanced Stripe Features for SaaS
Free Trial Implementation
To offer a free trial with Stripe, follow the free trial documentation:
// Documentation: https://docs.stripe.com/api/checkout/sessions/create
const session = await stripe.checkout.sessions.create({
// ... other parameters ...
line_items: [
{
price: process.env.SUBSCRIPTION_PRICE_ID, // Your subscription price ID
quantity: 1,
},
],
mode: "subscription",
subscription_data: {
trial_period_days: 14, // 14-day free trial
},
payment_method_collection: "always", // Collect payment method upfront
});
Usage-Based Billing
For metered billing where you charge based on usage, follow Stripe's metered billing guide:
- Create a metered price:
// Documentation: https://docs.stripe.com/api/prices/create
const price = await stripe.prices.create({
product: process.env.PRODUCT_ID,
unit_amount: 100, // $1.00 per unit
currency: "usd",
recurring: {
interval: "month",
usage_type: "metered", // Specify metered billing
},
});
- Report usage:
// Documentation: https://docs.stripe.com/api/usage_records/create
await stripe.subscriptionItems.createUsageRecord(
"si_123ABC", // Subscription item ID
{
quantity: 10, // Number of units used
timestamp: Math.floor(Date.now() / 1000),
action: "increment", // Add to the existing usage
}
);
Apply Discount Coupons
To offer discount coupons, follow Stripe's discount documentation:
// Documentation: https://docs.stripe.com/api/checkout/sessions/create
const session = await stripe.checkout.sessions.create({
// ... other parameters ...
discounts: [
{
coupon: "SUMMER20", // Coupon code created in Stripe Dashboard
},
],
});
Best Practices for Stripe Integration
1. Always Use Webhooks
Don't rely solely on client-side success redirects. Use webhooks to reliably track payment outcomes and subscription status changes. Stripe provides guidance in their webhook best practices.
2. Implement Idempotency
Use idempotency keys for API requests that might be retried to prevent duplicate transactions:
// Documentation: https://docs.stripe.com/api/idempotent_requests
await stripe.customers.create(
{ email: "[email protected]" },
{ idempotencyKey: "unique-key-123" }
);
3. Test Thoroughly with Stripe CLI
Use the Stripe CLI to test webhooks locally:
# Install the CLI (macOS)
brew install stripe/stripe-cli/stripe
# Forward webhooks to your local server
stripe listen --forward-to localhost:3000/api/webhooks
4. Store Stripe Customer IDs
Always store Stripe customer IDs in your database to link users with their payment history and subscriptions:
// Create a mapping in your database between your user ID and Stripe customer ID
db.users.update({ id: userId }, { stripeCustomerId: customer.id });
5. Handle Failed Payments Gracefully
Implement proper failed payment handling with appropriate retry logic and customer communication. Stripe provides automatic retries for failed subscription payments.
Conclusion
Integrating Stripe into your SaaS application gives you a robust payment infrastructure that scales with your business. By properly implementing both one-time and subscription payment flows, you can offer your customers flexible payment options while maintaining a secure and compliant payment system.
For more complex use cases like metered billing, free trials, or marketplace payments, Stripe provides extensive documentation and SDKs to handle virtually any payment scenario. Remember to always follow security best practices, properly test your integration, and set up reliable webhook handling to ensure your payment flows work flawlessly.
Additional Resources

Fekri