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

How to cancel a subscription with Stripe API

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.

What you'll learn

  • How to cancel subscriptions immediately using the Stripe API
  • How to schedule cancellation at the end of the billing period
  • How to issue prorated refunds for immediate cancellations
  • How to handle edge cases like already-canceled or trialing subscriptions
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read15 minutesStripe API v2024-12+, Node.js 18+March 2026RapidDev Engineering Team
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.

Prerequisites

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

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3async function cancelImmediately(subscriptionId) {
4 try {
5 const subscription = await stripe.subscriptions.cancel(subscriptionId);
6 console.log('Canceled:', subscription.id, subscription.status);
7 return subscription;
8 } catch (err) {
9 if (err.code === 'resource_missing') {
10 console.error('Subscription not found:', subscriptionId);
11 }
12 throw err;
13 }
14}

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.

typescript
1async function cancelAtPeriodEnd(subscriptionId) {
2 const subscription = await stripe.subscriptions.update(subscriptionId, {
3 cancel_at_period_end: true,
4 });
5
6 const endDate = new Date(subscription.current_period_end * 1000);
7 console.log(`Subscription will cancel on: ${endDate.toISOString()}`);
8
9 return subscription;
10}

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.

typescript
1async function cancelWithRefund(subscriptionId) {
2 // First, cancel the subscription with proration
3 const subscription = await stripe.subscriptions.cancel(subscriptionId, {
4 prorate: true, // Generate a prorated credit
5 invoice_now: true, // Create a final invoice with the credit
6 });
7
8 // The final invoice may have a negative amount (credit).
9 // To actually refund, retrieve the last paid invoice and refund.
10 const invoices = await stripe.invoices.list({
11 subscription: subscriptionId,
12 limit: 1,
13 });
14
15 if (invoices.data.length > 0) {
16 const lastInvoice = invoices.data[0];
17 if (lastInvoice.charge) {
18 const daysUsed = /* calculate */ 15;
19 const totalDays = 30;
20 const refundAmount = Math.round(
21 lastInvoice.amount_paid * ((totalDays - daysUsed) / totalDays)
22 );
23
24 const refund = await stripe.refunds.create({
25 charge: lastInvoice.charge,
26 amount: refundAmount, // Partial refund in cents
27 });
28 console.log('Refunded:', refund.amount);
29 }
30 }
31
32 return subscription;
33}

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.

typescript
1async function safeCancelSubscription(subscriptionId, atPeriodEnd = true) {
2 // Retrieve current state
3 const sub = await stripe.subscriptions.retrieve(subscriptionId);
4
5 if (sub.status === 'canceled') {
6 return { message: 'Subscription is already canceled' };
7 }
8
9 if (sub.cancel_at_period_end) {
10 return {
11 message: 'Subscription is already scheduled to cancel',
12 cancel_at: new Date(sub.current_period_end * 1000).toISOString(),
13 };
14 }
15
16 if (atPeriodEnd) {
17 const updated = await stripe.subscriptions.update(subscriptionId, {
18 cancel_at_period_end: true,
19 });
20 return {
21 message: 'Subscription will cancel at period end',
22 cancel_at: new Date(updated.current_period_end * 1000).toISOString(),
23 };
24 } else {
25 const canceled = await stripe.subscriptions.cancel(subscriptionId);
26 return { message: 'Subscription canceled immediately', status: canceled.status };
27 }
28}

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.

typescript
1// In your webhook handler:
2case 'customer.subscription.deleted': {
3 const subscription = event.data.object;
4 // Revoke access in your database
5 await db.users.update({
6 where: { stripeCustomerId: subscription.customer },
7 data: {
8 subscriptionStatus: 'canceled',
9 subscriptionId: null,
10 },
11 });
12 console.log('Access revoked for customer:', subscription.customer);
13 break;
14}

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

Complete working example

cancel-api.js
1const express = require('express');
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4const app = express();
5
6app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
7 const sig = req.headers['stripe-signature'];
8 let event;
9 try {
10 event = stripe.webhooks.constructEvent(
11 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
12 );
13 } catch (err) {
14 return res.status(400).send(`Webhook Error: ${err.message}`);
15 }
16
17 if (event.type === 'customer.subscription.deleted') {
18 console.log('Subscription fully canceled:', event.data.object.id);
19 // TODO: revoke access in your database
20 }
21 res.json({ received: true });
22});
23
24app.use(express.json());
25
26app.post('/cancel-subscription', async (req, res) => {
27 const { subscriptionId, immediate = false } = req.body;
28
29 try {
30 const sub = await stripe.subscriptions.retrieve(subscriptionId);
31
32 if (sub.status === 'canceled') {
33 return res.json({ message: 'Already canceled' });
34 }
35
36 if (immediate) {
37 const canceled = await stripe.subscriptions.cancel(subscriptionId);
38 return res.json({ status: canceled.status, message: 'Canceled immediately' });
39 }
40
41 const updated = await stripe.subscriptions.update(subscriptionId, {
42 cancel_at_period_end: true,
43 });
44
45 res.json({
46 message: 'Will cancel at period end',
47 access_until: new Date(updated.current_period_end * 1000).toISOString(),
48 });
49 } catch (err) {
50 res.status(500).json({ error: err.message });
51 }
52});
53
54app.post('/reactivate-subscription', async (req, res) => {
55 const { subscriptionId } = req.body;
56 try {
57 const updated = await stripe.subscriptions.update(subscriptionId, {
58 cancel_at_period_end: false,
59 });
60 res.json({ message: 'Reactivated', status: updated.status });
61 } catch (err) {
62 res.status(500).json({ error: err.message });
63 }
64});
65
66app.listen(4000, () => console.log('Server on port 4000'));

Common mistakes when canceling a subscription with Stripe API

Why it's a problem: Not checking if the subscription is already canceled before calling cancel

How to avoid: Retrieve the subscription first and check its status. Attempting to cancel an already-canceled subscription throws an error.

Why it's a problem: Revoking access on the update event instead of the deleted event

How to avoid: customer.subscription.updated fires when cancel_at_period_end is set but the subscription is still active. Only revoke access on customer.subscription.deleted.

Why it's a problem: Not offering a reactivation option

How to avoid: 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.

Why it's a problem: Forgetting to handle refunds for immediate cancellations

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Write Node.js functions to cancel a Stripe subscription both immediately and at period end. Include error handling for already-canceled subscriptions, a reactivation function, and a webhook handler for customer.subscription.deleted.

Stripe Prompt

Add a subscription cancellation API to my app. Create a POST /cancel-subscription endpoint that accepts subscriptionId and an 'immediate' boolean. Handle already-canceled subscriptions, and add a /reactivate-subscription endpoint. Include the webhook handler for customer.subscription.deleted.

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.

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.