Stripe webhook reference

Stripe customer.subscription.deleted

The webhook that fires when a Stripe subscription truly ends. The most common bug: treating it as the cancellation moment when it's actually the end-of-period moment, weeks too late for the exit email.

Quick answer

customer.subscription.deleted fires when the subscription period ends after a cancellation, not when the customer hits cancel. Listen for customer.subscription.updated with cancel_at_period_end=true to catch the cancellation moment. Use cancellation_details.reason to split voluntary vs involuntary churn.

The cancel_at_period_end gotcha

By default, when a customer cancels mid-period in your billing portal, Stripe sets cancel_at_period_end=true and keeps the subscription active until the period elapses. The event that fires at that moment is customer.subscription.updated, notcustomer.subscription.deleted.

If you wait for customer.subscription.deletedto trigger your exit-feedback email, you'll send it days or weeks after the customer decided to leave. By then they've moved on. Reply rates collapse from ~20% (sent at the moment of decision) to ~3% (sent weeks later).

Voluntary vs involuntary churn

The cancellation_details.reason field tells you why the subscription ended. Treat the two groups completely differently:

cancellation_details.reasonMeaningRight follow-up
cancellation_requestedCustomer chose to leavePlain-text exit email asking why
payment_failedDunning exhausted, card never fixed"We couldn't charge your card. Reactivate here"
incomplete_expiredInitial payment never completedOnboarding follow-up, not exit email

FAQ

When does customer.subscription.deleted fire?
When a Stripe subscription is fully ended. Either because the customer cancelled and the period elapsed (cancel_at_period_end → end), the merchant cancelled immediately, or Stripe ended the subscription after exhausting dunning retries on a past_due invoice. It does NOT fire when the customer schedules a cancellation; that's customer.subscription.updated.
Difference between cancel_at_period_end and customer.subscription.deleted?
When a customer hits 'cancel' in your portal, Stripe usually sets cancel_at_period_end=true and fires customer.subscription.updated. The subscription stays active until the end of the paid period. customer.subscription.deleted only fires when the period actually elapses and the subscription truly ends. If you trigger exit emails on .deleted, you'll be emailing the customer weeks after they decided to leave.
How do I detect a cancellation immediately?
Listen for customer.subscription.updated and check whether cancel_at_period_end transitioned from false to true. That's the real cancellation moment. When the customer hit the button. customer.subscription.deleted is the end-of-period event, not the cancellation event.
What's in the customer.subscription.deleted payload?
data.object is the Subscription. Useful fields: id, customer, status (will be 'canceled'), cancel_at_period_end, canceled_at (unix timestamp), cancellation_details.reason ('cancellation_requested', 'payment_failed', 'incomplete_expired'), and items.data for plan info. cancellation_details.reason is critical. It tells you voluntary vs involuntary.
How do I distinguish voluntary from involuntary churn?
Check cancellation_details.reason. 'cancellation_requested' = voluntary (customer chose to leave; send exit email asking why). 'payment_failed' or 'incomplete_expired' = involuntary (their card failed; this is a dunning-flow miss, not a customer choice). The two require completely different follow-up.
Should I send an exit email on customer.subscription.deleted?
Only if cancellation_details.reason is 'cancellation_requested'. Sending 'sorry to see you go, can you tell us why?' to someone whose card just failed is tone-deaf. They didn't choose to leave. For involuntary cancellations, send a 'we couldn't process payment, here's how to reactivate' message instead.
What if cancel_at_period_end is true on the deleted event?
That's the most common voluntary case. The customer cancelled earlier and now the period has ended. By this point they've already had access for days or weeks since deciding to leave. Best practice: send your exit-feedback email at the moment of cancellation (the .updated event), not weeks later at .deleted.
How does ChurnNote handle customer.subscription.deleted?
ChurnNote listens for both customer.subscription.updated (to catch the cancellation moment) and customer.subscription.deleted (to confirm and finalize). It sends a plain-text exit email at the cancellation moment, asks one open question, categorizes the reply, and queues a win-back when you ship a fix.

Catch cancellations the moment they happen.

ChurnNote listens for both .updated (cancel moment) and .deleted (period end), routes voluntary cancellers into exit-feedback and involuntary ones into reactivation. $12/mo flat.

Get started