Stripe Webhook Subscription Entitlement: The Part That Breaks Revenue
If you don't wire the Stripe webhook, paying customers stay on the free plan. Here's the full subscription-plus-credits entitlement loop I run in production.
By Mike Hodgen
Checkout Took the Money. The App Never Found Out.
A few months ago I caught a bug in one of my own SaaS tools that had been quietly costing me money. A customer had paid through Stripe Checkout. Their card was charged. The money was sitting right there in my Stripe dashboard, green checkmark and all.
But inside the app, they were still on the free tier with zero credits.
Nothing was broken in Stripe. The subscription was active. The invoice was paid. Stripe did its job perfectly. The problem was that my application had no idea any of it had happened.
This is the part of stripe webhook subscription entitlement that nobody demos, and it's the part that silently leaks revenue. Stripe Checkout is a hosted page. Its only job is to collect money. It takes the card, charges it, and shows the customer a success screen. It does not reach into your application and flip a switch that says "this person is now a paying customer."
That switch lives in your own database. Stripe calls it money. Your app calls it entitlement, which is just a fancy word for what a user is actually allowed to do. The plan tier they're on. The credits in their balance. The features they can touch.
The only thing connecting Stripe's "they paid" to your app's "they're allowed" is a webhook. A piece of plumbing you have to wire yourself.
When I hadn't wired it correctly, every customer who paid was, as far as my software was concerned, a free user. They handed me money and got nothing in return until I happened to notice. That's not a Stripe failure. That's a missing wire between two systems that were never designed to talk to each other automatically.
Let me walk through how that wire actually works, because once you've operated paid software, you never forget to check for it again.
Why Stripe Checkout and Entitlement Are Two Different Things
The mental model that fixes this: Stripe holds the receipt, your database holds the permission. The webhook is the wire between them.
Stripe holds the receipt, your database holds the permission, the webhook is the wire
What Stripe knows
Stripe is the source of truth for money. Did a card get charged. Is a subscription active. Did it renew this month or did the payment fail. Did the customer cancel. Stripe knows all of this with perfect accuracy, and it will defend that accuracy hard.
What Stripe does not know is anything about your product. It has no concept of your "Pro tier" or your "500 credit pack" or which dashboard a user can open. Those are your invention. Stripe stores a price ID and a subscription status, and that's where its knowledge ends.
What your app needs to know
Your database is the source of truth for entitlement. What plan tier is this org on. How many credits do they have left. Which features are unlocked. None of that lives in Stripe, and it shouldn't.
These two systems are deliberately separate. Stripe will never reach into your app and change a flag unless you build the mechanism that tells it how. That mechanism is the webhook.
Here's the trap most quick builds fall into. They wire the happy-path redirect. The customer pays, Stripe sends them to a success URL, and the app assumes "they hit the success page, so they paid." That assumption is wrong in both directions.
The redirect proves nothing. A user can land on your success page when the payment actually failed. And a user can complete a charge and never reach the success page at all, because they closed the tab, lost connection, or got distracted. The stripe checkout vs entitlement distinction matters precisely because the visible success screen and the real payment are two different events.
The redirect is for the human's eyes. The webhook is for your database.
The Events That Actually Control the Plan
Stripe sends a stream of events. A handful of them are the ones that should actually move data in your tables. This is how you keep your saas plan sync database honest.
The Stripe events that move data and what each one does
checkout.session.completed
This is the first signal that a payment succeeded. When it fires, you attach the Stripe customer ID to the org in your database and provision their initial plan or credit pack.
This event is the moment "an anonymous Checkout session" becomes "this specific customer in my system." If you skip it, you have a paid Stripe customer with no link back to a row in your database, which is exactly the orphaned-payment situation I described at the top.
subscription.created / updated / deleted
customer.subscription.created and customer.subscription.updated are where you set or change the plan tier on the org. Upgrades, downgrades, a trial converting to paid. Every one of those arrives as an updated event, and your handler reads the new price ID and writes the matching tier into your database.
customer.subscription.deleted is the one people forget. When it fires, you revert the org to the free tier and stop their entitlements. Without this, cancelled customers keep full access forever, which is a different kind of revenue leak.
invoice.paid is the renewal heartbeat. It fires every billing cycle, and it's where recurring credit grants land. New month, fresh credits.
The discipline here is simple to state and easy to get wrong: you mirror every relevant event back to your own tables, so your database always reflects Stripe's reality. The handling for subscription tiers and trial-to-paid transitions has real nuance, and I covered that separately in wiring annual billing, trials, and tier gates.
One more thing that bites people. Stripe can and will send the same event twice. Network retries, timeouts on your end, Stripe's own delivery guarantees all mean duplicates happen. So every write your webhook performs has to be safe to replay. If a duplicate invoice.paid grants 500 credits twice, you just gave away inventory. I wrote about how to make these writes safe in atomic, idempotent writes.
Credits Are Just Another Number You Have to Keep Honest
A lot of modern SaaS, especially AI products, meters usage by credit instead of by seat. The billing logic looks different but the principle is identical. This is the stripe credits billing saas pattern.
Credit metering system: balance, audit table, and 402 hard stop
The credit balance and the audit table
The credit balance is just an integer on the org. The webhook tops it up when a credit pack is purchased or a subscription renews. That part is almost boring.
The part you cannot skip is the credit-transactions audit table. Every grant and every deduction gets logged with a reason and a reference. "Granted 500, reason: monthly renewal, ref: invoice_xyz." "Deducted 3, reason: image generation, ref: job_abc."
Without that table you are flying blind. A customer emails asking why they have 12 credits and you have no way to answer. Something looks wrong and you can't reconcile your numbers against Stripe. I've run products without this table early on and the first time a customer disputed their balance, I had nothing to show them. The audit table is not optional once real money is involved.
402 when the tank runs dry
At runtime, when an action would take a customer's credits below zero, you return a 402 Payment Required with an upgrade prompt. You do not silently fail. You absolutely do not let the balance go negative.
The 402 is the most valuable moment in the whole system. A user who just hit zero is a user who wants more of your product right now. That's the moment you convert a maxed-out free experience into an upsell, instead of showing a broken page or a cryptic error.
I dug into the actual pricing math behind credit metering in pricing AI SaaS by the credit, because deciding what a credit is worth is its own problem. But the plumbing is what we're covering here, and the plumbing is: integer balance, audit table, hard stop at zero.
Self-Serve Billing With the Stripe Customer Portal
Once the webhook keeps your database honest, you really don't want to handle every upgrade, downgrade, card update, and cancellation by hand. I did that manually for about two weeks on an early product and it was miserable. Every "can you switch me to annual" email was a chore.
The Stripe-hosted customer portal solves it. You generate a portal session for the logged-in org's Stripe customer ID, send them the link, and they manage their own billing on Stripe's pages. Change the card, switch plans, cancel, view invoices. All of it self-serve.
Here is the insight that ties this back to everything above, and it's the part I think is genuinely elegant.
Every change a customer makes in the stripe customer portal comes back to your app as the exact same webhook events you already handle. They cancel, you get customer.subscription.deleted. They switch plans, you get customer.subscription.updated. They update their card, Stripe handles it and your entitlement logic doesn't even need to care.
So you write zero new handling for the portal. None. The portal is free self-serve billing management precisely because the webhook is already wired.
And the inverse is the warning. If your webhook isn't wired, the portal becomes another way for entitlement to silently drift out of sync. A customer downgrades in the portal, your database never hears about it, and now they're paying for the cheap tier while using the expensive one. The portal only works because the wire works.
The Full Loop, Start to Finish
Here is the entire stripe webhook subscription entitlement loop as a clean sequence:
The full Stripe webhook entitlement loop, start to finish
- The user hits Checkout and pays.
- Stripe fires
checkout.session.completedandcustomer.subscription.created. - Your webhook attaches the Stripe customer ID to the org, sets the plan tier, grants the credits, and logs the credit transaction in the audit table.
- At runtime, your app checks the database (never Stripe) to decide what the user can do.
- When credits hit zero, the app returns a 402 with an upgrade path.
- The user manages billing in the customer portal, which fires more webhook events that keep the database current.
Read that loop carefully and notice what's missing. At no point does your application ask Stripe a live question to decide whether someone is entitled to an action.
That's the whole point. The webhook keeps your database current, so every entitlement read is fast and local. You never make a user wait on a Stripe API call to find out if they can click a button. Stripe pushes the truth to you when it changes, you store it, and your app reads its own tables.
That is the entire reliable billing loop for a real SaaS. Receipt in Stripe, permission in your database, webhook keeping them in agreement.
This Is the Difference Between a Demo and Operated Software
A lot of AI-built SaaS demos beautifully. It collects money in a Stripe sandbox, shows a success screen, and looks finished. Then it never gets stress-tested against the boring part that decides whether revenue actually lands in the product the customer thinks they bought.
Demo vs operated software: the gap the webhook closes
The Stripe webhook is exactly that boring part.
I didn't learn this from a tutorial. I learned it by shipping products with live billing across several of my own SaaS tools, watching a paying customer sit on the free tier with their money already in my account, and tracing the missing wire. That's a specific, slightly sick feeling. Someone trusted you with their card and your software gave them nothing.
Honest take: this isn't hard once you know the events. checkout.session.completed, the subscription lifecycle, invoice.paid, idempotent writes, an audit table, a 402 at zero. That's the list. But you only know to look for it after you've operated paid software, because the demo never reveals it's broken. The sandbox is happy. The customer is not.
If you're shipping AI-generated SaaS, this is one of a handful of unglamorous things that separate a thing that works in a video from a thing that survives real customers. I wrote about the broader version of this in the security pass every AI-built SaaS needs.
When money has to map cleanly to permissions, that mapping has to be exact every single time. I wire it correctly because I run it myself, and I've felt what happens when it's wrong.
Ready to bring AI leadership into your company?
I work with a small number of companies at a time. If you're serious about AI, apply to work together and I'll review your application personally.
Get AI insights for business leaders
Practical AI strategy from someone who built the systems — not just studied them. No spam, no fluff.
Ready to automate your growth?
Book a free 30-minute strategy call with Hodgen.AI.
Book a Strategy Call