Stripe Subscription Billing Implementation: The Real Work
What it actually takes to wire Stripe subscription billing into a consumer AI app: annual prices, 7-day trials, idempotent webhooks, and tier gates.
By Mike Hodgen
The AI Was the Easy Part. Charging for It Wasn't.
I built a consumer AI app over a weekend. The model worked. The thing did what it was supposed to do, gave smart answers, felt like magic. I was proud of it for about three days.
Then I tried to charge people for it.
That's when I learned the lesson nobody tells you: the AI is the easy part. A working Stripe subscription billing implementation took me longer than the intelligence ever did. Days, not hours. And none of it was glamorous.
Here's the buyer doubt I want to kill right now, because I had it too: how much work is it really to charge for an AI product? The honest answer is more than you expect, and almost all of it is plumbing. Pricing tiers that change. Monthly versus annual price IDs. Trials. Webhooks that fire twice. Feature gates that decide who sees what. A sharing flow that, done wrong, leaks customer data.
None of that is AI. It's the boring scaffolding that turns a clever demo into a business.
The mistake founders make is assuming the model is the product. It isn't. The product is the model plus a billing surface that doesn't lose money, doesn't double-charge, and doesn't accidentally give paying customers the free tier. That surface is its own engineering project.
So let me walk you through every layer I had to build. This is the part people skip in the launch tweets. It's also the part that determines whether you actually get paid.
Pricing Tiers Will Change. Build For That.
Four tiers, then three, then renamed
The app I built started with four pricing tiers. Then I cut it to three because nobody understood the difference between two of them. Then I renamed the tiers entirely when the positioning shifted.
That happened over the life of one app. If you hardcode plan names in twenty places, every one of those changes becomes a search-and-replace nightmare where you miss one and break a customer's entitlement.
The lesson: your plan catalog is config, not scattered string literals. One source of truth. A single object that defines every plan, its price IDs, its features, its limits. When pricing changes (and it will), you change one file, not twenty.
My structure looked roughly like this: Pro at $108 a year, top tier at $228 a year, with a monthly equivalent for each. Real ratios, anonymized numbers. Everything else in the app referenced the catalog, never the raw strings.
Why per-seat pricing breaks for AI
Most SaaS pricing tiers are per-seat. Ten users, ten times the price. That model is clean because cost scales with headcount.
AI breaks it. Your cost scales with usage, not seats. One power user can burn more tokens than fifty light users combined. If you charge per seat, your heaviest users are your least profitable, and you can't see it coming.
I go deep on this in my piece on metering AI usage by the credit, but the short version is: seat-based pricing doesn't map cleanly to AI cost. You need a model that tracks consumption.
On the billing structure itself, annual matters more than people think. Annual plans pull cash forward and cut churn hard, because someone who paid for a year doesn't cancel after a rough month. I made annual the default toggle for exactly that reason. Monthly is for the hesitant. Annual is where the business lives.
Multiple Stripe Price IDs Per Plan (Monthly and Annual)
The billing param that threads everything
Here's where the plumbing gets real. Each plan needs at least two Stripe price IDs: one for monthly, one for annual. The same Pro plan has two prices behind it.
The Billing Param Threading Through the System
That means your pricing page toggle (monthly/annual) isn't just a UI flourish. It has to pass a billing param all the way through the system. Toggle to checkout route, checkout route to the Stripe Checkout session, Checkout session into the webhook.
If that param drops anywhere in the chain, you charge the wrong amount or grant the wrong access. The toggle is UI. The param is plumbing. And a broken param means a wrong entitlement, which means an angry customer or a refund.
Mapping annual price IDs back to plans
Now the webhook fires. Stripe tells you a subscription was created with price ID price_abc123. Your job is to map that ID back to your internal plan so you know what to unlock.
Price-ID-to-Plan Mapping (the missing annual ID failure)
This is where people lose data. They build a lookup table from price ID to plan, carefully map all the monthly IDs, and forget the annual ones. So a customer pays for an annual Pro plan, the webhook gets an unrecognized price ID, the mapping returns nothing, and the customer gets silently dropped to free.
They paid you $108. They got the free tier. They find out when a feature is locked, and now you're issuing apologies instead of collecting revenue.
The fix is simple and you have to do it deliberately: your price-ID-to-plan lookup must include every price ID, monthly and annual, for every plan. When you add a new plan or a new billing interval, you add both IDs to the map in the same commit. Treat the annual ID as a first-class citizen, never an afterthought.
7-Day Trials Across Paid Plans
I applied a 7-day trial across every paid plan. That sounds trivial. It is not.
Trial Subscription Lifecycle and Webhook Events
For the stripe trial setup itself, you set the trial period on the Checkout session or the subscription. Either works, but be consistent. I set it on the Checkout session so the trial is part of the same flow that captures the card.
The important decision: a trialing customer should have full access. Treat them as fully paid for entitlement purposes. The whole point of a trial is they get to use the real thing. If you gate trial users to a degraded experience, they never see the value and they never convert.
Here's the gotcha that bit me. A trialing subscription fires different webhook events than an active paid one. If you only listen for the "payment succeeded" event, your trial users get no access at all, because no payment has happened yet. They signed up, they're inside the trial, and your system thinks they don't exist.
You have to listen for the subscription-created and subscription-updated events, not just the payment ones. The subscription object tells you the customer is in a trialing state, and that's your signal to grant access.
Then handle the edges honestly:
- Card fails after trial. The trial ends, Stripe tries to charge, the charge fails. You need to catch the failed-payment event and revoke access (or grace-period it, your call).
- Cancel during trial. The customer bails before day 7. No charge, access ends at trial expiry. Don't bill them.
- Trial-to-paid conversion. The clean path. Trial ends, card charges, subscription goes active. Your webhook already handles active subscriptions, so this just works if you built the rest right.
Trials are easy to demo and easy to get wrong in the events. Wire the event handling first.
Idempotent Webhooks or Your Books Lie
Why Stripe sends the same event twice
Stripe will deliver the same webhook event more than once. Network hiccups, retries, timeouts on your end. This isn't an edge case, it's normal operation. Stripe retries to be safe, and that safety becomes your problem.
If you process a subscription-created event twice, you can double-grant access, send two welcome emails, or corrupt your subscription state. I've seen books that lie because a webhook ran twice and counted one signup as two.
A stripe_events table as the dedup key
The fix is stripe webhook idempotency, and it's not complicated. You keep a stripe_events table keyed on the Stripe event ID, which is unique per event.
Idempotent Webhook Dedup Pattern
Before you process anything, you check: have I seen this event ID? If yes, skip it entirely. If no, insert it and then process. Check-then-insert, before any side effects.
This makes every webhook handler safe to replay. Stripe can send the same event five times and your system processes it exactly once. No double-grants, no double-emails, no corrupted state.
There's a bigger principle underneath this, and I wrote a whole piece on wiring the Stripe webhook as your source of truth. The short version: the webhook is the source of truth for entitlement. Not the client-side checkout success page.
Why does that matter? Because customers close the tab. They pay, the Checkout completes, and then their browser crashes or they navigate away before your success page loads. If your access grant lives on that success page, they paid and got nothing.
The webhook fires regardless of what the customer's browser does. Grant access in the webhook, every time. If you don't wire this correctly, people who paid you stay on the free plan, and you won't even know until they complain.
Plan-Aware Feature Gates and a Top-Tier Sharing Unlock
Gating features by plan
Once you know a customer's plan, you have to enforce it. Every gated feature checks the customer's current plan before it runs. Not in the UI. On the server.
Hiding a button in the UI is not a feature gate. Anyone can hit your API directly. The check that matters is server-side: when the request comes in, you look up the plan, and you deny the action if the plan doesn't include it.
This is where pricing tiers stop being marketing and become real money. The gate is the difference between someone paying for the top tier and someone using top-tier features for free.
Caregiver-style sharing with RLS-backed permissions
The app had a top-tier-only feature: a sharing flow. A paying top-tier user could invite another person by email, a caregiver or a collaborator, and grant them access to specific shared content.
Feature Gate plus RLS for Top-Tier Sharing
This is one of the most dangerous features to build, because a sharing flow done wrong is a data leak. If the invited person can see anything beyond exactly what they were granted, you've exposed customer data.
I backed it with permission tables and row-level security. The invited person's queries are constrained at the database level so they only ever see the records explicitly shared with them, and nothing else. The security isn't enforced in application code where a missed check leaks everything. It's enforced in the database itself.
I lay out how I do this across complex schemas in my row-level security playbook. For a sharing feature, RLS is not optional. It's the layer that makes "share with this one person" actually mean "this one person and no one else."
And the gate ties it together: only top-tier customers can invite. The feature gate decides who reaches the sharing flow, RLS decides what the invitee sees. Both server-side. That's how a pricing tier turns into protected, paid functionality.
What This Costs You as a Founder (and Why It's Worth Owning)
Let me sum up the real cost. The billing surface for a consumer AI app is days of work, not hours. And I'll say it again: none of it is the AI.
The pattern that keeps it sane is the one I built every layer around. Rent the primitive, build the logic. You don't build a payment processor, you rent Stripe. But the plan catalog, the param threading, the price-ID mapping, the idempotent webhooks, the entitlement gates, the RLS-backed sharing, all of that is logic you own. I wrote about that division of labor in pay for primitives, build the logic.
Here's the honest part: this work is never truly done. Pricing keeps changing. I went from four tiers to three to renamed tiers, and each change had to be absorbed cheaply because the catalog was config. If you hardcode it, every pricing change is a project. If you build it right, it's a one-file edit.
This is the layer most founders underestimate. They spend the weekend on the model and then get blindsided by the billing. It's exactly the kind of unglamorous plumbing I build before the AI ever ships, because a brilliant model that can't charge customers correctly isn't a business.
If you're staring at a product that works but can't reliably take money, that's a solvable problem. It's mostly the stuff above, done carefully.
Want to explore what AI could do for your business?
Book a free 30-minute strategy call. No pitch deck, no sales team, just a real conversation about your operations and where AI fits.
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