Create a subscription programmatically by first creating a Customer and attaching a payment method, then calling stripe.subscriptions.create() with the customer ID and a recurring price ID. This gives you full control over the subscription flow compared to Checkout Sessions. Use webhooks to track the subscription lifecycle.
Creating Subscriptions Programmatically with the Stripe API
While Stripe Checkout handles subscriptions with minimal code, creating subscriptions via the API gives you full control over the customer experience. You control the UI, the timing, and the flow. The process is: create a Customer, collect and attach a payment method (via SetupIntent or PaymentIntent), then call stripe.subscriptions.create() with the customer and price. The subscription immediately bills the customer and begins the recurring cycle.
Prerequisites
- Node.js 18+ with the stripe npm package installed
- A recurring Price ID created in the Dashboard or via API
- A payment collection form using Stripe Elements (to get the payment method)
- Understanding of PaymentIntents and SetupIntents
Step-by-step guide
Create a Customer
Create a Customer
Start by creating a Stripe Customer. Attach the customer to your internal user record so you can look them up later.
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);23const customer = await stripe.customers.create({4 email: 'subscriber@example.com',5 name: 'Jane Doe',6 metadata: {7 internal_user_id: 'user_123',8 },9});1011console.log('Customer ID:', customer.id); // cus_xxxExpected result: A Stripe Customer object is created. Save the customer.id (cus_xxx) in your database.
Collect a payment method via SetupIntent
Collect a payment method via SetupIntent
Create a SetupIntent to securely collect the customer's payment method without charging them. This uses Stripe Elements on the frontend.
1// Server: Create SetupIntent2app.post('/create-setup-intent', async (req, res) => {3 const setupIntent = await stripe.setupIntents.create({4 customer: req.body.customerId,5 automatic_payment_methods: { enabled: true },6 });7 res.json({ clientSecret: setupIntent.client_secret });8});910// Frontend: Confirm the SetupIntent11// (After mounting PaymentElement)12const { error, setupIntent } = await stripe.confirmSetup({13 elements,14 redirect: 'if_required',15});1617if (setupIntent.status === 'succeeded') {18 // Payment method is saved to the customer19 console.log('Payment method:', setupIntent.payment_method);20}Expected result: The customer's payment method is saved to their Stripe Customer profile without any charge.
Set the default payment method
Set the default payment method
After the SetupIntent succeeds, set the payment method as the customer's default for invoices. This ensures subscriptions can bill automatically.
1await stripe.customers.update(customer.id, {2 invoice_settings: {3 default_payment_method: setupIntent.payment_method,4 },5});Expected result: The payment method is set as the default for future invoices and subscriptions.
Create the subscription
Create the subscription
Call stripe.subscriptions.create() with the customer ID and the recurring price ID. Stripe immediately creates the first invoice and attempts to charge the customer.
1const subscription = await stripe.subscriptions.create({2 customer: customer.id,3 items: [4 { price: 'price_xxx' }, // Your recurring Price ID5 ],6 payment_behavior: 'default_incomplete',7 expand: ['latest_invoice.payment_intent'],8});910console.log('Subscription ID:', subscription.id);11console.log('Status:', subscription.status); // 'incomplete' → 'active' after paymentExpected result: The subscription is created. If the default payment method is valid, it transitions to 'active' after the first invoice is paid.
Handle subscription statuses
Handle subscription statuses
Subscriptions transition through several statuses. Listen for webhooks to update your app accordingly.
1// Subscription statuses:2// 'incomplete' — first payment pending3// 'incomplete_expired' — first payment failed after 23 hours4// 'trialing' — in trial period5// 'active' — payment succeeded, subscription active6// 'past_due' — renewal payment failed, retrying7// 'canceled' — subscription ended8// 'unpaid' — all retries exhausted910// Webhook handler for status changes:11case 'customer.subscription.updated':12 const sub = event.data.object;13 if (sub.status === 'active') {14 grantAccess(sub.customer);15 } else if (sub.status === 'past_due') {16 notifyPaymentFailed(sub.customer);17 }18 break;Expected result: Your app responds to each subscription status change appropriately.
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 'customer.subscription.created':20 console.log('Subscription created:', event.data.object.id);21 break;22 case 'customer.subscription.updated':23 console.log('Subscription updated:', event.data.object.status);24 break;25 case 'customer.subscription.deleted':26 console.log('Subscription cancelled:', event.data.object.id);27 break;28 case 'invoice.paid':29 console.log('Invoice paid:', event.data.object.subscription);30 break;31 case 'invoice.payment_failed':32 console.log('Payment failed:', event.data.object.subscription);33 break;34 }35 res.json({ received: true });36});3738app.use(express.json());3940// Create customer41app.post('/create-customer', async (req, res) => {42 const customer = await stripe.customers.create({43 email: req.body.email,44 metadata: { user_id: req.body.userId },45 });46 res.json({ customerId: customer.id });47});4849// Setup intent for collecting payment method50app.post('/create-setup-intent', async (req, res) => {51 const setupIntent = await stripe.setupIntents.create({52 customer: req.body.customerId,53 automatic_payment_methods: { enabled: true },54 });55 res.json({ clientSecret: setupIntent.client_secret });56});5758// Create subscription59app.post('/create-subscription', async (req, res) => {60 const { customerId, priceId, paymentMethodId } = req.body;6162 // Set default payment method63 await stripe.customers.update(customerId, {64 invoice_settings: { default_payment_method: paymentMethodId },65 });6667 const subscription = await stripe.subscriptions.create({68 customer: customerId,69 items: [{ price: priceId }],70 expand: ['latest_invoice.payment_intent'],71 });7273 res.json({74 subscriptionId: subscription.id,75 status: subscription.status,76 clientSecret: subscription.latest_invoice?.payment_intent?.client_secret,77 });78});7980app.listen(4000, () => console.log('Server on port 4000'));Common mistakes when creating a subscription with Stripe API
Why it's a problem: Not setting a default payment method before creating the subscription
How to avoid: Without a default payment method, Stripe cannot charge the customer. Either set invoice_settings.default_payment_method on the customer or pass default_payment_method in the subscription creation.
Why it's a problem: Ignoring the 'incomplete' status
How to avoid: A subscription starts as 'incomplete' when payment_behavior is 'default_incomplete'. You must confirm the payment (e.g., handle 3D Secure) for it to become 'active'.
Why it's a problem: Not handling 'past_due' subscriptions
How to avoid: When a renewal payment fails, the subscription goes to 'past_due'. Notify the customer and direct them to the Customer Portal to update their payment method.
Why it's a problem: Confusing SetupIntents with PaymentIntents for subscriptions
How to avoid: Use a SetupIntent to save a payment method without charging. The subscription itself creates a PaymentIntent for the first charge. You can also use payment_behavior: 'default_incomplete' and confirm the subscription's PaymentIntent.
Best practices
- Store the Stripe Customer ID in your database alongside your user record
- Use SetupIntents to save payment methods before creating subscriptions
- Set the default payment method on the customer before subscription creation
- Listen for customer.subscription.updated to track status changes
- Handle all subscription statuses: active, past_due, canceled, incomplete, unpaid
- Use Stripe's Smart Retries for failed payments (enabled by default)
- Test with card 4242 4242 4242 4242 and use test clocks for renewal testing
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Write a Node.js Express server that creates Stripe subscriptions programmatically. Include endpoints for: creating a customer, creating a SetupIntent, setting the default payment method, and creating a subscription with a price ID. Add webhook handling for subscription lifecycle events.
Build a subscription system for my app. Create endpoints to: 1) Create a Stripe Customer from a user email. 2) Create a SetupIntent so the user can save their card. 3) Create a subscription with a given price ID. 4) Handle webhooks for subscription status changes.
Frequently asked questions
When should I use API subscriptions vs Checkout subscriptions?
Use Checkout Sessions for a quick, hosted subscription flow. Use the API when you need a fully custom UI, multi-step signup, or want to separate payment method collection from subscription creation.
What is payment_behavior: 'default_incomplete'?
It creates the subscription without requiring immediate payment success. The subscription starts as 'incomplete' until the first invoice's PaymentIntent is confirmed. This lets you handle 3D Secure on the frontend.
Can I create a subscription without charging immediately?
Yes. Use trial_period_days or trial_end to start with a free trial. Stripe creates the subscription but delays the first charge until the trial ends.
How do I handle SCA/3D Secure for subscriptions?
When using payment_behavior: 'default_incomplete', the subscription's first invoice has a PaymentIntent. Expand it with expand: ['latest_invoice.payment_intent'] and confirm it on the frontend using stripe.confirmPayment().
Can RapidDev help build a subscription management system?
Yes. RapidDev builds complete subscription systems including plan selection, payment method management, upgrade/downgrade flows, dunning (failed payment recovery), and admin dashboards for monitoring subscription metrics.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation