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
Cancel a subscription immediately
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.
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);23async 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.
Cancel at the end of the billing period
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.
1async function cancelAtPeriodEnd(subscriptionId) {2 const subscription = await stripe.subscriptions.update(subscriptionId, {3 cancel_at_period_end: true,4 });56 const endDate = new Date(subscription.current_period_end * 1000);7 console.log(`Subscription will cancel on: ${endDate.toISOString()}`);89 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.
Issue a prorated refund for immediate cancellation
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.
1async function cancelWithRefund(subscriptionId) {2 // First, cancel the subscription with proration3 const subscription = await stripe.subscriptions.cancel(subscriptionId, {4 prorate: true, // Generate a prorated credit5 invoice_now: true, // Create a final invoice with the credit6 });78 // 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 });1415 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 );2324 const refund = await stripe.refunds.create({25 charge: lastInvoice.charge,26 amount: refundAmount, // Partial refund in cents27 });28 console.log('Refunded:', refund.amount);29 }30 }3132 return subscription;33}Expected result: The subscription is canceled and a prorated refund is issued for the unused portion of the billing period.
Handle edge cases
Handle edge cases
Check subscription status before canceling to handle already-canceled, paused, or trialing subscriptions.
1async function safeCancelSubscription(subscriptionId, atPeriodEnd = true) {2 // Retrieve current state3 const sub = await stripe.subscriptions.retrieve(subscriptionId);45 if (sub.status === 'canceled') {6 return { message: 'Subscription is already canceled' };7 }89 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 }1516 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.
Set up the cancellation webhook handler
Set up the cancellation webhook handler
Listen for customer.subscription.deleted to revoke access when the subscription is fully canceled.
1// In your webhook handler:2case 'customer.subscription.deleted': {3 const subscription = event.data.object;4 // Revoke access in your database5 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
1const express = require('express');2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);34const app = express();56app.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_SECRET12 );13 } catch (err) {14 return res.status(400).send(`Webhook Error: ${err.message}`);15 }1617 if (event.type === 'customer.subscription.deleted') {18 console.log('Subscription fully canceled:', event.data.object.id);19 // TODO: revoke access in your database20 }21 res.json({ received: true });22});2324app.use(express.json());2526app.post('/cancel-subscription', async (req, res) => {27 const { subscriptionId, immediate = false } = req.body;2829 try {30 const sub = await stripe.subscriptions.retrieve(subscriptionId);3132 if (sub.status === 'canceled') {33 return res.json({ message: 'Already canceled' });34 }3536 if (immediate) {37 const canceled = await stripe.subscriptions.cancel(subscriptionId);38 return res.json({ status: canceled.status, message: 'Canceled immediately' });39 }4041 const updated = await stripe.subscriptions.update(subscriptionId, {42 cancel_at_period_end: true,43 });4445 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});5354app.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});6566app.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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation