<?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: Mueez Afzal</title>
    <description>The latest articles on DEV Community by Mueez Afzal (@mueezafzal).</description>
    <link>https://dev.to/mueezafzal</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%2F4194386%2F1b3cc9c8-7177-4491-99c6-7721cf06fdc1.jpg</url>
      <title>DEV Community: Mueez Afzal</title>
      <link>https://dev.to/mueezafzal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mueezafzal"/>
    <language>en</language>
    <item>
      <title>A Stripe API upgrade broke our webhooks, and nothing errored</title>
      <dc:creator>Mueez Afzal</dc:creator>
      <pubDate>Mon, 19 Oct 2026 14:59:59 +0000</pubDate>
      <link>https://dev.to/mueezafzal/a-stripe-api-upgrade-broke-our-webhooks-and-nothing-errored-1cil</link>
      <guid>https://dev.to/mueezafzal/a-stripe-api-upgrade-broke-our-webhooks-and-nothing-errored-1cil</guid>
      <description>&lt;p&gt;We run subscription billing for a multi-tenant Rails SaaS: 2,000+ businesses, about $1M a year in subscription volume, plus Stripe Connect payouts on top. After we moved to a newer Stripe API version, the scariest kind of bug showed up: nothing broke loudly.&lt;/p&gt;

&lt;p&gt;No exceptions. No failed jobs. Charges just stopped reconciling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;Newer Stripe API versions moved &lt;code&gt;current_period_start&lt;/code&gt; and &lt;code&gt;current_period_end&lt;/code&gt; off the Subscription object and onto each Subscription Item. Our webhook handlers, renewal logic and access checks all read the period from the subscription, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;period_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_period_end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the new API version that field is gone, so the code kept running with &lt;code&gt;nil&lt;/code&gt; instead. Ruby didn't complain, Stripe didn't complain, and our reconciliation quietly skipped work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we didn't notice
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nil&lt;/code&gt; is a valid value in a lot of Ruby code.&lt;/strong&gt; Guards like &lt;code&gt;return unless period_end&lt;/code&gt; turned a data problem into a silent no-op.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four different places read the billing period, each in its own way.&lt;/strong&gt; We had no single place to fix it and no single place to alert on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks fail politely.&lt;/strong&gt; A handler that returns 200 after doing nothing looks exactly like a handler that worked.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. One function for the billing period.&lt;/strong&gt; Every read now goes through one function that prefers the item-level period, falls back to the legacy subscription fields for old payloads, and &lt;strong&gt;raises&lt;/strong&gt; if neither exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;StripeBillingPeriod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;starts_at&lt;/span&gt;  &lt;span class="c1"&gt;# Time&lt;/span&gt;
&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ends_at&lt;/span&gt;    &lt;span class="c1"&gt;# Time&lt;/span&gt;
&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;source&lt;/span&gt;     &lt;span class="c1"&gt;# :item or :subscription&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put a cleaned-up version of this pattern into a tiny open-source gem: &lt;a href="https://github.com/moizafzal936/stripe_billing_period" rel="noopener noreferrer"&gt;&lt;code&gt;stripe_billing_period&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Fail loudly on money paths.&lt;/strong&gt; A missing period should be an exception, not a &lt;code&gt;nil&lt;/code&gt; that slips through.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Payment failures are now under 1% of charge volume, and there's exactly one place to change the next time Stripe moves a field.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone upgrading their Stripe API version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Grep for every read of &lt;code&gt;current_period_*&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, and other fields Stripe has moved. If it's read in more than one place, put it behind one function first.&lt;/li&gt;
&lt;li&gt;Replay real webhook payloads against the new version in staging before switching.&lt;/li&gt;
&lt;li&gt;Keep fixtures for both the old and new payload shapes of every event you handle.&lt;/li&gt;
&lt;li&gt;Reconcile on a schedule instead of trusting webhooks: compare what Stripe says with what you stored.&lt;/li&gt;
&lt;li&gt;Treat "handler returned 200 but changed nothing" as something you watch.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I'm a Rails engineer who mostly works on billing and payouts: Stripe Billing, Connect, and the webhooks in between. Happy to compare notes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>stripe</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
