Accept one-time payments and recurring subscriptions. Handle checkout, webhooks, and customer management.
Stripe is the most developer-friendly payment processor:
pk_test_)sk_test_)STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_SECRET_KEY=sk_test_xxx
Never expose your secret key in frontend code or commit it to git. Only use it on your backend server.
The easiest way to accept payments - Stripe hosts the checkout page.
// Install Stripe
npm install stripe
// routes/checkout.js
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
router.post('/create-checkout-session', async (req, res) => {
const { items } = req.body; // [{ priceId, quantity }]
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: items.map(item => ({
price: item.priceId, // Stripe Price ID
quantity: item.quantity,
})),
mode: 'payment',
success_url: `${process.env.DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.DOMAIN}/cart`,
customer_email: req.user?.email, // Optional: pre-fill email
});
res.json({ url: session.url });
});
// Frontend: redirect to Stripe
async function checkout(cart) {
const response = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: cart }),
});
const { url } = await response.json();
window.location.href = url;
}
Webhooks notify your server when payments complete, subscriptions renew, etc.
// routes/webhooks.js
// IMPORTANT: Use raw body for webhook verification
router.post('/stripe',
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 verification failed');
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
await createOrder(session);
await sendConfirmationEmail(session.customer_email);
break;
case 'invoice.paid':
const invoice = event.data.object;
await extendSubscription(invoice.customer);
break;
case 'customer.subscription.deleted':
await cancelSubscription(event.data.object.customer);
break;
}
res.json({ received: true });
});
Get your webhook secret from Stripe Dashboard → Developers → Webhooks → Add endpoint. Store it as STRIPE_WEBHOOK_SECRET.
Charge customers on a recurring basis (monthly, yearly, etc.).
price_)router.post('/create-subscription', requireAuth, async (req, res) => {
const { priceId } = req.body; // Stripe Price ID
// Get or create Stripe customer
let customerId = req.user.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
email: req.user.email,
metadata: { userId: req.user.id }
});
customerId = customer.id;
await updateUser(req.user.id, { stripeCustomerId: customerId });
}
const session = await stripe.checkout.sessions.create({
customer: customerId,
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity: 1 }],
mode: 'subscription', // Key difference from one-time payment
success_url: `${process.env.DOMAIN}/dashboard?subscribed=true`,
cancel_url: `${process.env.DOMAIN}/pricing`,
});
res.json({ url: session.url });
});
Let customers manage their subscriptions, update payment methods, and view invoices.
router.post('/create-portal-session', requireAuth, async (req, res) => {
const customerId = req.user.stripeCustomerId;
if (!customerId) {
return res.status(400).json({ error: 'No subscription found' });
}
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.DOMAIN}/settings`,
});
res.json({ url: session.url });
});
// Frontend
async function openPortal() {
const response = await fetch('/api/create-portal-session', { method: 'POST' });
const { url } = await response.json();
window.location.href = url;
}
Stripe provides test card numbers for development.
| Card Number | Result |
|---|---|
| 4242 4242 4242 4242 | Successful payment |
| 4000 0000 0000 0002 | Card declined |
| 4000 0000 0000 3220 | Requires 3D Secure |
Use any future expiration date (e.g., 12/34) and any 3-digit CVC.
Use the Stripe CLI to forward webhooks to your local server: stripe listen --forward-to localhost:3000/api/webhooks/stripe