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

How to integrate PayPal in Node.js

Integrating PayPal in Node.js requires calling the PayPal v2 Checkout Orders API to create and capture orders server-side, keeping your Client Secret secure and verifying webhooks to fulfill orders reliably. As the creator of CoreUI with 25 years of backend development experience, I’ve integrated PayPal payments in e-commerce platforms alongside Stripe to give customers maximum payment choice. The two-step pattern — create order from frontend, capture on approval via server — ensures that payment capture only happens after user confirmation. Always verify webhooks from PayPal for asynchronous payment confirmation rather than trusting the browser callback alone.

Create a reusable PayPal API client.

// src/paypal/paypal.client.js
const BASE_URL = process.env.NODE_ENV === 'production'
  ? 'https://api-m.paypal.com'
  : 'https://api-m.sandbox.paypal.com'

async function getAccessToken() {
  const credentials = Buffer.from(
    `${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`
  ).toString('base64')

  const res = await fetch(`${BASE_URL}/v1/oauth2/token`, {
    method: 'POST',
    headers: {
      Authorization: `Basic ${credentials}`,
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: 'grant_type=client_credentials'
  })

  if (!res.ok) throw new Error('PayPal auth failed')
  const { access_token } = await res.json()
  return access_token
}

export async function paypalRequest(method, path, body) {
  const token = await getAccessToken()
  const res = await fetch(`${BASE_URL}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: body ? JSON.stringify(body) : undefined
  })

  const data = await res.json()
  if (!res.ok) throw new Error(data.message ?? 'PayPal request failed')
  return data
}

The paypalRequest helper fetches a fresh access token for every request. In production, cache the token until its expires_in time to reduce API calls.

Creating a PayPal Order

Create an order and return the approval URL to the frontend.

// src/paypal/paypal.router.js
import { Router } from 'express'
import { paypalRequest } from './paypal.client.js'

const router = Router()

router.post('/create-order', async (req, res, next) => {
  try {
    const { amount, currency = 'USD' } = req.body

    const order = await paypalRequest('POST', '/v2/checkout/orders', {
      intent: 'CAPTURE',
      purchase_units: [{
        amount: {
          currency_code: currency,
          value: Number(amount).toFixed(2)
        },
        custom_id: String(req.user.id)
      }],
      application_context: {
        return_url: `${process.env.CLIENT_URL}/payment/success`,
        cancel_url: `${process.env.CLIENT_URL}/payment/cancel`,
        user_action: 'PAY_NOW'
      }
    })

    res.json({ orderId: order.id })
  } catch (err) {
    next(err)
  }
})

custom_id stores your user ID in the order for reference in webhook events. user_action: 'PAY_NOW' shows “Pay Now” on the PayPal confirmation page instead of “Continue”.

Capturing Payment After Approval

Capture the payment after user approval.

router.post('/capture-order/:orderId', async (req, res, next) => {
  try {
    const { orderId } = req.params
    const capture = await paypalRequest(
      'POST',
      `/v2/checkout/orders/${orderId}/capture`,
      {}
    )

    const status = capture.status
    const transactionId = capture.purchase_units[0].payments.captures[0].id

    if (status === 'COMPLETED') {
      // Save transaction to database and fulfill order
      res.json({ success: true, transactionId })
    } else {
      res.status(400).json({ error: `Payment status: ${status}` })
    }
  } catch (err) {
    next(err)
  }
})

export { router as paypalRouter }

Only fulfill the order when the capture status is COMPLETED. Other statuses like PENDING require additional verification before fulfillment.

Best Practice Note

This is the same PayPal server-side integration pattern referenced in CoreUI e-commerce templates. For subscriptions and recurring payments, use PayPal’s Subscriptions API instead of the Checkout Orders API. For the React frontend consuming this API, see how to integrate PayPal in React. Always use PayPal’s sandbox environment for testing with test accounts created in the PayPal Developer Dashboard.


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

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team