How to alert your team when a high-value Stripe account downgrades
A scheduled downgrade doesn't fire a cancellation event, and most webhook setups aren't listening for the event it does fire. Stripe added the ability to schedule a downgrade for the end of the billing period as a distinct, catchable thing, separate from a customer canceling outright. Catching it takes the same approach as anything else in Stripe, listening for the specific event and routing it somewhere a person will see it. The part that trips teams up is knowing which event that is.
This guide covers what event fires, how to tell a downgrade from the other reasons a subscription object changes, and the point where a Slack ping alone stops being enough for an account like that.
The schedule object behind a portal downgrade
In the customer portal's subscription-management settings, there's a "Manage downgrades" option. Per Stripe's own configuration docs, the default behavior is "Update immediately." Turn on "Manage downgrades" instead, and a customer who picks a lower-priced plan doesn't change today. The change is scheduled for the end of their current billing period, and the portal automatically creates and attaches a subscription schedule to the subscription to make that happen.
That schedule is a real, separate Stripe object with a phases array. The current phase carries today's price, and a second phase carries the lower price, set to start when the current billing period ends. Two events follow from this:
subscription_schedule.createdfires the moment the schedule is created, which for a portal downgrade is the moment the customer clicks confirm. This is the event Stripe's own subscription-schedules guide tells you to build on, noting you should "store the subscription schedule ID... through thesubscription_schedule.createdwebhook when Stripe creates it automatically, such as when a customer scheduled a downgrade in the Customer Portal."subscription_schedule.releasedfires later, but not because the customer changed their mind. Stripe's customer-portal documentation states plainly that "customers can't update or cancel subscriptions that currently have an update scheduled with a subscription schedule." A portal-scheduled downgrade only gets released in one of two ways. It happens automatically, once the schedule finishes its phases and hands control back to the plain subscription under the defaultreleaseend behavior. Or it happens earlier, if someone on your side calls the release endpoint to undo the pending change for the customer. That second path is the only way a scheduled downgrade gets walked back before it takes effect. The customer can't do it themselves.
Either way, subscription_schedule.created is the event to build the alert on. Not customer.subscription.updated, which won't fire until the schedule's second phase actually activates weeks later, at renewal, quietly, with no reason attached.
Stripe only allows a portal-scheduled downgrade "between prices that have the same product." If your pricing tiers are modeled as separate Stripe products rather than separate prices on one product, the portal's downgrade-scheduling feature doesn't apply, and a tier change there is an immediate update, a different and simpler problem than the one this guide covers.
The six-week gap a missed event leaves open
Picture a vendor selling infrastructure-monitoring software, priced by seat count across three tiers modeled as three prices on one Stripe product, the exact setup the portal's downgrade feature requires. An account has been on the top tier for close to a year. Someone on their side uses the self-serve portal to schedule a downgrade to the middle tier, effective at their next renewal, six weeks out. Nothing about that touches a support ticket, a Slack message, or an email. It's a portal click that fires one webhook event most setups aren't listening for.
Without the webhook above, the account team finds out at renewal, the day the invoice drops. That's one day of notice instead of six weeks to ask what changed and do something about it. Whatever's actually driving the downgrade, a stalled onboarding, a champion who left, a competitor's pitch, was knowable weeks earlier. The webhook is what turns that into something someone can act on before the invoice, not after.
Building the webhook that catches the scheduling moment, not the renewal
The build is a small, real thing, useful even before you decide whether you need more than it:
- Subscribe your webhook endpoint to
subscription_schedule.created. This is the event that fires at the moment of scheduling, not at the moment the change takes effect. - Verify the Stripe signature on the payload using the
Stripe-Signatureheader and your endpoint's signing secret before trusting anything in it, per Stripe's webhook guide. - Read the schedule's
phasesarray. Compare the price on the current phase to the price on the next phase. If the next phase's price is lower, it's a downgrade; if it's the same price with a different quantity, it's something else (a seat reduction, most commonly). - Look up the account's current value. Pull the price amounts (or your own stored plan-value mapping) so the alert can say what's actually at stake, not just that a schedule exists.
- Filter by size before you page anyone. A five-seat downgrade on a $40/month plan and a downgrade off a five-figure annual contract are not the same event; most teams only want the second one interrupting someone's day.
- Post it somewhere a person reads it, with the account name, the dollar amount moving, and the effective date. A Slack message to your account team's channel is the minimum viable version, and it buys back exactly the six weeks of notice the scenario above was missing.
That's an afternoon of work, not a project, and it closes the exact gap in the scenario above.
The gap a bare Slack ping leaves open
The failure mode isn't the webhook missing events. It's what a bare alert can't carry once account complexity grows:
- The alert has no memory of the account's other signals. A downgrade alert with just a dollar figure doesn't know about a stalled onboarding or an open support thread, and nobody reading the alert would think to go looking.
- The six-week window is only useful if someone acts inside it. An alert that lands and gets acknowledged with a thumbs-up emoji, then nothing, protects nothing. A raw webhook has no way to track whether the follow-up actually happened.
None of that is a bug in the webhook. It's what happens when a routing script hands off one fact about an account with no context about what else is true of it, the same ceiling covered in more general terms in how to give AI agents customer context.
What the alert carries once Modem is already reading it
This is the layer we build for. Modem connects to Stripe as a documented integration, keeping each company's plan and billing status current on its record, next to the Slack threads, support tickets, and call notes already attached to that same account. What exists today is narrower than the scheduling event itself. Modem's Stripe automation triggers cover new customers, subscriptions starting, subscriptions canceling, trials ending, and failed payments, and a scheduled downgrade isn't one of the five yet, so catching the scheduling moment still means the webhook above.
What changes is what happens to the alert next. Post the webhook's Slack message into a channel Modem is already reading, and it doesn't land as an isolated ping. It lands as a topic on that company's record, sitting next to whatever else is true of the account in the same window, such as an open support thread, a stalled onboarding step, or a call note about a champion leaving. That's the first gap closed. The alert arrives already carrying a reason, not just a number, which is the context graph idea applied to a churn signal instead of a bug report.
The second gap, whether anyone actually followed up, closes the same way requesters get tracked through to shipped work elsewhere in Modem. A topic can carry a linked Linear issue or an assigned owner, so there's a real status to check next month instead of a Slack thread nobody remembers to scroll back to.
If a raw Slack alert with the dollar amount is genuinely enough for your team's size, the webhook above does that job completely on its own. The related gap on immediate cancellations, rather than scheduled downgrades, is covered in how to route Stripe customer portal cancellation reasons to your product team. We build Modem, so weigh this recommendation knowing where it's coming from.
Start with the event, not the dashboard
If you're relying on noticing a downgrade at the Dashboard or at the renewal invoice, the fix is one webhook subscribed to subscription_schedule.created, a phase-price comparison, and a Slack message with a dollar figure attached. That alone turns a six-week blind spot into six weeks of notice, which is the whole gap this guide is about closing.
