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

How to create a subscription with Stripe API

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.

What you'll learn

  • How to create a Customer and attach a payment method
  • How to create a subscription via the Stripe API
  • How to handle the initial payment and status transitions
  • How to manage subscription statuses and lifecycle events
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read25 minutesStripe API v2024-12+, Node.js 18+March 2026RapidDev Engineering Team
TL;DR

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

1

Create a Customer

Start by creating a Stripe Customer. Attach the customer to your internal user record so you can look them up later.

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3const customer = await stripe.customers.create({
4 email: 'subscriber@example.com',
5 name: 'Jane Doe',
6 metadata: {
7 internal_user_id: 'user_123',
8 },
9});
10
11console.log('Customer ID:', customer.id); // cus_xxx

Expected result: A Stripe Customer object is created. Save the customer.id (cus_xxx) in your database.

2

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.

typescript
1// Server: Create SetupIntent
2app.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});
9
10// Frontend: Confirm the SetupIntent
11// (After mounting PaymentElement)
12const { error, setupIntent } = await stripe.confirmSetup({
13 elements,
14 redirect: 'if_required',
15});
16
17if (setupIntent.status === 'succeeded') {
18 // Payment method is saved to the customer
19 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.

3

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.

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

4

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.

typescript
1const subscription = await stripe.subscriptions.create({
2 customer: customer.id,
3 items: [
4 { price: 'price_xxx' }, // Your recurring Price ID
5 ],
6 payment_behavior: 'default_incomplete',
7 expand: ['latest_invoice.payment_intent'],
8});
9
10console.log('Subscription ID:', subscription.id);
11console.log('Status:', subscription.status); // 'incomplete' → 'active' after payment

Expected result: The subscription is created. If the default payment method is valid, it transitions to 'active' after the first invoice is paid.

5

Handle subscription statuses

Subscriptions transition through several statuses. Listen for webhooks to update your app accordingly.

typescript
1// Subscription statuses:
2// 'incomplete' — first payment pending
3// 'incomplete_expired' — first payment failed after 23 hours
4// 'trialing' — in trial period
5// 'active' — payment succeeded, subscription active
6// 'past_due' — renewal payment failed, retrying
7// 'canceled' — subscription ended
8// 'unpaid' — all retries exhausted
9
10// 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

subscription-api-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 '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});
37
38app.use(express.json());
39
40// Create customer
41app.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});
48
49// Setup intent for collecting payment method
50app.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});
57
58// Create subscription
59app.post('/create-subscription', async (req, res) => {
60 const { customerId, priceId, paymentMethodId } = req.body;
61
62 // Set default payment method
63 await stripe.customers.update(customerId, {
64 invoice_settings: { default_payment_method: paymentMethodId },
65 });
66
67 const subscription = await stripe.subscriptions.create({
68 customer: customerId,
69 items: [{ price: priceId }],
70 expand: ['latest_invoice.payment_intent'],
71 });
72
73 res.json({
74 subscriptionId: subscription.id,
75 status: subscription.status,
76 clientSecret: subscription.latest_invoice?.payment_intent?.client_secret,
77 });
78});
79
80app.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.

ChatGPT Prompt

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.

Stripe Prompt

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.

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.