Recurring revenue stops being manual once you automate billing. Stripe subscriptions handle the mechanics—charging on a schedule, retrying failed payments, prorating mid-cycle changes, and tracking invoices. You build once, Stripe handles the rest.
Create Products and Pricing Plans
Start by defining what you're selling and how much it costs. Products are what you offer; prices attach amounts and billing frequency to them.
Create a product
In the Stripe Dashboard, go to Catalog > Products and click Add product. Enter a name ("Premium Plan"), optional description, and choose Service for software. Hit save. Stripe assigns a product ID you'll reference in code.
const stripe = require('stripe')('sk_live_...');
const product = await stripe.products.create({
name: 'Premium Plan',
description: 'Unlimited analytics and integrations',
type: 'service',
});Add a monthly price
In Catalog > Prices, click Add price. Select the product, enter the amount in cents (9999 = $99.99), and set Recurring to Monthly. Stripe uses this price object to bill customers on a schedule.
const price = await stripe.prices.create({
product: product.id,
unit_amount: 9999,
currency: 'usd',
recurring: {
interval: 'month',
interval_count: 1,
},
});Create annual pricing for upsell
Add another price for the same product at an annual interval. Customers often prefer annual plans if you discount them 20%. In the Dashboard, add another Recurring price with Yearly selected.
const annualPrice = await stripe.prices.create({
product: product.id,
unit_amount: 99900, // $999/year = ~$83/month
currency: 'usd',
recurring: {
interval: 'year',
interval_count: 1,
},
});Create Customers and Subscriptions
A customer record ties billing to a person. Subscriptions link customers to prices and set the billing schedule.
Create a customer record
In Customers, click Add customer or create via API. Store their email, name, and any internal IDs in metadata. You'll need the customer ID to create a subscription.
const customer = await stripe.customers.create({
email: '[email protected]',
name: 'Jane Doe',
metadata: {
userId: '12345', // Your internal user ID
accountTier: 'pro',
},
});Create a subscription
Link the customer to a price. Use payment_behavior: 'default_incomplete' to handle payment asynchronously—useful for web flows where you confirm payment on the frontend after the customer fills in their card. Expand latest_invoice.payment_intent to check payment status.
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [
{ price: price.id },
],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
});
const paymentIntent = subscription.latest_invoice.payment_intent;
if (paymentIntent?.status === 'requires_action') {
// Return client_secret to frontend for payment confirmation
return { clientSecret: paymentIntent.client_secret };
}Confirm payment on the frontend
Use Stripe.js and confirmCardPayment() to prompt the customer for payment confirmation. Once confirmed, the subscription activates and recurring billing begins.
// Frontend code
const stripe = Stripe('pk_live_...');
const { clientSecret } = await createSubscription();
const { paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement, // Stripe Element
billing_details: { name: 'Jane Doe' },
},
});
if (paymentIntent.status === 'succeeded') {
console.log('Subscription active');
}payment_behavior: 'default_incomplete' means the subscription won't be active until payment succeeds. Don't grant access to the product yet.Manage Subscriptions Over Time
Customers upgrade, downgrade, and cancel. Handle these changes without losing revenue or data.
Upgrade or downgrade a plan
Use the subscription update endpoint. Provide the subscription ID and new price. Stripe prorates by default—if upgrading mid-cycle, the customer is charged the difference immediately. If downgrading, they get a credit toward the next cycle.
const updated = await stripe.subscriptions.update(subscription.id, {
items: [
{
id: subscription.items.data[0].id, // Get current item ID
price: newPriceId,
},
],
proration_behavior: 'create_prorations', // Adjust charges now
});Check upcoming charges
Before a customer renews, see what they'll be charged. Use invoices.retrieveUpcoming() to peek at the next invoice without creating it. This is useful for showing "renewal date" on your UI.
const upcoming = await stripe.invoices.retrieveUpcoming({
customer: customer.id,
});
const renewalDate = new Date(upcoming.next_payment_attempt * 1000);
const amount = (upcoming.amount_due / 100).toFixed(2);
console.log(`Next charge: $${amount} on ${renewalDate.toLocaleDateString()}`);Handle failed payments
Set up webhooks for invoice.payment_failed and invoice.payment_succeeded. Stripe retries failed payments automatically (3x over 3 days by default), but you should email the customer and prompt them to update their card.
const express = require('express');
const app = express();
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const event = JSON.parse(req.body);
if (event.type === 'invoice.payment_failed') {
const invoice = event.data.object;
const customerId = invoice.customer;
console.log(`Payment failed for ${customerId}. Amount: $${invoice.amount_due / 100}`);
// Send email: "Your payment failed. Update your card: ..."
}
if (event.type === 'invoice.payment_succeeded') {
const invoice = event.data.object;
console.log(`Charged ${invoice.customer} $${invoice.amount_paid / 100}`);
// Log revenue, update metrics
}
res.json({ received: true });
});Cancel a subscription
In Customers > [Customer] > Subscriptions, click the subscription and select Cancel subscription. Toggle Refund if canceling mid-cycle to credit the customer for unused time. In code, use the delete endpoint.
const canceled = await stripe.subscriptions.del(subscription.id, {
proration_behavior: 'create_prorations', // Issue refund for unused time
});
console.log(`Subscription ${canceled.id} canceled on ${new Date(canceled.canceled_at * 1000).toLocaleDateString()}`);Common Pitfalls
- Creating subscriptions with
payment_behavior: 'default_incomplete'but not handlingrequires_actionstatus—the subscription sits incomplete and the customer never gets charged. - Forgetting to expand
latest_invoice.payment_intentwhen creating subscriptions—you won't see payment status and can't return the client secret to the frontend. - Not setting up webhooks for billing events—you'll miss failed payments, cancellations, and invoice updates. Polling the API is unreliable.
- Ignoring
proration_behaviorduring updates—Stripe defaults to creating prorations, which charges immediately, but some businesses need to bill on the next cycle instead.
Wrapping Up
You now have a production-grade subscription system: products with tiered pricing, customers you charge reliably, and tools to handle upgrades, cancellations, and failed payments without manual work. Recurring revenue scales as your customer base grows. If you want to track MRR, churn, and cohort retention across Stripe and your other tools, Product Analyst can help you connect and analyze the data.