See who cancelled on Stripe but hasn’t left yet
Last updated 12 October 2026
When a customer cancels at the end of their billing period, Stripe doesn’t end the subscription. It stays active, and they keep what they paid for until the date comes. Of everyone you lose, these are the only people you can still keep: they’ve decided, but they haven’t gone. Here’s how to list them, how long each one has left, and what to write.
There’s no filter for it
The subscriptions list won’t filter on a scheduled cancellation. Pass cancel_at and Stripe answers 400 Received unknown parameter. So read the subscriptions that are still running and pick them out yourself:
const leaving = [];
for await (const s of stripe.subscriptions.list({ limit: 100, expand: ['data.customer'] })) {
if (s.cancel_at == null) continue; // nothing scheduled
if (s.schedule != null) continue; // a fixed term ending, not a cancel
if (!['active', 'trialing'].includes(s.status)) continue; // still paying
leaving.push(s);
}
leaving.sort((a, b) => a.cancel_at - b.cancel_at); // fewest days left firstLeaving out status already drops subscriptions that have ended. Expanding the customer brings each name and email in the same request. A row falls off on its own once its date passes, because Stripe then sets it to canceled.
Select on cancel_at, not cancel_at_period_end
Both ways of cancelling fill in cancel_at. The flag doesn’t follow it: a cancellation scheduled for a date sets cancel_at_period_end to false, so a list built on the flag quietly misses people. Read the date, never the flag.
Three rows that need a second look
- A fixed term that’s ending. A subscription schedule set to cancel at its end stamps the same
cancel_at,canceled_atand evencancellation_details.reason: cancellation_requestedon the subscription. Nobody hit cancel, and onlys.scheduletells the two apart. Skip that check and you write “if you left because of something we could fix” to someone who never left. - Trials. Someone who cancels during a trial is the most saveable person on the list, so keep them. But they haven’t paid anything, so don’t count their price as money at stake. Don’t tell them “you won’t be charged again” either; they were never charged.
- The other statuses. A list without a status filter also returns
past_due,unpaid,pausedandincomplete. None of those is still paying, so keep onlyactiveandtrialing.
Count days the way you print the date
(cancel_at - now) / 86400 counts 24-hour blocks, but the date you show is a calendar date in your time zone. Whenever the cancellation’s time of day is earlier than now, the count comes out a day short. On one afternoon’s test that was 15 rows out of 19, and an email said “ends on Sep 20, you still have 0 days of access”. Count calendar days in your own time zone, and it can’t disagree with the date beside it.
The amount is also more than one price. Subscriptions with several items are real, and items.data[0].price reports one item as the whole subscription. Add up every item, and take off any coupon.
Read why they left
cancellation_details.comment is the best field on the subscription: what the customer typed when they cancelled, like “Your CSV export keeps timing out on big accounts”. Read it first, then feedback, which is a fixed reason and empty for most people. It’s only filled when they cancelled through a portal with the survey switched on.
The email
Everything it needs is already on the row:
Subject: Your Pro access ends Oct 30
Hi Maya,
Your subscription is set to end on Oct 30. You still have 18 days of access, and you won’t be charged again.
If you left because of something we could fix, reply and tell me. I’d rather fix it than lose you.
If not, no action needed.
Two rules:
- Never write “your account has been cancelled”. It hasn’t been. The status is still
activeand they can still log in. - Don’t lead with a discount before you know the reason. The reply is how you find it out.
Keeping track of who you emailed
Mark people as contacted by canceled_at, not by cancel_at. Cancel, undo, cancel again: cancel_at comes back identical, so a mark built on it says you already wrote about a cancellation you’ve never seen. canceled_at is stamped at each request and cleared when one is reversed.
Gradsey’s Scheduled cancellations report is this list, kept for you. It shows every customer leaving, the days they have left, what they’re worth, how long they’ve been with you and what they wrote, with an email like the one above already filled in for each. The daily email adds anyone new each morning, so nobody runs out their paid days unnoticed.