Add Stripe Payments to React: Setup Guide (2026)
Add Stripe checkout to your React app with working code examples. Covers payment intents, webhooks, and subscription billing.
You built the app. People want to use it. Now you need them to pay you. If you want to add Stripe payments to React, the official docs will send you on a scavenger hunt across three different guides, a migration notice, and a changelog entry from 2024. This post gives you everything in one place with working code.
We will cover the full flow: installing the Stripe React SDK, creating a payment form with PaymentElement, setting up payment intents on your backend, handling webhooks, and adding subscription billing. By the end, you will have a working payment system that actually processes real money.
What You Need Before Starting
Before writing any code, you need:
- A Stripe account (free to create)
- Your publishable key and secret key from the Stripe Dashboard
- A React frontend (Create React App, Vite, Next.js — doesn’t matter)
- A Node.js backend (Express, Fastify, or any server that can handle HTTP requests)
Grab your test keys from the Stripe Dashboard under Developers > API Keys. The test keys let you process fake transactions without charging real cards.
Step 1: Install the Stripe React SDK
You need two packages on the frontend:
npm install @stripe/react-stripe-js @stripe/stripe-js
@stripe/stripe-js loads the Stripe.js library. @stripe/react-stripe-js gives you React components and hooks that wrap it.
On your backend, install the Stripe Node library:
npm install stripe
Step 2: Set Up the Stripe Provider
Wrap your app (or the part that handles payments) with Stripe’s Elements provider. This makes the Stripe context available to all child components.
// src/App.tsx
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import CheckoutForm from './CheckoutForm';
const stripePromise = loadStripe('pk_test_your_publishable_key');
function App() {
return (
<Elements stripe={stripePromise}>
<CheckoutForm />
</Elements>
);
}
export default App;
Important: Call loadStripe outside your component. If you call it inside, it will reload Stripe.js on every render, which is slow and will break things.
Step 3: Create a Payment Intent on the Backend
A Payment Intent is Stripe’s way of tracking a payment from creation to completion. Your backend creates one, and your frontend confirms it.
// server.js (Express)
const express = require('express');
const Stripe = require('stripe');
const cors = require('cors');
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const app = express();
app.use(cors());
app.use(express.json());
app.post('/api/create-payment-intent', async (req, res) => {
try {
const { amount, currency = 'usd' } = req.body;
const paymentIntent = await stripe.paymentIntents.create({
amount, // amount in cents — $10.00 = 1000
currency,
automatic_payment_methods: { enabled: true },
});
res.json({ clientSecret: paymentIntent.client_secret });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(4242, () => console.log('Server running on port 4242'));
The clientSecret is what your frontend needs to confirm the payment. Never expose your secret key to the client.
Step 4: Build the Payment Form with PaymentElement
PaymentElement is Stripe’s all-in-one form component. It handles cards, Apple Pay, Google Pay, bank transfers, and more — all in one element. It replaces the older CardElement and you should use it for all new projects.
First, update your app to fetch the client secret and pass it to Elements:
// src/App.tsx
import { useState, useEffect } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import CheckoutForm from './CheckoutForm';
const stripePromise = loadStripe('pk_test_your_publishable_key');
function App() {
const [clientSecret, setClientSecret] = useState('');
useEffect(() => {
fetch('/api/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 1000 }), // $10.00
})
.then((res) => res.json())
.then((data) => setClientSecret(data.clientSecret));
}, []);
if (!clientSecret) return <div>Loading...</div>;
return (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm />
</Elements>
);
}
export default App;
Now build the actual checkout form:
// src/CheckoutForm.tsx
import { useState } from 'react';
import {
PaymentElement,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
export default function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const [message, setMessage] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
setIsProcessing(true);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/payment-success`,
},
});
if (error) {
setMessage(error.message ?? 'Something went wrong.');
}
setIsProcessing(false);
};
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<button disabled={isProcessing || !stripe || !elements}>
{isProcessing ? 'Processing...' : 'Pay now'}
</button>
{message && <div>{message}</div>}
</form>
);
}
That is a fully working payment form. Stripe handles all the PCI compliance, card validation, and payment method rendering. You just confirm the payment and redirect on success.
Step 5: Handle Webhooks
Payments are asynchronous. A customer’s bank might take a few seconds (or hours for some payment methods) to confirm. Webhooks are how Stripe tells your server what happened.
// server.js — add this to your Express app
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
app.post(
'/api/webhooks/stripe',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
switch (event.type) {
case 'payment_intent.succeeded':
const payment = event.data.object;
console.log(`Payment ${payment.id} succeeded`);
// Grant access, send receipt, update database
break;
case 'payment_intent.payment_failed':
console.log('Payment failed:', event.data.object.id);
// Notify user, log failure
break;
case 'customer.subscription.deleted':
console.log('Subscription canceled:', event.data.object.id);
// Revoke access
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.json({ received: true });
}
);
Critical detail: The webhook route must use express.raw() for the body parser, not express.json(). If you parse the body as JSON first, signature verification will fail. This is the most common webhook bug and it wastes hours of debugging time.
To test webhooks locally, use the Stripe CLI:
stripe listen --forward-to localhost:4242/api/webhooks/stripe
This gives you a webhook signing secret that starts with whsec_. Use that as your STRIPE_WEBHOOK_SECRET env variable during development.
Step 6: Subscription Billing
One-time payments are nice, but subscriptions are how most SaaS products make money. Here is how to set up recurring billing.
First, create a product and price in the Stripe Dashboard (or via the API). Then create a checkout session for subscriptions:
// server.js
app.post('/api/create-subscription', async (req, res) => {
try {
const { email, priceId } = req.body;
// Create or retrieve customer
const customers = await stripe.customers.list({ email, limit: 1 });
let customer = customers.data[0];
if (!customer) {
customer = await stripe.customers.create({ email });
}
// Create subscription with a trial or immediate charge
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: priceId }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
});
res.json({
subscriptionId: subscription.id,
clientSecret:
subscription.latest_invoice.payment_intent.client_secret,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
On the frontend, you use the same PaymentElement and confirmPayment flow — the only difference is the client secret comes from the subscription’s payment intent instead of a standalone one.
Handle subscription lifecycle events in your webhook handler:
case 'invoice.payment_succeeded':
// Renewal payment went through — extend access
break;
case 'invoice.payment_failed':
// Payment failed — notify user, retry logic
break;
case 'customer.subscription.updated':
// Plan changed — update user's access level
break;
case 'customer.subscription.deleted':
// Subscription canceled — revoke access
break;
These four events cover 90% of subscription management. Listen for them and update your database accordingly.
Skip the Boilerplate
If this is starting to feel like a lot of plumbing code, that is because it is. Authentication, payments, webhook handling, subscription management — every SaaS app needs all of it, and every founder ends up building it from scratch.
Beag.io gives you auth and payments out of the box. You can add auth + payments to your app in 5 minutes and get back to building the features that actually make your product unique. If you are tired of wiring up Stripe for the third time, check it out.
Common Mistakes to Avoid
Using CardElement instead of PaymentElement. CardElement only handles cards. PaymentElement supports 25+ payment methods and automatically shows the most relevant ones based on the customer’s location. There is no reason to use CardElement in 2026.
Hardcoding amounts on the frontend. Always calculate prices on the server. A malicious user can modify frontend JavaScript to change the amount. The Payment Intent amount set on your backend is what Stripe actually charges.
Forgetting to handle the return_url properly. After payment confirmation, Stripe redirects to your return_url with query parameters like payment_intent and redirect_status. Parse those on your success page to show the right message.
Not verifying webhook signatures. Without signature verification, anyone can send fake webhook events to your server. Always use stripe.webhooks.constructEvent() with your webhook secret.
Testing Your Integration
Stripe provides test card numbers for every scenario:
| Card Number | Scenario |
|---|---|
| 4242 4242 4242 4242 | Successful payment |
| 4000 0000 0000 3220 | 3D Secure authentication required |
| 4000 0000 0000 9995 | Payment declined |
| 4000 0025 0000 3155 | Requires authentication |
Use any future expiry date, any 3-digit CVC, and any postal code. These cards only work with test API keys.
If you already have authentication set up — great. If not, you will need it before payments make sense. We covered how to do that in our guide on adding auth to a Next.js app. Auth and payments together form the backbone of any SaaS product.
FAQ
Do I need a backend to use Stripe with React?
Yes. You cannot create Payment Intents or handle webhooks from the frontend. Your secret API key must never be exposed to the browser. You need a server — even a serverless function on Vercel or AWS Lambda counts.
How much does Stripe cost?
Stripe charges 2.9% + 30 cents per successful transaction. There is no monthly fee, no setup cost, and no minimum. For subscriptions, Stripe Billing adds 0.5% on top of the standard processing fee. You only pay when you get paid.
Can I use Stripe with Next.js API routes instead of a separate backend?
Absolutely. Next.js API routes work perfectly as your backend. Put your Payment Intent creation in app/api/create-payment-intent/route.ts and your webhook handler in app/api/webhooks/stripe/route.ts. The code is identical — just formatted as Next.js route handlers instead of Express.
What is the difference between Stripe Checkout and PaymentElement?
Stripe Checkout is a hosted payment page — Stripe controls the entire UI and redirects users to stripe.com to pay. PaymentElement is an embeddable component that renders inside your app with your own design. Use Checkout if you want something working in 10 minutes. Use PaymentElement if you want full control over the look and feel.
Wrapping Up
Adding Stripe payments to a React app is not complicated, but it has a lot of moving parts: provider setup, backend endpoints, client secrets, webhook handlers, and subscription lifecycle management. Get each piece right and you have a reliable payment system.
If you would rather not build all of this yourself, Beag.io handles auth, payments, and subscription billing so you can focus on your actual product. Either way, now you have the code to do it from scratch.
Ready to Make Money From Your SaaS?
Turn your SaaS into cash with Beag.io. Get started now!
Start 7-day free trial →