Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to integrate Stripe in Node.js

Integrating Stripe in Node.js requires the Stripe SDK to create payment intents server-side, a webhook handler to confirm payments asynchronously, and proper error handling for declined cards and API failures. As the creator of CoreUI with 25 years of backend development experience, I’ve built Stripe integrations for multiple production SaaS and e-commerce platforms. The most important rule is that all payment logic lives on the server — never expose your secret key or process charges from the frontend. The server creates a payment intent, sends the client_secret to the frontend, and then receives confirmation via Stripe webhook when payment succeeds.

Install Stripe and initialize the client.

// src/stripe/stripe.client.js
import Stripe from 'stripe'

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2023-10-16'
})
// src/stripe/payments.router.js
import { Router } from 'express'
import { stripe } from './stripe.client.js'
import { PrismaClient } from '@prisma/client'

const router = Router()
const prisma = new PrismaClient()

Pinning the API version in the constructor ensures your integration doesn’t break when Stripe releases new API versions. Keep the client in a shared module so it’s instantiated once.

Creating a Payment Intent

Generate a payment intent and return the client secret to the frontend.

router.post('/create-intent', async (req, res, next) => {
  try {
    const { amount, currency = 'usd', customerId } = req.body
    const userId = req.user.id

    const intentData = {
      amount: Math.round(amount * 100), // cents
      currency,
      automatic_payment_methods: { enabled: true },
      metadata: { userId: String(userId) }
    }

    // Attach to Stripe customer if provided
    if (customerId) {
      intentData.customer = customerId
    }

    const intent = await stripe.paymentIntents.create(intentData)

    res.json({
      clientSecret: intent.client_secret,
      intentId: intent.id
    })
  } catch (err) {
    next(err)
  }
})

automatic_payment_methods enables all payment methods configured in your Stripe dashboard. Pass amount in cents — Stripe uses the smallest currency unit. The metadata field stores your internal user ID for reference in webhook events.

Creating and Managing Customers

Save customers in Stripe for future payments and subscriptions.

router.post('/customers', async (req, res, next) => {
  try {
    const { email, name } = req.body
    const userId = req.user.id

    const customer = await stripe.customers.create({
      email,
      name,
      metadata: { userId: String(userId) }
    })

    // Save Stripe customer ID in your database
    await prisma.user.update({
      where: { id: userId },
      data: { stripeCustomerId: customer.id }
    })

    res.json({ customerId: customer.id })
  } catch (err) {
    next(err)
  }
})

Storing the Stripe customer.id in your database links your user to their Stripe customer record, enabling saved payment methods, subscription management, and payment history.

Handling Stripe Webhooks

Receive and verify payment confirmation events.

// Webhook must receive raw body - register BEFORE express.json()
router.post(
  '/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['stripe-signature']
    let event

    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        sig,
        process.env.STRIPE_WEBHOOK_SECRET
      )
    } catch (err) {
      return res.status(400).send(`Webhook Error: ${err.message}`)
    }

    switch (event.type) {
      case 'payment_intent.succeeded': {
        const intent = event.data.object
        console.log(`Payment succeeded: ${intent.id}`)
        // Fulfill order, send email, etc.
        break
      }
      case 'payment_intent.payment_failed': {
        const intent = event.data.object
        console.log(`Payment failed: ${intent.last_payment_error?.message}`)
        break
      }
    }

    res.json({ received: true })
  }
)

stripe.webhooks.constructEvent verifies the stripe-signature header against your webhook secret. If the signature is invalid, it throws — reject the request with 400 to prevent replaying faked events.

Best Practice Note

This is the same server-side Stripe integration pattern used in CoreUI e-commerce and SaaS templates. For subscriptions, use stripe.subscriptions.create() instead of payment intents. For the React frontend that calls these endpoints, see how to integrate Stripe in React. Always test with Stripe test cards (4242 4242 4242 4242) and your webhook via stripe listen --forward-to localhost:3000/stripe/webhook during development.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author