# How to create a subscription with Stripe API

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

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

## Before you start

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

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

const customer = await stripe.customers.create({
  email: 'subscriber@example.com',
  name: 'Jane Doe',
  metadata: {
    internal_user_id: 'user_123',
  },
});

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

```
// Server: Create SetupIntent
app.post('/create-setup-intent', async (req, res) => {
  const setupIntent = await stripe.setupIntents.create({
    customer: req.body.customerId,
    automatic_payment_methods: { enabled: true },
  });
  res.json({ clientSecret: setupIntent.client_secret });
});

// Frontend: Confirm the SetupIntent
// (After mounting PaymentElement)
const { error, setupIntent } = await stripe.confirmSetup({
  elements,
  redirect: 'if_required',
});

if (setupIntent.status === 'succeeded') {
  // Payment method is saved to the customer
  console.log('Payment method:', setupIntent.payment_method);
}
```

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

```
await stripe.customers.update(customer.id, {
  invoice_settings: {
    default_payment_method: setupIntent.payment_method,
  },
});
```

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

```
const subscription = await stripe.subscriptions.create({
  customer: customer.id,
  items: [
    { price: 'price_xxx' }, // Your recurring Price ID
  ],
  payment_behavior: 'default_incomplete',
  expand: ['latest_invoice.payment_intent'],
});

console.log('Subscription ID:', subscription.id);
console.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.

```
// Subscription statuses:
// 'incomplete'       — first payment pending
// 'incomplete_expired' — first payment failed after 23 hours
// 'trialing'         — in trial period
// 'active'           — payment succeeded, subscription active
// 'past_due'         — renewal payment failed, retrying
// 'canceled'         — subscription ended
// 'unpaid'           — all retries exhausted

// Webhook handler for status changes:
case 'customer.subscription.updated':
  const sub = event.data.object;
  if (sub.status === 'active') {
    grantAccess(sub.customer);
  } else if (sub.status === 'past_due') {
    notifyPaymentFailed(sub.customer);
  }
  break;
```

**Expected result:** Your app responds to each subscription status change appropriately.

## Complete code example

File: `subscription-api-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 'customer.subscription.created':
      console.log('Subscription created:', event.data.object.id);
      break;
    case 'customer.subscription.updated':
      console.log('Subscription updated:', event.data.object.status);
      break;
    case 'customer.subscription.deleted':
      console.log('Subscription cancelled:', event.data.object.id);
      break;
    case 'invoice.paid':
      console.log('Invoice paid:', event.data.object.subscription);
      break;
    case 'invoice.payment_failed':
      console.log('Payment failed:', event.data.object.subscription);
      break;
  }
  res.json({ received: true });
});

app.use(express.json());

// Create customer
app.post('/create-customer', async (req, res) => {
  const customer = await stripe.customers.create({
    email: req.body.email,
    metadata: { user_id: req.body.userId },
  });
  res.json({ customerId: customer.id });
});

// Setup intent for collecting payment method
app.post('/create-setup-intent', async (req, res) => {
  const setupIntent = await stripe.setupIntents.create({
    customer: req.body.customerId,
    automatic_payment_methods: { enabled: true },
  });
  res.json({ clientSecret: setupIntent.client_secret });
});

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

  // Set default payment method
  await stripe.customers.update(customerId, {
    invoice_settings: { default_payment_method: paymentMethodId },
  });

  const subscription = await stripe.subscriptions.create({
    customer: customerId,
    items: [{ price: priceId }],
    expand: ['latest_invoice.payment_intent'],
  });

  res.json({
    subscriptionId: subscription.id,
    status: subscription.status,
    clientSecret: subscription.latest_invoice?.payment_intent?.client_secret,
  });
});

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

## Common mistakes

- **Not setting a default payment method before creating the subscription** — undefined Fix: 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.
- **Ignoring the 'incomplete' status** — undefined Fix: 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'.
- **Not handling 'past_due' subscriptions** — undefined Fix: 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.
- **Confusing SetupIntents with PaymentIntents for subscriptions** — undefined Fix: 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

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

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-create-a-subscription-with-stripe-api
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-create-a-subscription-with-stripe-api
