Stripe Integration in Next.js: Subscription Billing and Payment Processing | SoniNow Blog

Limited TimeLearn More

stripepaymentssubscriptionsnext.jsecommerce

Stripe Integration in Next.js: Subscription Billing and Payment Processing

Published

2026-06-23

Read Time

4 mins

Stripe Integration in Next.js: Subscription Billing and Payment Processing

Stripe remains the gold standard for payment processing on the web, and its integration with Next.js has become more seamless with each framework release. Whether you're building a SaaS with recurring subscriptions or an e-commerce storefront with one-time purchases, the combination of Stripe's APIs and Next.js's App Router provides a robust foundation for handling money on the internet.

Setting Up Stripe in Next.js

Start by installing the Stripe SDK and its TypeScript types. The server-side client should be initialized in a shared module with your secret key, while the client-side @stripe/react-stripe-js package handles tokenized card entry:

// lib/stripe-server.ts
import Stripe from 'stripe'

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2025-02-24', // latest stable
  typescript: true,
})
// app/providers/stripe-provider.tsx
'use client'

import { Elements } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!)

export function StripeProvider({ children }: { children: React.ReactNode }) {
  return (
    <Elements stripe={stripePromise}>
      {children}
    </Elements>
  )
}

Never expose the secret key to the client. Use Server Components or Route Handlers for all Stripe API calls that require authentication.

Creating Checkout Sessions

The recommended approach for accepting payments is Stripe Checkout—a hosted payment page that handles card entry, address collection, and payment method storage:

// app/api/checkout/route.ts
import { stripe } from '@/lib/stripe-server'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(req: NextRequest) {
  const { priceId, userId } = await req.json()

  const session = await stripe.checkout.sessions.create({
    customer_email: user.email,
    line_items: [{ price: priceId, quantity: 1 }],
    mode: 'subscription',
    success_url: `${req.headers.get('origin')}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${req.headers.get('origin')}/pricing`,
    metadata: { userId },
  })

  return NextResponse.json({ url: session.url })
}

Redirect the user to session.url and Stripe handles the rest. After payment, the customer is redirected back to your success URL with the session ID as a query parameter.

Handling Webhooks for Subscription Lifecycle

Webhooks are critical for subscription management. Stripe sends events when invoices are paid, subscriptions renew, or payments fail. Use Next.js Route Handlers with raw body parsing (required for signature verification):

// app/api/webhooks/stripe/route.ts
import { stripe } from '@/lib/stripe-server'
import { NextRequest, NextResponse } from 'next/server'

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

  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(
      body, sig, process.env.STRIPE_WEBHOOK_SECRET!
    )
  } catch {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object
      await activateUserSubscription(session.metadata.userId, session.subscription)
      break
    }
    case 'invoice.payment_failed': {
      const invoice = event.data.object
      await notifyPaymentFailure(invoice.subscription)
      break
    }
    case 'customer.subscription.deleted': {
      const subscription = event.data.object
      await deactivateSubscription(subscription.id)
      break
    }
  }

  return NextResponse.json({ received: true })
}

Test webhooks locally using the Stripe CLI, which forwards events to your local server with stripe listen --forward-to localhost:3000/api/webhooks/stripe.

Customer Portal for Self-Service

Stripe's Customer Portal allows users to manage their subscription without exposing your admin interface. Generate a portal session link and redirect users there:

export async function createPortalSession(customerId: string) {
  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard`,
  })
  return session.url
}

The portal handles plan upgrades, downgrades, payment method updates, and cancellations. It saves months of development time compared to building these UIs yourself.

Error Handling and Resilience

Payment failures happen. Stripe's API returns specific error types that map to user-facing messages:

try {
  await stripe.paymentIntents.create({ ... })
} catch (error) {
  if (error instanceof Stripe.errors.StripeCardError) {
    return { error: 'Your card was declined. Please try a different payment method.' }
  }
  if (error instanceof Stripe.errors.RateLimitError) {
    return { error: 'Too many requests. Please try again.' }
  }
  // Log to error tracking
  return { error: 'An unexpected error occurred.' }
}

For recurring billing, implement dunning—automated retry logic that Stripe provides out of the box. Configure Smart Retries in the Stripe dashboard to retry failed payments on a schedule that maximizes success rates.

Stripe and Next.js together give you a production-ready payment infrastructure in a single codebase. From checkout sessions to recurring billing to webhook-driven fulfillment, the patterns above form the backbone of any serious web application that handles money.

<a href="/services/web-development">Our web development team</a> has deep experience with Stripe integrations and can accelerate your go-to-market timeline. Let's talk about your payment requirements.