Stripe has become the go-to payment processor for modern e-commerce platforms. Its robust API, excellent documentation, and powerful features make it ideal for businesses of all sizes. This guide will take you through everything you need to know to integrate Stripe into your e-commerce platform.
Getting Started with Stripe
Account Setup
First, create a Stripe account and obtain your API keys:
- 1Sign up at stripe.com
- 2Navigate to the Developers section
- 3Get your publishable key (for frontend) and secret key (for backend)
- 4Use test mode for development
Install Stripe SDK
npm install stripe @stripe/stripe-jsBackend Integration
Initialize Stripe
// lib/stripe.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
typescript: true,
});
export default stripe;Create Payment Intent
Payment Intents are the modern way to handle payments in Stripe:
// api/create-payment-intent.ts
import stripe from '@/lib/stripe';
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { items, currency = 'usd' } = req.body;
// Calculate amount
const amount = items.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
// Create payment intent
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(amount * 100), // Convert to cents
currency,
automatic_payment_methods: {
enabled: true,
},
metadata: {
order_id: generateOrderId(),
items: JSON.stringify(items.map(i => ({ id: i.id, qty: i.quantity }))),
},
});
res.json({
clientSecret: paymentIntent.client_secret,
paymentIntentId: paymentIntent.id,
});
} catch (error) {
console.error('Payment intent error:', error);
res.status(500).json({ error: 'Payment processing failed' });
}
}Frontend Integration
Stripe Elements
Stripe Elements are pre-built UI components that handle card input securely:
// components/CheckoutForm.tsx
import { loadStripe } from '@stripe/stripe-js';
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { useState } from 'react';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
export const CheckoutForm = ({ amount, onSuccess }) => {
const stripe = useStripe();
const elements = useElements();
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (event) => {
event.preventDefault();
if (!stripe || !elements) {
return;
}
setProcessing(true);
setError(null);
const cardElement = elements.getElement(CardElement);
const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement!,
billing_details: {
name: 'Customer Name',
email: 'customer@example.com',
},
},
});
if (error) {
setError(error.message || 'Payment failed');
setProcessing(false);
} else if (paymentIntent?.status === 'succeeded') {
onSuccess(paymentIntent);
setProcessing(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="p-4 border rounded-lg">
<CardElement
options={{
style: {
base: {
fontSize: '16px',
color: '#424770',
'::placeholder': {
color: '#aab7c4',
},
},
},
}}
/>
</div>
{error && (
<div className="text-red-600 text-sm">{error}</div>
)}
<button
type="submit"
disabled={!stripe || processing}
className="w-full bg-blue-600 text-white py-3 rounded-lg disabled:bg-gray-400"
>
{processing ? 'Processing...' : `Pay $${amount}`}
</button>
</form>
);
};
export const StripeWrapper = ({ amount, onSuccess }) => {
const [clientSecret, setClientSecret] = useState('');
useEffect(() => {
// Fetch payment intent
fetch('/api/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: [{ price: amount, quantity: 1 }] }),
})
.then(res => res.json())
.then(data => setClientSecret(data.clientSecret));
}, [amount]);
if (!clientSecret) {
return <div>Loading...</div>;
}
return (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm amount={amount} onSuccess={onSuccess} />
</Elements>
);
};Handling Webhooks
Webhooks are crucial for handling asynchronous events from Stripe:
// api/webhooks/stripe.ts
import stripe from '@/lib/stripe';
import { buffer } from 'micro';
export const config = {
api: {
bodyParser: false,
},
};
export default async function handler(req, res) {
const buf = await buffer(req);
const sig = req.headers['stripe-signature']!;
let event;
try {
event = stripe.webhooks.constructEvent(
buf,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.log('Webhook signature verification failed.', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
await handleSuccessfulPayment(paymentIntent);
break;
case 'payment_intent.payment_failed':
const failedPayment = event.data.object;
await handleFailedPayment(failedPayment);
break;
case 'charge.refunded':
const charge = event.data.object;
await handleRefund(charge);
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
res.json({ received: true });
}
async function handleSuccessfulPayment(paymentIntent) {
const orderId = paymentIntent.metadata.order_id;
// Update order status
await updateOrderStatus(orderId, 'paid');
// Send confirmation email
await sendOrderConfirmation(orderId);
// Clear cart
await clearUserCart(paymentIntent.metadata.user_id);
console.log(`Payment succeeded for order ${orderId}`);
}
async function handleFailedPayment(paymentIntent) {
const orderId = paymentIntent.metadata.order_id;
// Update order status
await updateOrderStatus(orderId, 'payment_failed');
// Notify customer
await notifyPaymentFailed(orderId);
console.log(`Payment failed for order ${orderId}`);
}Advanced Features
Subscriptions
For recurring payments, use Stripe Subscriptions:
// Create subscription
export async function createSubscription(
customerId: string,
priceId: string
) {
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
});
return subscription;
}
// Create customer and subscription
export async function createCustomerAndSubscribe(
email: string,
paymentMethodId: string,
priceId: string
) {
const customer = await stripe.customers.create({
email,
payment_method: paymentMethodId,
invoice_settings: {
default_payment_method: paymentMethodId,
},
});
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: priceId }],
expand: ['latest_invoice.payment_intent'],
});
return { customer, subscription };
}Saving Payment Methods
Allow customers to save cards for future purchases:
// Setup intent for saving card
export async function createSetupIntent(customerId: string) {
return await stripe.setupIntents.create({
customer: customerId,
});
}
// Attach payment method to customer
export async function attachPaymentMethod(
paymentMethodId: string,
customerId: string
) {
return await stripe.paymentMethods.attach(paymentMethodId, {
customer: customerId,
});
}
// List customer payment methods
export async function listPaymentMethods(customerId: string) {
return await stripe.paymentMethods.list({
customer: customerId,
type: 'card',
});
}Best Practices
1. Never Trust the Client
Always verify amounts and items on the server:
// ❌ Bad: Trust client data
const amount = req.body.amount;
// ✅ Good: Verify on server
const items = await getOrderItems(req.body.order_id);
const amount = calculateOrderTotal(items);2. Handle Idempotency
Prevent duplicate charges with idempotency keys:
const paymentIntent = await stripe.paymentIntents.create(
{ amount: 2000, currency: 'usd' },
{ idempotencyKey: `order-${orderId}` }
);3. Proper Error Handling
try {
const paymentIntent = await stripe.paymentIntents.create(params);
} catch (error) {
if (error.type === 'StripeCardError') {
// Card declined
console.log(error.message);
} else if (error.type === 'StripeInvalidRequestError') {
// Invalid parameters
console.log('Invalid request:', error.message);
} else {
// Other error
console.log('Unexpected error:', error);
}
}4. Test Thoroughly
Stripe provides test card numbers for various scenarios:
- Success: 4242 4242 4242 4242
- Decline: 4000 0000 0000 0002
- Insufficient Funds: 4000 0000 0000 9995
- Expired Card: 4000 0000 0000 0069
Conclusion
Stripe provides a powerful, developer-friendly platform for payment processing. By following this guide and implementing the patterns shown, you'll have a robust payment integration that handles edge cases, provides excellent user experience, and scales with your business.
Remember to always prioritize security, test thoroughly, and stay updated with Stripe's latest features and best practices. Your payment integration is the final and most critical step in the e-commerce journey—get it right, and your customers will thank you with their loyalty.