Stripe Integration
Accept payments with Stripe - one-time payments, subscriptions, and checkout flows.
9 min read
Stripe Overview#
Stripe is the most popular payment processor for developers:
| Feature | Description |
|---|---|
| Checkout | Pre-built payment page |
| Elements | Embedded card inputs |
| Payment Intents | Custom payment flows |
| Subscriptions | Recurring billing |
| Connect | Marketplace payments |
Setup#
bash
npm install stripe
javascript
// src/lib/stripe.js
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2023-10-16',
});
Option 1: Checkout Sessions (Easiest)#
Let Stripe handle the entire checkout UI:
javascript
// src/services/paymentService.js
import { stripe } from '../lib/stripe.js';
export const PaymentService = {
// One-time payment
async createCheckoutSession(userId, items) {
const lineItems = items.map(item => ({
price_data: {
currency: 'usd',
product_data: {
name: item.name,
images: item.image ? [item.image] : [],
},
unit_amount: Math.round(item.price * 100), // Cents
},
quantity: item.quantity,
}));
const session = await stripe.checkout.sessions.create({
mode: 'payment',
payment_method_types: ['card'],
line_items: lineItems,
success_url: `${process.env.APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/checkout/cancel`,
metadata: {
userId,
},
});
return { url: session.url, sessionId: session.id };
},
// Subscription checkout
async createSubscriptionCheckout(userId, priceId) {
// Get or create Stripe customer
const user = await UserService.findById(userId);
let customerId = user.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
email: user.email,
name: user.name,
metadata: { userId },
});
customerId = customer.id;
await UserService.update(userId, { stripeCustomerId: customerId });
}
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer: customerId,
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.APP_URL}/subscription/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/pricing`,
metadata: { userId },
subscription_data: {
metadata: { userId },
},
});
return { url: session.url };
},
};
API Endpoints#
javascript
// src/routes/payments.js
import express from 'express';
import { PaymentService } from '../services/paymentService.js';
import { authenticate } from '../middleware/auth.js';
const router = express.Router();
// Create checkout session
router.post('/checkout', authenticate, async (req, res) => {
const { items } = req.body;
const session = await PaymentService.createCheckoutSession(
req.user.id,
items
);
res.json({ data: session });
});
// Create subscription checkout
router.post('/subscribe', authenticate, async (req, res) => {
const { priceId } = req.body;
const session = await PaymentService.createSubscriptionCheckout(
req.user.id,
priceId
);
res.json({ data: session });
});
// Verify session after success
router.get('/checkout/verify', authenticate, async (req, res) => {
const { session_id } = req.query;
const session = await stripe.checkout.sessions.retrieve(session_id);
if (session.payment_status === 'paid') {
// Order was already created via webhook, just return success
res.json({ status: 'success', session });
} else {
res.json({ status: 'pending', session });
}
});
export default router;
Option 2: Payment Intents (More Control)#
For custom checkout experiences:
javascript
// src/services/paymentService.js
export const PaymentService = {
// Create payment intent for frontend
async createPaymentIntent(userId, amount, currency = 'usd') {
const user = await UserService.findById(userId);
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(amount * 100),
currency,
customer: user.stripeCustomerId,
metadata: { userId },
automatic_payment_methods: {
enabled: true,
},
});
return {
clientSecret: paymentIntent.client_secret,
paymentIntentId: paymentIntent.id,
};
},
// Confirm payment was successful
async verifyPayment(paymentIntentId) {
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
return {
status: paymentIntent.status,
amount: paymentIntent.amount / 100,
currency: paymentIntent.currency,
};
},
};
// API endpoint
router.post('/create-payment-intent', authenticate, async (req, res) => {
const { amount } = req.body;
const { clientSecret } = await PaymentService.createPaymentIntent(
req.user.id,
amount
);
res.json({ clientSecret });
});
Frontend with Stripe Elements#
javascript
// React frontend example
import { loadStripe } from '@stripe/stripe-js';
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
function CheckoutForm({ clientSecret }) {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/checkout/success`,
},
});
if (error) {
setError(error.message);
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
{error && <div className="error">{error}</div>}
<button disabled={!stripe || loading}>
{loading ? 'Processing...' : 'Pay'}
</button>
</form>
);
}
function Checkout({ amount }) {
const [clientSecret, setClientSecret] = useState(null);
useEffect(() => {
fetch('/api/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount }),
})
.then(res => res.json())
.then(data => setClientSecret(data.clientSecret));
}, [amount]);
if (!clientSecret) return <div>Loading...</div>;
return (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm clientSecret={clientSecret} />
</Elements>
);
}
Subscriptions#
Create Subscription#
javascript
// src/services/subscriptionService.js
import { stripe } from '../lib/stripe.js';
import { prisma } from '../lib/prisma.js';
export const SubscriptionService = {
async createSubscription(userId, priceId) {
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user.stripeCustomerId) {
throw new Error('User has no Stripe customer');
}
const subscription = await stripe.subscriptions.create({
customer: user.stripeCustomerId,
items: [{ price: priceId }],
payment_behavior: 'default_incomplete',
payment_settings: {
save_default_payment_method: 'on_subscription',
},
expand: ['latest_invoice.payment_intent'],
metadata: { userId },
});
return {
subscriptionId: subscription.id,
clientSecret: subscription.latest_invoice.payment_intent.client_secret,
};
},
async cancelSubscription(userId) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { stripeSubscriptionId: true },
});
if (!user.stripeSubscriptionId) {
throw new Error('No active subscription');
}
// Cancel at period end (user keeps access until then)
await stripe.subscriptions.update(user.stripeSubscriptionId, {
cancel_at_period_end: true,
});
return { message: 'Subscription will cancel at period end' };
},
async cancelImmediately(userId) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { stripeSubscriptionId: true },
});
await stripe.subscriptions.cancel(user.stripeSubscriptionId);
await prisma.user.update({
where: { id: userId },
data: {
stripeSubscriptionId: null,
subscriptionStatus: 'canceled',
},
});
return { message: 'Subscription canceled' };
},
async changePlan(userId, newPriceId) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { stripeSubscriptionId: true },
});
const subscription = await stripe.subscriptions.retrieve(
user.stripeSubscriptionId
);
// Update subscription with new price
await stripe.subscriptions.update(user.stripeSubscriptionId, {
items: [{
id: subscription.items.data[0].id,
price: newPriceId,
}],
proration_behavior: 'create_prorations',
});
return { message: 'Plan updated' };
},
async getSubscriptionStatus(userId) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
stripeSubscriptionId: true,
subscriptionStatus: true,
subscriptionPlan: true,
subscriptionPeriodEnd: true,
},
});
return user;
},
};
Subscription API Routes#
javascript
// src/routes/subscriptions.js
router.post('/create', authenticate, async (req, res) => {
const { priceId } = req.body;
const result = await SubscriptionService.createSubscription(req.user.id, priceId);
res.json({ data: result });
});
router.post('/cancel', authenticate, async (req, res) => {
const result = await SubscriptionService.cancelSubscription(req.user.id);
res.json({ data: result });
});
router.post('/change-plan', authenticate, async (req, res) => {
const { priceId } = req.body;
const result = await SubscriptionService.changePlan(req.user.id, priceId);
res.json({ data: result });
});
router.get('/status', authenticate, async (req, res) => {
const status = await SubscriptionService.getSubscriptionStatus(req.user.id);
res.json({ data: status });
});
Webhooks (Critical)#
Handle Stripe events to update your database:
javascript
// src/routes/webhooks/stripe.js
import express from 'express';
import { stripe } from '../../lib/stripe.js';
import { prisma } from '../../lib/prisma.js';
const router = express.Router();
const handlers = {
'checkout.session.completed': async (session) => {
if (session.mode === 'subscription') {
const userId = session.metadata.userId;
const subscription = await stripe.subscriptions.retrieve(session.subscription);
await prisma.user.update({
where: { id: userId },
data: {
stripeSubscriptionId: subscription.id,
subscriptionStatus: subscription.status,
subscriptionPlan: subscription.items.data[0].price.lookup_key,
subscriptionPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
} else {
// One-time payment - create order
const userId = session.metadata.userId;
await OrderService.createFromCheckout(userId, session);
}
},
'invoice.paid': async (invoice) => {
// Subscription renewed successfully
if (invoice.subscription) {
const subscription = await stripe.subscriptions.retrieve(invoice.subscription);
const userId = subscription.metadata.userId;
await prisma.user.update({
where: { id: userId },
data: {
subscriptionStatus: 'active',
subscriptionPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
},
'invoice.payment_failed': async (invoice) => {
if (invoice.subscription) {
const subscription = await stripe.subscriptions.retrieve(invoice.subscription);
const userId = subscription.metadata.userId;
await prisma.user.update({
where: { id: userId },
data: { subscriptionStatus: 'past_due' },
});
// Send email notification
await EmailService.sendPaymentFailed(userId);
}
},
'customer.subscription.updated': async (subscription) => {
const userId = subscription.metadata.userId;
await prisma.user.update({
where: { id: userId },
data: {
subscriptionStatus: subscription.status,
subscriptionPlan: subscription.items.data[0].price.lookup_key,
subscriptionPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
},
});
},
'customer.subscription.deleted': async (subscription) => {
const userId = subscription.metadata.userId;
await prisma.user.update({
where: { id: userId },
data: {
stripeSubscriptionId: null,
subscriptionStatus: 'canceled',
subscriptionPlan: null,
},
});
},
};
router.post('/',
express.raw({ type: 'application/json' }),
async (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) {
console.error('Webhook signature failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
const handler = handlers[event.type];
if (handler) {
try {
await handler(event.data.object);
console.log(`Processed: ${event.type}`);
} catch (err) {
console.error(`Error handling ${event.type}:`, err);
}
}
res.json({ received: true });
}
);
export default router;
Customer Portal#
Let users manage their own subscriptions:
javascript
// src/services/subscriptionService.js
async createPortalSession(userId) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { stripeCustomerId: true },
});
const session = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.APP_URL}/settings/billing`,
});
return { url: session.url };
}
// API route
router.post('/portal', authenticate, async (req, res) => {
const { url } = await SubscriptionService.createPortalSession(req.user.id);
res.json({ url });
});
Products & Prices#
javascript
// Create products and prices programmatically
async function setupProducts() {
// Create product
const product = await stripe.products.create({
name: 'Pro Plan',
description: 'Access to all features',
});
// Create monthly price
const monthlyPrice = await stripe.prices.create({
product: product.id,
unit_amount: 1999, // $19.99
currency: 'usd',
recurring: { interval: 'month' },
lookup_key: 'pro_monthly',
});
// Create yearly price
const yearlyPrice = await stripe.prices.create({
product: product.id,
unit_amount: 19900, // $199.00
currency: 'usd',
recurring: { interval: 'year' },
lookup_key: 'pro_yearly',
});
return { product, monthlyPrice, yearlyPrice };
}
Testing#
bash
# Use Stripe CLI for webhook testing
stripe listen --forward-to localhost:3000/webhooks/stripe
# Test cards
# Success: 4242 4242 4242 4242
# Decline: 4000 0000 0000 0002
# Requires auth: 4000 0025 0000 3155
# Trigger events
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.created
Key Takeaways#
- Use Checkout Sessions - Easiest path to accepting payments
- Handle webhooks - Don't rely on success URLs alone
- Store customer IDs - Link Stripe customers to your users
- Test thoroughly - Use test mode and Stripe CLI
- Provide customer portal - Let users manage subscriptions
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.