# How to cancel a subscription with Stripe API

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

## TL;DR

Cancel a Stripe subscription via the API using stripe.subscriptions.cancel(subscriptionId) for immediate cancellation or stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: true }) for end-of-period cancellation. Handle the customer.subscription.deleted webhook to revoke access and optionally issue prorated refunds.

## API-Based Subscription Cancellation in Stripe

The Stripe API provides two cancellation approaches: immediate cancellation via stripe.subscriptions.cancel() which ends the subscription right now, and scheduled cancellation via stripe.subscriptions.update() with cancel_at_period_end: true which lets the subscription run until the current period ends. This guide covers both, including error handling, prorated refunds, and the webhook events you must handle to keep your app in sync.

## Before you start

- An active Stripe subscription to cancel
- Node.js 18+ with the stripe npm package
- A webhook endpoint for customer.subscription.deleted events
- The subscription ID (sub_xxx) stored in your database

## Step-by-step guide

### 1. Cancel a subscription immediately

Call stripe.subscriptions.cancel() with the subscription ID. The subscription status immediately changes to 'canceled' and no further invoices are generated.

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

async function cancelImmediately(subscriptionId) {
  try {
    const subscription = await stripe.subscriptions.cancel(subscriptionId);
    console.log('Canceled:', subscription.id, subscription.status);
    return subscription;
  } catch (err) {
    if (err.code === 'resource_missing') {
      console.error('Subscription not found:', subscriptionId);
    }
    throw err;
  }
}
```

**Expected result:** The subscription status is 'canceled'. Stripe fires a customer.subscription.deleted webhook event.

### 2. Cancel at the end of the billing period

Set cancel_at_period_end to true. The subscription remains active and usable until the current period ends, then cancels automatically.

```
async function cancelAtPeriodEnd(subscriptionId) {
  const subscription = await stripe.subscriptions.update(subscriptionId, {
    cancel_at_period_end: true,
  });

  const endDate = new Date(subscription.current_period_end * 1000);
  console.log(`Subscription will cancel on: ${endDate.toISOString()}`);

  return subscription;
}
```

**Expected result:** The subscription remains active but is scheduled to cancel. The customer can use the service until the period end date.

### 3. Issue a prorated refund for immediate cancellation

When canceling immediately mid-period, the customer has unused time. Optionally issue a prorated refund for the remaining days.

```
async function cancelWithRefund(subscriptionId) {
  // First, cancel the subscription with proration
  const subscription = await stripe.subscriptions.cancel(subscriptionId, {
    prorate: true,          // Generate a prorated credit
    invoice_now: true,       // Create a final invoice with the credit
  });

  // The final invoice may have a negative amount (credit).
  // To actually refund, retrieve the last paid invoice and refund.
  const invoices = await stripe.invoices.list({
    subscription: subscriptionId,
    limit: 1,
  });

  if (invoices.data.length > 0) {
    const lastInvoice = invoices.data[0];
    if (lastInvoice.charge) {
      const daysUsed = /* calculate */ 15;
      const totalDays = 30;
      const refundAmount = Math.round(
        lastInvoice.amount_paid * ((totalDays - daysUsed) / totalDays)
      );

      const refund = await stripe.refunds.create({
        charge: lastInvoice.charge,
        amount: refundAmount, // Partial refund in cents
      });
      console.log('Refunded:', refund.amount);
    }
  }

  return subscription;
}
```

**Expected result:** The subscription is canceled and a prorated refund is issued for the unused portion of the billing period.

### 4. Handle edge cases

Check subscription status before canceling to handle already-canceled, paused, or trialing subscriptions.

```
async function safeCancelSubscription(subscriptionId, atPeriodEnd = true) {
  // Retrieve current state
  const sub = await stripe.subscriptions.retrieve(subscriptionId);

  if (sub.status === 'canceled') {
    return { message: 'Subscription is already canceled' };
  }

  if (sub.cancel_at_period_end) {
    return {
      message: 'Subscription is already scheduled to cancel',
      cancel_at: new Date(sub.current_period_end * 1000).toISOString(),
    };
  }

  if (atPeriodEnd) {
    const updated = await stripe.subscriptions.update(subscriptionId, {
      cancel_at_period_end: true,
    });
    return {
      message: 'Subscription will cancel at period end',
      cancel_at: new Date(updated.current_period_end * 1000).toISOString(),
    };
  } else {
    const canceled = await stripe.subscriptions.cancel(subscriptionId);
    return { message: 'Subscription canceled immediately', status: canceled.status };
  }
}
```

**Expected result:** The function handles already-canceled and scheduled-to-cancel cases gracefully.

### 5. Set up the cancellation webhook handler

Listen for customer.subscription.deleted to revoke access when the subscription is fully canceled.

```
// In your webhook handler:
case 'customer.subscription.deleted': {
  const subscription = event.data.object;
  // Revoke access in your database
  await db.users.update({
    where: { stripeCustomerId: subscription.customer },
    data: {
      subscriptionStatus: 'canceled',
      subscriptionId: null,
    },
  });
  console.log('Access revoked for customer:', subscription.customer);
  break;
}
```

**Expected result:** When the subscription is fully canceled, your database is updated and the customer loses access.

## Complete code example

File: `cancel-api.js`

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

const app = express();

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}`);
  }

  if (event.type === 'customer.subscription.deleted') {
    console.log('Subscription fully canceled:', event.data.object.id);
    // TODO: revoke access in your database
  }
  res.json({ received: true });
});

app.use(express.json());

app.post('/cancel-subscription', async (req, res) => {
  const { subscriptionId, immediate = false } = req.body;

  try {
    const sub = await stripe.subscriptions.retrieve(subscriptionId);

    if (sub.status === 'canceled') {
      return res.json({ message: 'Already canceled' });
    }

    if (immediate) {
      const canceled = await stripe.subscriptions.cancel(subscriptionId);
      return res.json({ status: canceled.status, message: 'Canceled immediately' });
    }

    const updated = await stripe.subscriptions.update(subscriptionId, {
      cancel_at_period_end: true,
    });

    res.json({
      message: 'Will cancel at period end',
      access_until: new Date(updated.current_period_end * 1000).toISOString(),
    });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.post('/reactivate-subscription', async (req, res) => {
  const { subscriptionId } = req.body;
  try {
    const updated = await stripe.subscriptions.update(subscriptionId, {
      cancel_at_period_end: false,
    });
    res.json({ message: 'Reactivated', status: updated.status });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

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

## Common mistakes

- **Not checking if the subscription is already canceled before calling cancel** — undefined Fix: Retrieve the subscription first and check its status. Attempting to cancel an already-canceled subscription throws an error.
- **Revoking access on the update event instead of the deleted event** — undefined Fix: customer.subscription.updated fires when cancel_at_period_end is set but the subscription is still active. Only revoke access on customer.subscription.deleted.
- **Not offering a reactivation option** — undefined Fix: When a subscription is set to cancel_at_period_end, allow the customer to reactivate by setting cancel_at_period_end: false before the period ends.
- **Forgetting to handle refunds for immediate cancellations** — undefined Fix: Immediate cancellation does not automatically refund the current period. If your policy requires it, calculate and issue a prorated refund via stripe.refunds.create().

## Best practices

- Default to cancel_at_period_end: true for a better customer experience
- Always retrieve the subscription before canceling to handle edge cases
- Listen for customer.subscription.deleted (not updated) to revoke access
- Offer a reactivation option before the period ends
- Issue prorated refunds for immediate cancellations when appropriate
- Test cancellation flows with test subscriptions created with card 4242 4242 4242 4242
- Log all cancellation actions for audit and customer support purposes

## Frequently asked questions

### What happens to pending invoices when I cancel immediately?

Pending invoices are voided. If you pass invoice_now: true, Stripe generates a final invoice with prorated charges/credits before canceling.

### Can I cancel and refund in one API call?

No. Cancellation and refunds are separate operations. Cancel the subscription first, then create a refund via stripe.refunds.create() for the last payment.

### What if the customer wants to resubscribe after cancellation?

Once a subscription is fully canceled (not just scheduled), you need to create a new subscription. You can reuse the same customer and payment method.

### How do I test cancellation?

Create a test subscription with card 4242 4242 4242 4242, then call the cancel endpoint. Check the Stripe Dashboard to verify the subscription status changed to 'canceled'.

### Can RapidDev help with subscription lifecycle management?

Yes. RapidDev can build custom cancellation flows with win-back offers, pause functionality, downgrade options, and churn analytics integrated with your Stripe subscription system.

---

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