Development3 min read

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.

Digital Applied Team
January 3, 2026
3 min read
135+

Currencies supported

SAQ A

PCI compliance tier (Checkout)

2.9% + $0.30

Standard US card rate

< 1 sec

Typical webhook delivery time

Key Takeaways

Use Checkout or Payment Links for fastest integration: Stripe Checkout (hosted payment page) reduces integration time from days to hours. It handles all payment method rendering, 3D Secure authentication, mobile optimization, and currency detection automatically. Only use custom Payment Elements when you need full UI control.
Webhooks are more reliable than redirect callbacks: Never fulfill orders based solely on the redirect URL after payment — users can close the browser tab before the redirect completes. Webhooks deliver payment confirmation reliably even when the user connection drops. Always use checkout.session.completed webhook for order fulfillment.
Store Stripe IDs, not payment details: Never store card numbers, CVVs, or raw payment data — PCI DSS prohibits this. Store Stripe customer IDs, subscription IDs, and price IDs in your database. Retrieve payment details from Stripe API when needed. This approach is SAQ A compliant — the lowest and simplest PCI compliance tier.
Idempotency keys prevent duplicate charges: Use idempotency keys on all Stripe API write operations. If a network timeout causes your server to retry a payment creation, the same idempotency key ensures Stripe returns the original result rather than creating a second charge. Use a UUID generated per transaction attempt.
Test all failure scenarios before go-live: Stripe provides test card numbers for every failure scenario: insufficient funds, card declined, 3D Secure required, expired card. Test each path in your checkout flow before production. The most common integration bug is missing error handling for declined cards — users see a broken page instead of an error message.

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.

ObjectID PrefixPurposeStore in DB?
Customercus_Represents a buyer — stores payment methods, subscriptionsYes (stripeCustomerId)
PaymentMethodpm_Card or bank details attached to a CustomerOptional (card last4, expiry only)
PaymentIntentpi_Single payment attempt — tracks 3DS, success, failureFor dispute tracking
CheckoutSessioncs_Hosted checkout flow — wraps PaymentIntent or SubscriptionTemporarily (until webhook confirms)
Productprod_Defines what you're sellingYes (for UI catalog)
Priceprice_Amount, currency, billing interval for a ProductYes (stripePrice Id)
Subscriptionsub_Recurring billing relationshipYes (status, period end)
Invoicein_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 EventWhen FiresAction Required
checkout.session.completedUser completes checkout (trial or paid)Create subscription record in DB, grant access
customer.subscription.updatedPlan change, trial end, status changeUpdate subscription status and plan in DB
customer.subscription.trial_will_end3 days before trial endsSend reminder email to user
invoice.payment_succeededSuccessful recurring paymentUpdate period_end in DB, send receipt
invoice.payment_failedPayment fails (retry scheduled)Email user to update payment method
customer.subscription.deletedSubscription 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.

SAQ A (Stripe Checkout / Elements)
  • 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
SAQ D (Custom card handling)
  • 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 NumberScenarioUse To Test
4242 4242 4242 4242Successful paymentHappy path checkout flow
4000 0025 0000 31553D Secure required3DS authentication flow
4000 0000 0000 9995Insufficient fundsDeclined card error handling
4000 0000 0000 0069Expired cardExpired card error message
4000 0000 0000 0002Card declined (generic)Generic decline error handling
4000 0000 0000 0127Incorrect CVCCVC error message

Go-Live Checklist

Switch all STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY to live mode keys
Register production webhook endpoint in Stripe Dashboard (separate from test endpoint)
Update STRIPE_WEBHOOK_SECRET to the live webhook signing secret
Enable Stripe Radar (fraud prevention) — review default rules
Configure email receipts in Stripe Dashboard
Set up Stripe billing email templates for subscription notifications
Enable Stripe Tax if applicable (automatic tax calculation)
Configure payout schedule (daily, weekly, or manual)
Review disputed payment policy and set up Stripe Radar rules
Test one real transaction with a physical card before launch

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.