Stripe has become the de facto standard for online payments, offering a comprehensive platform that handles everything from simple one-time payments to complex subscription billing. This guide walks you through implementing Stripe payments in your web application, covering best practices, security considerations, and real-world code examples.
Why Choose Stripe?
- Developer-Friendly: Excellent documentation and APIs
- Global Support: Accepts payments from 195+ countries
- Security: PCI compliance handled for you
- Flexible: One-time payments, subscriptions, marketplace features
- No Monthly Fees: Pay only per transaction (2.9% + 30¢)
- Built-in Features: Fraud detection, billing portal, tax calculation
Getting Started: Initial Setup
1. Create a Stripe Account
Sign up at stripe.com and get your API keys. You'll have test keys for development and live keys for production.
2. Install Stripe SDK
# For Node.js backend
npm install stripe
# For frontend (Stripe.js)
npm install @stripe/stripe-js
# For React (optional)
npm install @stripe/react-stripe-js
3. Initialize Stripe
// Backend (Node.js)
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2023-10-16',
});
// Frontend
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
Security Note
Never expose your secret key! Use environment variables and only use the publishable key on the frontend. Your secret key should only exist on your backend server.
Implementing One-Time Payments
Payment Flow Overview
- Customer enters payment details on your frontend
- Frontend creates a PaymentIntent on your backend
- Backend returns client secret to frontend
- Frontend confirms payment with Stripe
- Webhook notifies your backend of payment success
Backend: Create Payment Intent
// pages/api/create-payment-intent.js
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { amount, currency = 'usd', metadata = {} } = req.body;
// Create a PaymentIntent
const paymentIntent = await stripe.paymentIntents.create({
amount: amount * 100, // Convert to cents
currency,
metadata, // Store custom data
automatic_payment_methods: {
enabled: true,
},
});
res.status(200).json({
clientSecret: paymentIntent.client_secret,
});
} catch (error) {
console.error('Payment Intent Error:', error);
res.status(500).json({ error: error.message });
}
}
Frontend: Payment Form with Stripe Elements
// components/CheckoutForm.jsx
import React, { useState } from 'react';
import {
PaymentElement,
useStripe,
useElements
} from '@stripe/react-stripe-js';
export default function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState(null);
const [processing, setProcessing] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
if (!stripe || !elements) {
return;
}
setProcessing(true);
// Confirm the payment
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/payment-success`,
},
});
if (error) {
setError(error.message);
setProcessing(false);
}
// If successful, user will be redirected to return_url
};
return (
);
}
Complete Checkout Page
// pages/checkout.jsx
import { useEffect, useState } from 'react';
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
import CheckoutForm from '../components/CheckoutForm';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
export default function CheckoutPage() {
const [clientSecret, setClientSecret] = useState('');
useEffect(() => {
// Create PaymentIntent on mount
fetch('/api/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: 50, // $50.00
metadata: {
orderId: '12345',
userId: 'user_abc',
},
}),
})
.then((res) => res.json())
.then((data) => setClientSecret(data.clientSecret));
}, []);
return (
Checkout
{clientSecret && (
)}
);
}
Implementing Subscriptions
Subscription Flow
- Create products and prices in Stripe Dashboard
- Create a customer in Stripe
- Create a subscription with payment method
- Listen to subscription events via webhooks
Backend: Create Subscription
// pages/api/create-subscription.js
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { email, priceId, paymentMethodId } = req.body;
// 1. Create or retrieve customer
const customer = await stripe.customers.create({
email,
payment_method: paymentMethodId,
invoice_settings: {
default_payment_method: paymentMethodId,
},
});
// 2. Create subscription
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: priceId }],
payment_settings: {
payment_method_types: ['card'],
save_default_payment_method: 'on_subscription',
},
expand: ['latest_invoice.payment_intent'],
});
res.status(200).json({
subscriptionId: subscription.id,
clientSecret: subscription.latest_invoice.payment_intent.client_secret,
});
} catch (error) {
console.error('Subscription Error:', error);
res.status(500).json({ error: error.message });
}
}
Managing Subscription Tiers
// pages/api/change-subscription.js
export default async function handler(req, res) {
const { subscriptionId, newPriceId } = req.body;
try {
// Get current subscription
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
// Update to new price
const updatedSubscription = await stripe.subscriptions.update(
subscriptionId,
{
items: [{
id: subscription.items.data[0].id,
price: newPriceId,
}],
proration_behavior: 'create_prorations', // Pro-rate the difference
}
);
res.status(200).json(updatedSubscription);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
Cancel Subscription
// pages/api/cancel-subscription.js
export default async function handler(req, res) {
const { subscriptionId, cancelAtPeriodEnd = true } = req.body;
try {
const subscription = await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: cancelAtPeriodEnd,
});
// Or cancel immediately:
// const subscription = await stripe.subscriptions.cancel(subscriptionId);
res.status(200).json(subscription);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
Webhook Implementation
Webhooks are critical for receiving real-time updates about payments, subscriptions, and disputes. Never rely solely on client-side success notifications.
Setting Up Webhooks
// pages/api/webhooks/stripe.js
import Stripe from 'stripe';
import { buffer } from 'micro';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
// Disable body parsing for webhooks
export const config = {
api: {
bodyParser: false,
},
};
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const buf = await buffer(req);
const sig = req.headers['stripe-signature'];
let event;
try {
// Verify webhook signature
event = stripe.webhooks.constructEvent(buf, sig, webhookSecret);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle different event types
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
case 'payment_intent.payment_failed':
await handlePaymentFailure(event.data.object);
break;
case 'customer.subscription.created':
await handleSubscriptionCreated(event.data.object);
break;
case 'customer.subscription.updated':
await handleSubscriptionUpdated(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(event.data.object);
break;
case 'invoice.payment_succeeded':
await handleInvoicePayment(event.data.object);
break;
case 'invoice.payment_failed':
await handleInvoiceFailure(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.status(200).json({ received: true });
}
async function handlePaymentSuccess(paymentIntent) {
console.log('Payment succeeded:', paymentIntent.id);
// Update your database
// Send confirmation email
// Fulfill the order
}
async function handleSubscriptionCreated(subscription) {
console.log('Subscription created:', subscription.id);
// Update user's subscription status in database
// Grant access to premium features
// Send welcome email
}
async function handleInvoicePayment(invoice) {
console.log('Invoice paid:', invoice.id);
// Extend subscription period
// Send receipt email
}
Webhook Testing
Use the Stripe CLI to test webhooks locally:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Security Best Practices
1. Never Trust the Client
Always verify payment status on your backend using webhooks. Clients can manipulate frontend code.
// DON'T DO THIS
// Frontend (insecure)
const handleSuccess = () => {
// Client could fake this!
grantPremiumAccess();
};
// DO THIS INSTEAD
// Backend webhook (secure)
async function handlePaymentSuccess(paymentIntent) {
// Verify payment in webhook
await grantPremiumAccess(paymentIntent.metadata.userId);
}
2. Use Idempotency Keys
// Prevent duplicate charges
const paymentIntent = await stripe.paymentIntents.create({
amount: 5000,
currency: 'usd',
}, {
idempotencyKey: `order_${orderId}`,
});
3. Validate Webhook Signatures
Always verify that webhook events came from Stripe using the signature verification shown earlier.
4. Store Minimal Card Data
Never store card numbers or CVV codes. Use Stripe's tokens and customer IDs instead.
5. Implement Rate Limiting
// Example with express-rate-limit
import rateLimit from 'express-rate-limit';
const paymentLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 requests per window
message: 'Too many payment attempts, please try again later',
});
app.post('/api/create-payment-intent', paymentLimiter, async (req, res) => {
// Handle payment
});
Handling Edge Cases
Failed Payments
// Retry failed payments with exponential backoff
async function retryFailedPayment(paymentIntentId) {
try {
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
if (paymentIntent.status === 'requires_payment_method') {
// Notify customer to update payment method
await sendPaymentFailureEmail(paymentIntent.customer);
}
} catch (error) {
console.error('Retry failed:', error);
}
}
Disputed Charges
// Handle disputes via webhook
case 'charge.dispute.created':
const dispute = event.data.object;
// Gather evidence
await stripe.disputes.update(dispute.id, {
evidence: {
customer_name: 'John Doe',
customer_signature: 'https://example.com/signature.png',
receipt: 'https://example.com/receipt.pdf',
},
});
break;
Refunds
// pages/api/refund.js
export default async function handler(req, res) {
const { paymentIntentId, amount, reason } = req.body;
try {
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
amount, // Optional: partial refund
reason, // 'duplicate', 'fraudulent', or 'requested_by_customer'
});
res.status(200).json(refund);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
Testing Your Integration
Test Card Numbers
- Success: 4242 4242 4242 4242
- Decline: 4000 0000 0000 0002
- 3D Secure: 4000 0027 6000 3184
- Insufficient Funds: 4000 0000 0000 9995
Use any future expiration date and any 3-digit CVC.
Testing Checklist
- Test successful payments
- Test declined cards
- Test subscription creation and cancellation
- Test webhook delivery
- Test refunds
- Test error handling
- Test with different currencies
Going Live
Pre-Launch Checklist
- Complete Stripe account verification
- Switch from test keys to live keys
- Configure live webhook endpoints
- Test with real cards (small amounts)
- Set up monitoring and alerts
- Review PCI compliance requirements
- Configure tax settings if needed
- Set up customer support for payment issues
Monitoring and Optimization
- Stripe Dashboard: Monitor payments, failed charges, and disputes
- Radar: Use Stripe Radar for fraud detection
- Analytics: Track conversion rates and payment success rates
- Logs: Monitor webhook delivery and errors
- Alerts: Set up email notifications for failed payments
Conclusion
Implementing Stripe payments doesn't have to be complicated. By following this guide and security best practices, you can create a robust payment system that handles one-time payments, subscriptions, and edge cases gracefully.
Remember to always verify payments on the backend using webhooks, never trust client-side data, and test thoroughly before going live. Stripe's excellent documentation and developer tools make it easier than ever to build production-ready payment systems.
Need Help Implementing Stripe Payments?
Yonda Solutions specializes in integrating payment systems into web and mobile applications. We can help you implement Stripe payments securely and efficiently, from basic checkout flows to complex subscription billing. Contact us today for a free consultation.