# How to accept recurring payments in Stripe

- Tool: Stripe
- Difficulty: Beginner
- Time required: 20 minutes
- Compatibility: Stripe API v2024-12+, Node.js 18+
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// Create a product
const product = await stripe.products.create({
  name: 'Pro Plan',
  description: 'Full access to all features',
});

// Create a monthly recurring price
const monthlyPrice = await stripe.prices.create({
  product: product.id,
  unit_amount: 2900, // $29.00/month
  currency: 'usd',
  recurring: {
    interval: 'month',
  },
});

// Create a yearly recurring price (with discount)
const yearlyPrice = await stripe.prices.create({
  product: product.id,
  unit_amount: 29000, // $290.00/year (2 months free)
  currency: 'usd',
  recurring: {
    interval: 'year',
  },
});

console.log('Monthly Price ID:', monthlyPrice.id);
console.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.

```
app.post('/create-subscription-session', async (req, res) => {
  const { priceId, customerId } = req.body;

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    customer: customerId || undefined,
    line_items: [
      {
        price: priceId, // e.g., 'price_xxx'
        quantity: 1,
      },
    ],
    success_url: 'https://yoursite.com/subscription-success?session_id={CHECKOUT_SESSION_ID}',
    cancel_url: 'https://yoursite.com/pricing',
  });

  res.json({ url: session.url });
});
```

**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.

```
// Create a portal session to redirect the customer
app.post('/create-portal-session', async (req, res) => {
  const { customerId } = req.body;

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: 'https://yoursite.com/account',
  });

  res.json({ url: portalSession.url });
});
```

**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.

```
app.post('/webhook', express.raw({ type: 'application/json' }), (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) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  switch (event.type) {
    case 'checkout.session.completed':
      // New subscription started — grant access
      break;
    case 'invoice.paid':
      // Recurring payment succeeded — continue access
      break;
    case 'invoice.payment_failed':
      // Payment failed — notify customer, consider grace period
      break;
    case 'customer.subscription.deleted':
      // Subscription cancelled — revoke access
      break;
  }

  res.json({ received: true });
});
```

**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.

```
// Successful payment: 4242 4242 4242 4242
// Payment requires auth: 4000 0025 0000 3155
// Payment will fail: 4000 0000 0000 0341

// To test renewal: Stripe Dashboard → Developers → Clocks
// 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 code example

File: `subscription-server.js`

```javascript
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express();

// Webhook — before express.json()
app.post('/webhook', express.raw({ type: 'application/json' }), (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) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object;
      console.log('Subscription started:', session.subscription);
      // Grant access to subscriber
      break;
    }
    case 'invoice.paid': {
      const invoice = event.data.object;
      console.log('Recurring payment succeeded:', invoice.subscription);
      break;
    }
    case 'invoice.payment_failed': {
      const invoice = event.data.object;
      console.log('Payment failed for:', invoice.subscription);
      // Email customer to update payment method
      break;
    }
    case 'customer.subscription.deleted': {
      const subscription = event.data.object;
      console.log('Subscription cancelled:', subscription.id);
      // Revoke access
      break;
    }
  }
  res.json({ received: true });
});

app.use(express.json());

// Create subscription checkout
app.post('/create-subscription-session', async (req, res) => {
  const { priceId } = req.body;
  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${req.headers.origin}/pricing`,
  });
  res.json({ url: session.url });
});

// Customer portal
app.post('/create-portal-session', async (req, res) => {
  const { customerId } = req.body;
  const portal = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${req.headers.origin}/account`,
  });
  res.json({ url: portal.url });
});

app.listen(4000, () => console.log('Subscription server on port 4000'));
```

## Common mistakes

- **Using mode: 'payment' instead of mode: 'subscription'** — undefined Fix: Checkout mode: 'payment' creates a one-time charge. For recurring billing, use mode: 'subscription' with a recurring Price.
- **Not listening for invoice.payment_failed webhooks** — undefined Fix: 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.
- **Granting access based on the success URL redirect** — undefined Fix: Use the checkout.session.completed webhook to grant access, not the success page redirect. The redirect can fail or be accessed without payment.
- **Using price_data instead of a Price ID for subscriptions** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-accept-recurring-payments-in-stripe
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-accept-recurring-payments-in-stripe
