<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: North Metric</title>
    <description>The latest articles on DEV Community by North Metric (@northmetric).</description>
    <link>https://dev.to/northmetric</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4232001%2F0d4684ce-adeb-42f3-a573-584f56cd9ce6.png</url>
      <title>DEV Community: North Metric</title>
      <link>https://dev.to/northmetric</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/northmetric"/>
    <language>en</language>
    <item>
      <title>Which Stripe signals predict churn (and how to read them without a data team)</title>
      <dc:creator>North Metric</dc:creator>
      <pubDate>Sat, 07 Nov 2026 12:10:55 +0000</pubDate>
      <link>https://dev.to/northmetric/which-stripe-signals-predict-churn-and-how-to-read-them-without-a-data-team-3na2</link>
      <guid>https://dev.to/northmetric/which-stripe-signals-predict-churn-and-how-to-read-them-without-a-data-team-3na2</guid>
      <description>&lt;p&gt;Most small SaaS teams find out about churn from a Stripe email saying a subscription was canceled. By then the customer made the decision weeks ago. The card failed twice, the team dropped from eight seats to three, or someone quietly set the subscription to cancel at period end.&lt;/p&gt;

&lt;p&gt;Bigger companies catch this with a customer success team and a health score. You probably don't have either. You do have Stripe, though, and Stripe already records most of the warning signs. This post covers which objects and events to look at, what each one means, and how to turn them into a weekly list without building a data pipeline.&lt;/p&gt;

&lt;p&gt;For context on why this matters: median monthly churn for SaaS under $300k ARR is around 6.5% (ChartMogul, 2021-22 data). At that rate, you replace most of your customer base every year just to stand still. (We keep a &lt;a href="https://www.northmetric.io/benchmarks/saas-churn-rate" rel="noopener noreferrer"&gt;table of churn benchmarks by stage and MRR band&lt;/a&gt; if you want to see where you sit.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Before you start: use a read-only key
&lt;/h3&gt;

&lt;p&gt;Everything below only needs read access. In the Stripe dashboard, create a &lt;strong&gt;restricted key&lt;/strong&gt; with read permissions for Customers, Subscriptions, Invoices, Charges and Payment Methods, plus webhook access if you're listening for events. Don't use your secret key for analytics scripts, and don't give it to any tool that only needs to read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 1: failed payments (&lt;code&gt;invoice.payment_failed&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Involuntary churn is the easiest kind to fix and the kind most teams ignore. In Stripe's data, failed payments cause about &lt;strong&gt;22% of SaaS churn, 16% for B2B&lt;/strong&gt; (Stripe data via Churnkey, 2024).&lt;/p&gt;

&lt;p&gt;On each &lt;code&gt;invoice.payment_failed&lt;/code&gt; event, look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;attempt_count&lt;/code&gt;: how many times Stripe has tried.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next_payment_attempt&lt;/code&gt;: when the next retry is. If it's &lt;code&gt;null&lt;/code&gt;, retries are over, and the subscription is about to be canceled, marked unpaid, or left past due, depending on your settings.&lt;/li&gt;
&lt;li&gt;The decline code on the related charge (&lt;code&gt;failure_code&lt;/code&gt;, or &lt;code&gt;decline_code&lt;/code&gt; on the error). &lt;code&gt;insufficient_funds&lt;/code&gt; accounts for roughly &lt;strong&gt;41-42% of failed payments&lt;/strong&gt; (Churnkey, State of Retention 2025) and often clears on a later retry. &lt;code&gt;expired_card&lt;/code&gt; and &lt;code&gt;incorrect_number&lt;/code&gt; won't clear, so email those customers immediately. There's a &lt;a href="https://www.northmetric.io/stripe-decline-codes" rel="noopener noreferrer"&gt;reference of what each Stripe decline code means and what to do about it&lt;/a&gt; if you want the full list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tooling matters less than you'd think. Smart Retries alone recovered about &lt;strong&gt;51%&lt;/strong&gt; of failed payments, and adding a payment wall and dunning lifted that to about &lt;strong&gt;70%&lt;/strong&gt; (Churnkey). Dunning emails on their own recover little: the first recovers &lt;strong&gt;2.8%&lt;/strong&gt; (Churnkey). A recovered monthly subscription lasts about &lt;strong&gt;seven more months&lt;/strong&gt; on average (Stripe, reported by FF News, 2024), so each save is worth more than one invoice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 2: cards about to expire
&lt;/h3&gt;

&lt;p&gt;Pull the default payment method for each active subscription and check &lt;code&gt;card.exp_month&lt;/code&gt; and &lt;code&gt;card.exp_year&lt;/code&gt;. Anything expiring this month or next goes on the list.&lt;/p&gt;

&lt;p&gt;Card networks' automatic updater fixes many of these, and you'll see &lt;code&gt;payment_method.automatically_updated&lt;/code&gt; when it does. It doesn't catch all of them, and a short "your card expires soon" email before the renewal date costs nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 3: &lt;code&gt;cancel_at_period_end&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;When a customer cancels through the customer portal or your own flow, the subscription usually stays active with &lt;code&gt;cancel_at_period_end: true&lt;/code&gt; until the period ends. This is the most honest signal you have: the customer has already decided, and you have days or weeks to change their mind.&lt;/p&gt;

&lt;p&gt;If you use Stripe's customer portal with cancellation reasons turned on, &lt;code&gt;cancellation_details.feedback&lt;/code&gt; and &lt;code&gt;cancellation_details.comment&lt;/code&gt; tell you why. Budget is the top stated reason for cancelling (about &lt;strong&gt;33%&lt;/strong&gt;), with infrequent usage close behind (about &lt;strong&gt;31%&lt;/strong&gt;) and rising (Churnkey, State of Retention 2025). Those two need very different responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 4: downgrades and seat drops (&lt;code&gt;customer.subscription.updated&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Contraction usually comes before churn. A team that goes from ten seats to four is telling you something, even though it's still paying.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;customer.subscription.updated&lt;/code&gt; includes &lt;code&gt;data.previous_attributes&lt;/code&gt;, which holds only the fields that changed. Compare the old and new items:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Express + stripe-node. Needs the raw body for signature checks.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/stripe/webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer.subscription.updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previous_attributes&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cancel_at_period_end&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cancel_at_period_end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scheduled_cancel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unit_amount&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;seats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;seats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;seat_drop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;downgrade&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice.payment_failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next_payment_attempt&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment_retrying&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment_final&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;flag()&lt;/code&gt; can just write a row to a table or a spreadsheet. If you use tiered or usage-based prices, &lt;code&gt;unit_amount&lt;/code&gt; can be null, so compare the upcoming invoice amount instead. Field names also move between Stripe API versions, so check them against the version your account is pinned to.&lt;/p&gt;

&lt;p&gt;A complete version of this listener (signature checks, de-duplication of retried events, all the signals in this post, CSV output) is on GitHub as &lt;a href="https://github.com/north-metric/stripe-churn-signals" rel="noopener noreferrer"&gt;stripe-churn-signals&lt;/a&gt;, MIT licensed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 5: usage declines
&lt;/h3&gt;

&lt;p&gt;Billing tells you &lt;em&gt;that&lt;/em&gt; an account is at risk. Usage tells you &lt;em&gt;why&lt;/em&gt;. If you bill by usage through Stripe meters, a falling meter total is the signal. If you don't, pull a weekly active-user or core-action count per account from your product analytics.&lt;/p&gt;

&lt;p&gt;Two rules keep this simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compare each account with &lt;strong&gt;its own&lt;/strong&gt; baseline, not with other customers. A 50% drop in the last 14 days against its previous 28-day average is a useful starting threshold.&lt;/li&gt;
&lt;li&gt;Watch new accounts closely. In Amplitude's 2025 benchmarks, &lt;strong&gt;69%&lt;/strong&gt; of products with strong 7-day activation also had strong 3-month retention. Accounts that never activate rarely renew.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Signal 6: upcoming renewals
&lt;/h3&gt;

&lt;p&gt;Annual customers churn on a schedule. &lt;code&gt;invoice.upcoming&lt;/code&gt; fires before each renewal (the timing is set in Billing settings). An annual renewal in the next 30-60 days on an account with weak usage should be the first thing you look at that week.&lt;/p&gt;

&lt;h3&gt;
  
  
  Putting it together without a data team
&lt;/h3&gt;

&lt;p&gt;You don't need a model. A simple weekly sheet works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One row per active subscription: customer, MRR, and a column per signal above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High risk:&lt;/strong&gt; two or more strong signals (for example, a failed payment plus a usage drop, or a seat drop plus a renewal within 30 days). &lt;strong&gt;Medium:&lt;/strong&gt; one strong signal. &lt;strong&gt;Low:&lt;/strong&gt; one weak signal.&lt;/li&gt;
&lt;li&gt;Sort by MRR within each risk band. A medium-risk account paying $800 a month usually deserves attention before a high-risk one paying $19.&lt;/li&gt;
&lt;li&gt;Match the action to the signal: a card-update link for payment issues, a short personal check-in for usage drops, a right-sized plan for seat drops.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then close the loop. Record what you did and whether the customer stayed. Research on retention targeting (Ascarza, &lt;em&gt;Journal of Marketing Research&lt;/em&gt;, 2018) found that targeting customers by how they respond to outreach beats targeting by risk alone. Your own outcome log is how you learn which customers respond.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrapping up
&lt;/h3&gt;

&lt;p&gt;None of this needs more than read access to Stripe and an hour a week. Start with failed payments and &lt;code&gt;cancel_at_period_end&lt;/code&gt;, since they're the most reliable signals. Add usage once those are routine.&lt;/p&gt;

&lt;p&gt;If you'd rather not wire it up yourself, we build &lt;a href="https://www.northmetric.io" rel="noopener noreferrer"&gt;North Metric&lt;/a&gt;, which reads Stripe billing and product usage, ranks at-risk customers by revenue, and suggests a save action for each. You can start with a free churn report. Either way, the list above works fine with a spreadsheet.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>stripe</category>
      <category>startup</category>
      <category>analytics</category>
    </item>
  </channel>
</rss>
