Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to accept recurring payments in Stripe

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.

What you'll learn

  • How to create Products and recurring Prices in Stripe
  • How to start a subscription via Checkout Session
  • How to set up the Customer Portal for self-service management
  • Key subscription webhooks to listen for
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner6 min read20 minutesStripe API v2024-12+, Node.js 18+March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3// Create a product
4const product = await stripe.products.create({
5 name: 'Pro Plan',
6 description: 'Full access to all features',
7});
8
9// Create a monthly recurring price
10const monthlyPrice = await stripe.prices.create({
11 product: product.id,
12 unit_amount: 2900, // $29.00/month
13 currency: 'usd',
14 recurring: {
15 interval: 'month',
16 },
17});
18
19// 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});
28
29console.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.

2

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.

typescript
1app.post('/create-subscription-session', async (req, res) => {
2 const { priceId, customerId } = req.body;
3
4 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 });
16
17 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.

3

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.

typescript
1// Create a portal session to redirect the customer
2app.post('/create-portal-session', async (req, res) => {
3 const { customerId } = req.body;
4
5 const portalSession = await stripe.billingPortal.sessions.create({
6 customer: customerId,
7 return_url: 'https://yoursite.com/account',
8 });
9
10 res.json({ url: portalSession.url });
11});

Expected result: Customers are redirected to a Stripe-hosted portal where they can manage their subscription.

4

Handle subscription webhooks

Listen for key subscription lifecycle events to grant/revoke access and handle billing issues.

typescript
1app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
2 const sig = req.headers['stripe-signature'];
3 let event;
4
5 try {
6 event = stripe.webhooks.constructEvent(
7 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
8 );
9 } catch (err) {
10 return res.status(400).send(`Webhook Error: ${err.message}`);
11 }
12
13 switch (event.type) {
14 case 'checkout.session.completed':
15 // New subscription started — grant access
16 break;
17 case 'invoice.paid':
18 // Recurring payment succeeded — continue access
19 break;
20 case 'invoice.payment_failed':
21 // Payment failed — notify customer, consider grace period
22 break;
23 case 'customer.subscription.deleted':
24 // Subscription cancelled — revoke access
25 break;
26 }
27
28 res.json({ received: true });
29});

Expected result: Your server processes subscription lifecycle events to manage customer access.

5

Test the subscription flow

Use test mode and test cards to verify the full subscription lifecycle. Stripe provides special cards to test different scenarios.

typescript
1// Successful payment: 4242 4242 4242 4242
2// Payment requires auth: 4000 0025 0000 3155
3// Payment will fail: 4000 0000 0000 0341
4
5// To test renewal: Stripe Dashboard → Developers → Clocks
6// Test clocks let you advance time to trigger renewal billing

Expected result: The subscription is created in test mode. Use test clocks to simulate renewal cycles.

Complete working example

subscription-server.js
1const express = require('express');
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4const app = express();
5
6// 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_SECRET
13 );
14 } catch (err) {
15 return res.status(400).send(`Webhook Error: ${err.message}`);
16 }
17
18 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 subscriber
23 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 method
34 break;
35 }
36 case 'customer.subscription.deleted': {
37 const subscription = event.data.object;
38 console.log('Subscription cancelled:', subscription.id);
39 // Revoke access
40 break;
41 }
42 }
43 res.json({ received: true });
44});
45
46app.use(express.json());
47
48// Create subscription checkout
49app.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});
59
60// Customer portal
61app.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});
69
70app.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.

ChatGPT Prompt

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.

Stripe Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.