<?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: Jake Hoffman</title>
    <description>The latest articles on DEV Community by Jake Hoffman (@jake_hoffman).</description>
    <link>https://dev.to/jake_hoffman</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%2F3963720%2F10114758-dd7e-4adf-8b06-d56928f570fd.png</url>
      <title>DEV Community: Jake Hoffman</title>
      <link>https://dev.to/jake_hoffman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jake_hoffman"/>
    <language>en</language>
    <item>
      <title>Stripe annual subscriptions: how to recognize the revenue correctly, with the actual journal entries</title>
      <dc:creator>Jake Hoffman</dc:creator>
      <pubDate>Wed, 05 Aug 2026 13:13:15 +0000</pubDate>
      <link>https://dev.to/jake_hoffman/stripe-annual-subscriptions-how-to-recognize-the-revenue-correctly-with-the-actual-journal-entries-14eb</link>
      <guid>https://dev.to/jake_hoffman/stripe-annual-subscriptions-how-to-recognize-the-revenue-correctly-with-the-actual-journal-entries-14eb</guid>
      <description>&lt;p&gt;A customer pays $1,200 for a year of your SaaS. Stripe takes its cut and drops the rest in your balance. So how much of that $1,200 is revenue this month?&lt;/p&gt;

&lt;p&gt;If you answered $1,200, your books are wrong, and an accountant will tell you so. You collected the cash, but you haven't earned it yet. You owe the customer twelve months of service, and the revenue is earned as you deliver it. Booking the whole year in January overstates that month and leaves the next eleven looking empty. This is revenue recognition, and for annual plans it's the part of Stripe bookkeeping people get wrong most often.&lt;/p&gt;

&lt;p&gt;Here is how to do it correctly, with the real entries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cash lands in a liability, not revenue
&lt;/h2&gt;

&lt;p&gt;When the annual invoice is paid, the money you collected is a liability until you earn it. It goes to an account called Deferred Revenue. A $1,200 annual plan, after Stripe's processing fee:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dr  Stripe Clearing        $1,164.00
Dr  Stripe Processing Fees    $36.00
Cr  Deferred Revenue               $1,200.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth noticing. The Stripe fee is a business expense, on its own line, not netted against revenue, so your top line still reflects what the customer actually paid. The cash that reaches your balance is the amount minus that fee, which is why the clearing account gets $1,164 and not the full $1,200. The credit is the full $1,200, and it goes to Deferred Revenue, a liability, because right now you owe a year of service and have earned none of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then you recognize it, one month at a time
&lt;/h2&gt;

&lt;p&gt;Each month, as you deliver the service, you move one twelfth from the liability into actual revenue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(every month, for twelve months)
Dr  Deferred Revenue        $100.00
Cr  Subscription Revenue           $100.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve of those, dated one per month, and the liability drains to zero exactly as the year completes. That's all revenue recognition is: cash up front sitting in a liability, released to revenue as it's earned. Accountants call it ratable recognition, and it's what ASC 606 and IFRS 15 ask for. Your monthly income statement now shows the $100 you actually earned, instead of a $1,200 spike followed by eleven flat months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five details that trip people up
&lt;/h2&gt;

&lt;p&gt;The concept is simple. The places it goes wrong are in the details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sales tax is not revenue.&lt;/strong&gt; If Stripe Tax collected tax on the invoice, that money isn't yours. You're holding it for the tax authority. It comes out to its own liability at the moment of collection, and it is not deferred, because you owe it now regardless of when you earn the revenue. A $1,000 plan plus $100 of tax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dr  Stripe Clearing        $1,067.00
Dr  Stripe Processing Fees    $33.00
Cr  Sales Tax Payable                $100.00
Cr  Deferred Revenue               $1,000.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the $1,000 gets a recognition schedule. The $100 of tax sits in its liability until you remit it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The schedule has to reconcile to the penny.&lt;/strong&gt; $1,000 over twelve months is $83.33 a month, and twelve times $83.33 is $999.96. If you post $83.33 twelve times, four cents are stranded in Deferred Revenue and the account never returns to zero. The fix is to let the last month absorb the remainder: eleven months of $83.33 and a final month of $83.37, which sums to exactly $1,000. It's a small thing that quietly breaks a lot of homegrown schedules, and it's the first place I'd look if your deferred revenue balance won't clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A mid-term plan change replaces the remaining schedule.&lt;/strong&gt; Suppose the customer upgrades in month five and Stripe creates a paid proration invoice. The months already recognized stay unchanged. Combine the deferred balance still left from the old invoice with the new deferred amount, then spread that total over the dates left in the original term. Do not keep releasing the old $100 while also starting a second full-length schedule from the change date, because that second schedule can run past the contract end. A credit note works in the other direction: reduce the remaining deferred balance and re-spread what is left. The invoice or credit note is the accounting event. A &lt;code&gt;customer.subscription.updated&lt;/code&gt; notice by itself is not enough, because Stripe can send it for a change that moves no money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cancellation stops future recognition, but does not decide the money entry.&lt;/strong&gt; Keep recognizing revenue through the customer's actual last day of service, then stop every later month. Do not wait for a credit note before stopping the schedule. A normal end-of-term cancellation may have no credit note because the customer simply uses the time they already paid for. An early refund, credit note, or proration is a separate money event and should be booked on its own. If deferred revenue remains after service ends, have an accountant decide how that balance should be cleared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A one-time fee needs its own check.&lt;/strong&gt; Being billed once does not always mean the fee is earned right away. If the customer receives a separate service, such as a finished consulting session, you may recognize that fee when you deliver it. If the fee only covers work that gets the customer ready for the annual service, ASC 606 and IFRS 15 often treat it as an advance payment for that future service. Recognition depends on what the customer receives, not only on how the Stripe line item is dated. Ledgerly treats an instant-period line as earned now, so use that shape only when the line truly represents a separate service and have an accountant check the setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  You shouldn't be doing this by hand
&lt;/h2&gt;

&lt;p&gt;The mapping from a Stripe event to these entries is deterministic. The same invoice always produces the same journal entry, which makes it exactly the kind of work that should be automated instead of reconciled by hand every month.&lt;/p&gt;

&lt;p&gt;That's the problem I built Ledgerly for. It's an open-source TypeScript engine that turns Stripe webhook events into these balanced entries and exports them to QuickBooks Online or Xero, including the twelve-month schedule, the tax split, and the rounding remainder above. Its self-hosted receiver also rebuilds the remaining same-currency schedule after a paid mid-term upgrade, draws it down when a later credit note arrives, and stops future recognition after Stripe reports the customer's actual last service date. Refunds, credit notes, and prorations remain separate money events. It's Apache-2.0 and self-hostable, so it runs on your own infrastructure with no third party sitting in your financial data.&lt;/p&gt;

&lt;p&gt;You can watch it map a real annual invoice in about ten seconds, with no install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;invoice.json | npx ledgerly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pipe any Stripe event in and it prints the entry it produces, or the exact QuickBooks or Xero JSON with &lt;code&gt;--qbo&lt;/code&gt; or &lt;code&gt;--xero&lt;/code&gt;. There are sample events in the repo if you don't have one handy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you ship it
&lt;/h2&gt;

&lt;p&gt;Chart of accounts and tax treatment vary by business and by jurisdiction, so run your setup past an accountant before you rely on it. The mechanics above are standard, but the account names and the tax handling depend on where you are and how you're registered.&lt;/p&gt;

&lt;p&gt;If you keep books for a SaaS and any of these entries looks wrong to you, I'd genuinely like to hear it. Getting the accounting right is the whole point, and the corner cases are where it's easy to be quietly off.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/jakethehoffer/ledgerly" rel="noopener noreferrer"&gt;https://github.com/jakethehoffer/ledgerly&lt;/a&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>stripe</category>
      <category>accounting</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I had Claude build a real fintech library in two weeks. Now I want it audited.</title>
      <dc:creator>Jake Hoffman</dc:creator>
      <pubDate>Tue, 02 Jun 2026 16:14:27 +0000</pubDate>
      <link>https://dev.to/jake_hoffman/i-had-claude-build-a-real-fintech-library-in-two-weeks-now-i-want-it-audited-1e3l</link>
      <guid>https://dev.to/jake_hoffman/i-had-claude-build-a-real-fintech-library-in-two-weeks-now-i-want-it-audited-1e3l</guid>
      <description>&lt;p&gt;I wanted to know whether an AI coding agent could build something real instead of a prototype, so I gave it the least forgiving problem I could find. Accounting. Turning Stripe's webhook events into proper double-entry bookkeeping you could hand to QuickBooks or Xero.&lt;/p&gt;

&lt;p&gt;Accounting is a good test because there's nowhere to hide. The debits equal the credits or the entry is wrong. An accountant either recognizes what you booked or they don't. There is a right answer and you can't argue your way to it.&lt;/p&gt;

&lt;p&gt;Two weeks later I have Ledgerly. It's on npm, it's Apache-2.0, and I'm not fully sure every entry is correct, which is the reason I'm writing this.&lt;/p&gt;

&lt;p&gt;Most days I gave Claude a single instruction and let it pick the most useful next thing to build. It read what already existed, chose something, and implemented it. I made the calls at the real forks, approved or rejected the accounting decisions, and pushed back when an entry looked off. I didn't write the code. My job was deciding what to build and checking whether it was right.&lt;/p&gt;

&lt;p&gt;Fifteen releases later it does more than I expected going in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;13 Stripe event types: charges, refunds, disputes, payouts, invoices.&lt;/li&gt;
&lt;li&gt;Revenue recognition. Pay a year up front and it books the cash to deferred revenue, then recognizes it across twelve months. The monthly pieces reconcile back to the original amount exactly.&lt;/li&gt;
&lt;li&gt;Sales tax kept as its own liability instead of mixed into revenue, and refunds draw it back down proportionally.&lt;/li&gt;
&lt;li&gt;Currency handling. Entries post in whatever currency your Stripe balance settled in, and it records a realized gain or loss when the rate moved between a charge and a later refund.&lt;/li&gt;
&lt;li&gt;Exporters for QuickBooks Online and Xero.&lt;/li&gt;
&lt;li&gt;A webhook server with signature verification, dedup, SQLite storage, a retry queue that dead-letters what keeps failing, and OAuth for both platforms.&lt;/li&gt;
&lt;li&gt;A Docker image and an npm package, both with signed build provenance.&lt;/li&gt;
&lt;li&gt;589 tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The moment that stuck with me was about payouts. When Stripe pays you into a bank account held in a different currency than your balance, it converts the money and takes a fee, and the bookkeeping for that fee is genuinely fiddly. I expected the agent to wing it, since making things up is the failure everyone warns about. It didn't. It told me it couldn't model that case correctly without real example payloads, wrote up what it would need to see, and left the code refusing those payouts with a clear error rather than posting a number it couldn't defend. I've worked with people who would have shipped the guess.&lt;/p&gt;

&lt;p&gt;Is the accounting right? I think it mostly is. The entries balance, the recognition schedule reconciles, there's a test behind every case, and there's a document in the repo that explains the reasoning for each entry so you can check the logic without reading the code. But me being fairly sure is not the same as an accountant confirming it, and I won't pretend it is.&lt;/p&gt;

&lt;p&gt;That's the ask. If you keep books for a SaaS, or you know the corners of the Stripe API, or you just like finding the thing that's broken, go break it. It's npm install ledgerly and the core is small enough to read in one sitting.&lt;/p&gt;

&lt;p&gt;What surprised me most had nothing to do with the code. Writing it was never the constraint. The constraint was knowing what to ask for, and knowing what correct looked like in a field where I'm not the expert. The agent produced every line, but I still had to decide what a refund does to a tax liability and whether revenue recognizes monthly, and some of the most useful moments were when the right answer was to admit I didn't know yet. I'd rather you check it than take my word for it.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/jakethehoffer/ledgerly" rel="noopener noreferrer"&gt;https://github.com/jakethehoffer/ledgerly&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>typescript</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
