Guide · facts checked 4 Nov 2026
Stripe cancellation_details: the reason values and how to read them
When a customer cancels in Stripe's customer portal and picks a reason, Stripe stores it on the subscription in cancellation_details.feedback, one of 8 fixed values. Anything they typed after choosing Other is in cancellation_details.comment. Both arrive on the customer.subscription.updated event when the cancel is scheduled for period end (and on customer.subscription.deleted when it takes effect immediately), so your code can act the moment someone cancels.
The fields that hold a cancel reason
| Field | What Stripe says it holds |
|---|---|
cancellation_details.feedback | The reason the customer submitted for cancelling, if they cancelled explicitly. One of 8 values (below). |
cancellation_details.comment | Additional comments about why the user cancelled, if they cancelled explicitly. |
cancellation_details.feedback_option | Customised feedback options that give deeper insight into why the subscription was cancelled, if it was cancelled explicitly by the user. |
cancellation_details.reason | Why the subscription was cancelled from Stripe’s side: cancellation_requested, payment_failed, payment_disputed or canceled_by_retention_policy. |
Do not mix up feedback and reason. feedback is what the customer chose. reason is what happened to the subscription, and it is how you tell a customer who left from a card that failed.
The 8 feedback values
Stripe’s API reference lists these values for cancellation_details.feedback. The right-hand column is the portal wording from Stripe’s cancellation page that each value corresponds to; the pairing is read from the two lists, not stated by Stripe.
| API value | Portal reason it corresponds to |
|---|---|
too_expensive | It’s too expensive |
missing_features | I need more features |
switched_service | I found an alternative |
unused | I no longer need it |
customer_service | Customer service was less than expected |
too_complex | Ease of use was less than expected |
low_quality | Quality was less than expected |
other | Other reason (the customer can type free text) |
You choose which of the reasons your customers see in the portal settings, so a given subscription may only ever carry some of these values. The reading cancel reasons guide maps each one to an offer worth trying.
Reading the reason from a webhook
Stripe’s cancellation page names the subscription webhook as one of the places to find the reasons customers select, next to the subscription page in the Dashboard and Stripe Sigma. In code, the subscription sits in event.data.object on a customer.subscription.updated event:
// Inside your verified webhook handler (see the Stripe cancel flow guide for signature checking)
if (event.type === "customer.subscription.updated") {
const sub = event.data.object;
const d = sub.cancellation_details; // null if nobody cancelled explicitly
if (d && d.feedback) {
await saveCancelReason({
subscriptionId: sub.id,
feedback: d.feedback, // e.g. "too_expensive"
comment: d.comment ?? null, // free text, mostly with "other"
});
}
}
Every field is nullable, so test for cancellation_details and for feedback before you use them. customer.subscription.updated fires for every change to a subscription, so write by subscription ID (an upsert) and you will not store a cancel twice. When a cancel takes effect immediately, the subscription is deleted and the same cancellation_details object is on the customer.subscription.deleted event. The handler in the Stripe cancel flow guide already reads feedback and comment when a cancel is scheduled and is covered by tests: add the lines above where it handles the update.
What the fields do not tell you
- Your own cancel page leaves these fields empty unless you fill them. The subscription cancel and update calls accept
cancellation_details.feedbackandcomment, so write the reason there and every cancel, portal or not, lands in one place. A failed card hasreason: payment_failedand nofeedback. - Eight buckets are coarse. Too expensive may mean the price, or that they stopped getting value. The
commentfield is where the difference lives, and in the portal free text is offered only after Other reason.
That last gap is where a cancel flow earns its keep: it asks the question in your own words, reads the sentence, and answers with one offer that fits.
See a reason become an offer
Run a sample cancel session: play a subscriber, pick a reason, type a sentence, and see the offer, the wording and the Stripe change that come back.
90 seconds, no sign-up. You play the subscriber; the AI answers, and “Cancel anyway” is always one click away.