Stripe Payment Integration: Complete Dev Guide 2026
Integrate Stripe payments into your web application. Checkout sessions, subscriptions, webhooks, and PCI compliance best practices for developers.
Currencies supported
PCI compliance tier (Checkout)
Standard US card rate
Typical webhook delivery time
Key Takeaways
Stripe is the de facto payment infrastructure for modern web applications. With over 135 currency support, built-in fraud prevention, and an API designed for developers, it powers payment flows from one-time purchases to complex subscription billing with usage-based pricing. This guide covers production-ready integration patterns — not just the happy path.
Code examples use the Stripe Node.js SDK v17 (API version 2026-01-28) in Next.js App Router context with TypeScript. The patterns apply equally to Express, Fastify, or any Node.js runtime.
Stripe Architecture
Stripe's core objects form a hierarchy. Understanding how they relate prevents the most common integration mistakes — particularly around Customer, PaymentMethod, and PaymentIntent relationships.
| Object | ID Prefix | Purpose | Store in DB? |
|---|---|---|---|
| Customer | cus_ | Represents a buyer — stores payment methods, subscriptions | Yes (stripeCustomerId) |
| PaymentMethod | pm_ | Card or bank details attached to a Customer | Optional (card last4, expiry only) |
| PaymentIntent | pi_ | Single payment attempt — tracks 3DS, success, failure | For dispute tracking |
| CheckoutSession | cs_ | Hosted checkout flow — wraps PaymentIntent or Subscription | Temporarily (until webhook confirms) |
| Product | prod_ | Defines what you're selling | Yes (for UI catalog) |
| Price | price_ | Amount, currency, billing interval for a Product | Yes (stripePrice Id) |
| Subscription | sub_ | Recurring billing relationship | Yes (status, period end) |
| Invoice | in_ | Single billing event (created automatically for subscriptions) | For receipts/accounting |
Checkout Sessions
Stripe Checkout is the fastest path to a working payment flow. Create a session server-side, return the URL, and redirect the user. Stripe handles everything including mobile optimization, payment method selection, 3DS authentication, and receipt emails.
// Next.js App Router — Create checkout session (Server Action)
"use server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2026-01-28",
});
export async function createCheckoutSession(priceId: string, userId: string) {
const session = await stripe.checkout.sessions.create({
mode: "payment", // or "subscription" for recurring
payment_method_types: ["card"], // or remove to use automatic_payment_methods
automatic_payment_methods: { enabled: true }, // recommended
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
client_reference_id: userId, // link session to your user ID
customer_creation: "always", // create Stripe Customer for reuse
metadata: { userId }, // accessible in webhook payload
allow_promotion_codes: true, // enable discount codes
});
return session.url;
}// Server Action invocation pattern (Next.js 15)
// Client component
"use client";
import { useTransition } from "react";
import { createCheckoutSession } from "@/app/actions/stripe";
export function CheckoutButton({ priceId }: { priceId: string }) {
const [isPending, startTransition] = useTransition();
const handleCheckout = () => {
startTransition(async () => {
const url = await createCheckoutSession(priceId, userId);
if (url) { window.location.href = url; }
});
};
return (
<button disabled={isPending} onClick={handleCheckout} type="button">
{isPending ? "Redirecting..." : "Purchase"}
</button>
);
}Subscription Billing
Subscription billing adds recurring revenue with Stripe's Customer and Subscription objects. The recommended pattern uses Checkout with mode=subscription — Stripe handles trial periods, failed payment retries (Smart Retries), dunning emails, and proration for plan changes.
// Subscription checkout with trial period
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: monthlyPriceId, quantity: 1 }],
subscription_data: {
trial_period_days: 14, // 14-day free trial
metadata: { userId },
},
success_url: `${origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/pricing`,
customer_email: user.email, // pre-fill if not using customer ID
allow_promotion_codes: true,
});
// Key subscription statuses to handle in webhooks:
// trialing → access granted (trial period)
// active → access granted (paid)
// past_due → payment failed, retrying (warn user)
// unpaid → max retries exceeded (revoke access)
// canceled → subscription ended (revoke access)
// incomplete → requires payment action (3DS pending)| Webhook Event | When Fires | Action Required |
|---|---|---|
| checkout.session.completed | User completes checkout (trial or paid) | Create subscription record in DB, grant access |
| customer.subscription.updated | Plan change, trial end, status change | Update subscription status and plan in DB |
| customer.subscription.trial_will_end | 3 days before trial ends | Send reminder email to user |
| invoice.payment_succeeded | Successful recurring payment | Update period_end in DB, send receipt |
| invoice.payment_failed | Payment fails (retry scheduled) | Email user to update payment method |
| customer.subscription.deleted | Subscription canceled (final) | Revoke access, send cancellation email |
For the eCommerce context — headless stores with Stripe as the payment backend — see our headless commerce Next.js guide for the full payment integration architecture.
Webhook Handling
Webhooks are the most critical reliability component in any Stripe integration. A webhook handler that returns non-200 responses causes Stripe to retry for up to 72 hours with exponential backoff — and a handler that crashes silently means your database is permanently out of sync with Stripe's state.
// Next.js App Router webhook route — /app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
import { NextRequest, NextResponse } from "next/server";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(request: NextRequest) {
const body = await request.text(); // CRITICAL: raw text, not JSON
const signature = request.headers.get("stripe-signature");
if (!signature) {
return NextResponse.json({ error: "No signature" }, { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
// Always return 200 for events you don't handle
try {
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutComplete(session);
break;
}
case "customer.subscription.updated":
case "customer.subscription.deleted": {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionChange(subscription);
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentFailed(invoice);
break;
}
}
} catch (err) {
// Log the error but still return 200 to prevent Stripe retries
// for application-level errors (DB down, etc.)
// Use a dead letter queue for failed events
return NextResponse.json({ error: "Processing failed" }, { status: 500 });
}
return NextResponse.json({ received: true });
}PCI Compliance
Payment Card Industry Data Security Standard (PCI DSS) compliance is required for any business that processes, stores, or transmits cardholder data. Using Stripe Checkout or Stripe.js elements reduces your compliance scope to SAQ A — the simplest tier.
- Card data never touches your servers
- Annual self-assessment questionnaire only
- No on-site scan required
- Roughly 22 requirements to attest
- Most hosting providers are already compliant
- Stripe provides SAQ A documentation
- Full PCI DSS compliance required
- 350+ requirements to implement
- Annual on-site QSA assessment
- Quarterly network scans required
- Significant ongoing cost ($10,000-$100,000+/year)
- Almost never worth building vs. using Stripe
Error Handling
Stripe error handling is essential for user experience and fraud prevention. Stripe returns structured errors with machine-readable codes that allow you to display helpful messages rather than generic errors.
// Stripe error handling pattern
import Stripe from "stripe";
async function createCharge() {
try {
const paymentIntent = await stripe.paymentIntents.create({ ... });
} catch (err) {
if (err instanceof Stripe.errors.StripeCardError) {
// Card declined — show user-friendly message
switch (err.code) {
case "card_declined":
return { error: "Your card was declined. Please use a different card." };
case "insufficient_funds":
return { error: "Insufficient funds. Please use a different card." };
case "expired_card":
return { error: "Your card has expired. Please update your payment method." };
case "incorrect_cvc":
return { error: "Incorrect CVC. Please check your card details." };
case "processing_error":
return { error: "A processing error occurred. Please try again." };
default:
return { error: err.message };
}
} else if (err instanceof Stripe.errors.StripeRateLimitError) {
// Too many requests — implement exponential backoff
} else if (err instanceof Stripe.errors.StripeInvalidRequestError) {
// Invalid parameters — log and fix the code
} else if (err instanceof Stripe.errors.StripeConnectionError) {
// Network issue — safe to retry
} else {
throw err; // Unknown error — let it propagate
}
}
}Testing & Go-Live
Test mode uses separate API keys and simulated card numbers. Stripe provides test cards for every scenario — use them to validate every code path before going live.
| Test Card Number | Scenario | Use To Test |
|---|---|---|
| 4242 4242 4242 4242 | Successful payment | Happy path checkout flow |
| 4000 0025 0000 3155 | 3D Secure required | 3DS authentication flow |
| 4000 0000 0000 9995 | Insufficient funds | Declined card error handling |
| 4000 0000 0000 0069 | Expired card | Expired card error message |
| 4000 0000 0000 0002 | Card declined (generic) | Generic decline error handling |
| 4000 0000 0000 0127 | Incorrect CVC | CVC error message |
Go-Live Checklist
Our eCommerce solutions service includes full Stripe integration — from initial setup through webhook architecture, subscription management, and ongoing payment infrastructure maintenance.
Need help with your payment integration?
Our development team builds production-ready Stripe integrations — one-time payments, subscriptions, webhooks, and fraud prevention configured correctly from day one.
Frequently Asked Questions
Related Guides
Continue exploring web development and eCommerce architecture.