1
0 Comments

Your customers cancel and hear silence. Here’s the email that fixes it.

I run a small invoicing SaaS. Last week I did something I had never done: I subscribed to my own product with a real card, then cancelled it.

Not because anything was broken. Because I had never walked the path a leaving customer walks.

The cancellation itself worked. The Stripe customer portal did exactly what it should — confirmed it, told me access continues until the end of the billing period, downgraded the plan on schedule.

Then I waited for the confirmation email.

Nothing. Not in the inbox. Not in spam. Nothing at all.

Stripe doesn’t send one.

I went and checked the list of customer emails Stripe can send: failed payments, finalised invoices, receipts for paid invoices, trial-ending reminders, upcoming renewal reminders, expiring card warnings.

Cancellation confirmation is not on that list.

So every customer who cancels gets silence.

Why silence is expensive

Put yourself on the other side of it. You clicked cancel. Nothing arrived. You don’t actually know whether it worked.

So you check your bank statement. You check again the following week. And if you’re the cautious type, you file a chargeback just to be safe — because that’s the one mechanism you’re certain works.

That silence is a support ticket, a chargeback, and a one-star review, all queued up behind an email that takes an hour to build.

There’s also the part that isn’t about risk. Cancellation is the last thing a customer experiences from you. It’s the moment they decide what they’ll say about your product afterwards. Handling it well costs almost nothing. Handling it badly becomes the story they tell.

The implementation, since this is the part that took me longest

The obvious move is to hang the email off customer.subscription.deleted.

That’s wrong. If your portal cancels at period end — which it should — that event doesn’t fire for weeks. Your customer gets their confirmation long after they’ve stopped caring.

The right hook is customer.subscription.updated, at the moment cancel_at_period_end flips to true.

The catch is that subscription.updated fires constantly, so you need to know that this particular update is the cancellation. Stripe already solves this: the event payload includes previous_attributes, containing only the fields that actually changed. If cancel_at_period_end is present in there and the current value is true, that’s your moment. No extra database column, no flag, no dedupe table.

One more thing that cost me a deploy: newer Stripe API versions express period-end cancellation through cancel_at rather than cancel_at_period_end. My first version checked only the old field and fired exactly zero times, with no error to tell me why. Accept either shape, and log the raw values while you’re testing.

What actually goes in the email

Four things, in this order:

1. The exact date access ends. Not “at the end of your billing period” — the date.

2. Confirmation they won’t be charged again. This is the anxiety you’re defusing.

3. That nothing is deleted. Their data is still there if they come back.

4. One line: “If you cancelled because something didn’t work, reply and tell me.”

That last line is the entire reason to send it. A cancellation is a customer telling you something without words. This email is your only chance to ask what.

The lesson I keep relearning

Everyone tests signup. Almost nobody tests cancellation.

We instrument the funnel, obsess over onboarding, rewrite the pricing page — and leave the exit unlit. But the exit is where churn gets explained, where refunds get requested, and where reviews get written.

Go and cancel your own subscription this week. Real card, real account, all the way through. I’d bet a decent number of you find the same silence I did.

What else have you found by walking your own unhappy paths?