Accept recurring payments by creating a Stripe Product and recurring Price, then starting a Checkout Session in subscription mode. Stripe handles billing cycles, payment collection, and failed payment retries automatically. Use the customer_portal for self-service subscription management.
Setting Up Recurring Payments with Stripe Checkout
Stripe subscriptions are built on three concepts: Products (what you sell), Prices (how much and how often), and Subscriptions (the ongoing billing relationship). The fastest way to start is creating a recurring Price in the Dashboard or API, then launching a Checkout Session with mode: 'subscription'. Stripe handles the first payment, ongoing billing, failed payment retries, and cancellation. You listen to webhooks to grant or revoke access.
Prerequisites
- A Stripe account with test API keys
- Node.js 18+ with the stripe package installed
- A product/plan defined (either in Dashboard or via API)
- A webhook endpoint for subscription lifecycle events
Step-by-step guide
Create a Product and recurring Price
Create a Product and recurring Price
Create a Product (what you sell) and a recurring Price (how much per billing cycle). You can do this in the Dashboard or via API.
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);23// Create a product4const product = await stripe.products.create({5 name: 'Pro Plan',6 description: 'Full access to all features',7});89// Create a monthly recurring price10const monthlyPrice = await stripe.prices.create({11 product: product.id,12 unit_amount: 2900, // $29.00/month13 currency: 'usd',14 recurring: {15 interval: 'month',16 },17});1819// Create a yearly recurring price (with discount)20const yearlyPrice = await stripe.prices.create({21 product: product.id,22 unit_amount: 29000, // $290.00/year (2 months free)23 currency: 'usd',24 recurring: {25 interval: 'year',26 },27});2829console.log('Monthly Price ID:', monthlyPrice.id);30console.log('Yearly Price ID:', yearlyPrice.id);Expected result: A Product with two Prices (monthly and yearly) is created. You'll use the Price IDs in Checkout Sessions.
Create a subscription Checkout Session
Create a subscription Checkout Session
Start a Checkout Session with mode: 'subscription' and the recurring Price ID. Stripe collects the first payment and starts the subscription.
1app.post('/create-subscription-session', async (req, res) => {2 const { priceId, customerId } = req.body;34 const session = await stripe.checkout.sessions.create({5 mode: 'subscription',6 customer: customerId || undefined,7 line_items: [8 {9 price: priceId, // e.g., 'price_xxx'10 quantity: 1,11 },12 ],13 success_url: 'https://yoursite.com/subscription-success?session_id={CHECKOUT_SESSION_ID}',14 cancel_url: 'https://yoursite.com/pricing',15 });1617 res.json({ url: session.url });18});Expected result: The Checkout page shows the subscription price with billing interval (e.g., '$29.00/month'). After payment, the subscription starts.
Set up the Customer Portal
Set up the Customer Portal
Enable the Stripe Customer Portal so subscribers can manage their own subscriptions — update payment method, change plan, or cancel. Configure it in Dashboard → Settings → Customer portal.
1// Create a portal session to redirect the customer2app.post('/create-portal-session', async (req, res) => {3 const { customerId } = req.body;45 const portalSession = await stripe.billingPortal.sessions.create({6 customer: customerId,7 return_url: 'https://yoursite.com/account',8 });910 res.json({ url: portalSession.url });11});Expected result: Customers are redirected to a Stripe-hosted portal where they can manage their subscription.
Handle subscription webhooks
Handle subscription webhooks
Listen for key subscription lifecycle events to grant/revoke access and handle billing issues.
1app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {2 const sig = req.headers['stripe-signature'];3 let event;45 try {6 event = stripe.webhooks.constructEvent(7 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET8 );9 } catch (err) {10 return res.status(400).send(`Webhook Error: ${err.message}`);11 }1213 switch (event.type) {14 case 'checkout.session.completed':15 // New subscription started — grant access16 break;17 case 'invoice.paid':18 // Recurring payment succeeded — continue access19 break;20 case 'invoice.payment_failed':21 // Payment failed — notify customer, consider grace period22 break;23 case 'customer.subscription.deleted':24 // Subscription cancelled — revoke access25 break;26 }2728 res.json({ received: true });29});Expected result: Your server processes subscription lifecycle events to manage customer access.
Test the subscription flow
Test the subscription flow
Use test mode and test cards to verify the full subscription lifecycle. Stripe provides special cards to test different scenarios.
1// Successful payment: 4242 4242 4242 42422// Payment requires auth: 4000 0025 0000 31553// Payment will fail: 4000 0000 0000 034145// To test renewal: Stripe Dashboard → Developers → Clocks6// Test clocks let you advance time to trigger renewal billingExpected result: The subscription is created in test mode. Use test clocks to simulate renewal cycles.
Complete working example
1const express = require('express');2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);34const app = express();56// Webhook — before express.json()7app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {8 const sig = req.headers['stripe-signature'];9 let event;10 try {11 event = stripe.webhooks.constructEvent(12 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET13 );14 } catch (err) {15 return res.status(400).send(`Webhook Error: ${err.message}`);16 }1718 switch (event.type) {19 case 'checkout.session.completed': {20 const session = event.data.object;21 console.log('Subscription started:', session.subscription);22 // Grant access to subscriber23 break;24 }25 case 'invoice.paid': {26 const invoice = event.data.object;27 console.log('Recurring payment succeeded:', invoice.subscription);28 break;29 }30 case 'invoice.payment_failed': {31 const invoice = event.data.object;32 console.log('Payment failed for:', invoice.subscription);33 // Email customer to update payment method34 break;35 }36 case 'customer.subscription.deleted': {37 const subscription = event.data.object;38 console.log('Subscription cancelled:', subscription.id);39 // Revoke access40 break;41 }42 }43 res.json({ received: true });44});4546app.use(express.json());4748// Create subscription checkout49app.post('/create-subscription-session', async (req, res) => {50 const { priceId } = req.body;51 const session = await stripe.checkout.sessions.create({52 mode: 'subscription',53 line_items: [{ price: priceId, quantity: 1 }],54 success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,55 cancel_url: `${req.headers.origin}/pricing`,56 });57 res.json({ url: session.url });58});5960// Customer portal61app.post('/create-portal-session', async (req, res) => {62 const { customerId } = req.body;63 const portal = await stripe.billingPortal.sessions.create({64 customer: customerId,65 return_url: `${req.headers.origin}/account`,66 });67 res.json({ url: portal.url });68});6970app.listen(4000, () => console.log('Subscription server on port 4000'));Common mistakes when accepting recurring payments in Stripe
Why it's a problem: Using mode: 'payment' instead of mode: 'subscription'
How to avoid: Checkout mode: 'payment' creates a one-time charge. For recurring billing, use mode: 'subscription' with a recurring Price.
Why it's a problem: Not listening for invoice.payment_failed webhooks
How to avoid: When a renewal payment fails, you need to notify the customer. Listen for invoice.payment_failed and prompt them to update their payment method via the Customer Portal.
Why it's a problem: Granting access based on the success URL redirect
How to avoid: Use the checkout.session.completed webhook to grant access, not the success page redirect. The redirect can fail or be accessed without payment.
Why it's a problem: Using price_data instead of a Price ID for subscriptions
How to avoid: While price_data works for one-time Checkout, subscriptions usually use pre-created Price IDs. Create Prices in the Dashboard and reference them by ID.
Best practices
- Create Products and Prices in the Stripe Dashboard for fixed plans — use API for dynamic pricing
- Use mode: 'subscription' in Checkout Sessions for recurring billing
- Set up the Customer Portal for self-service subscription management
- Listen for invoice.paid, invoice.payment_failed, and customer.subscription.deleted webhooks
- Use Stripe's Smart Retries (enabled by default) to recover failed payments automatically
- Test subscription renewals using Stripe test clocks in Dashboard → Developers → Clocks
- Provide both monthly and yearly pricing options to increase conversion
- Use card 4242 4242 4242 4242 in test mode and test clocks to simulate the full lifecycle
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Write a Node.js Express server that creates a Stripe subscription checkout with a monthly price. Include a webhook endpoint for checkout.session.completed, invoice.paid, invoice.payment_failed, and customer.subscription.deleted. Also add a Customer Portal endpoint.
Add subscription billing to my app. Create a /create-subscription-session endpoint that starts a Stripe Checkout in subscription mode, a /create-portal-session endpoint for customer self-service, and webhook handlers for subscription lifecycle events.
Frequently asked questions
What happens when a recurring payment fails?
Stripe's Smart Retries automatically retries failed payments up to 4 times over several weeks. You receive invoice.payment_failed webhooks for each failure. After all retries are exhausted, the subscription is cancelled.
Can customers change their plan themselves?
Yes, if you enable plan switching in the Customer Portal settings. Customers can upgrade or downgrade, and Stripe handles proration automatically.
How do I offer a free trial?
Add trial_period_days to the Checkout Session subscription_data, or use trial_end with a specific timestamp. See the 'How to Implement Subscription Trials' tutorial.
Can I test subscription renewals without waiting a month?
Yes. Use Stripe test clocks (Dashboard → Developers → Clocks) to simulate time passing and trigger renewal events instantly.
What if I need complex subscription logic?
For metered billing, usage-based pricing, multi-product bundles, or custom trial flows, RapidDev can help architect and implement the subscription system tailored to your business model.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation