I've now been through three major iterations of Stripe integration in Codelit. The first took 20 minutes. The second took a week. The third, fixing all the edge cases I missed, took another three weeks.
A Stripe SaaS integration
Here's every lesson, every gotcha, and the pricing mistake that cost me real money.
How long does a Stripe integration actually take?
A working Stripe Checkout session takes about twenty minutes. Production-ready billing — idempotent webhooks, trial expiry, card declines, proration — took me three iterations, and the last pass alone was three weeks of edge cases.
The fast part is genuinely fast. Create a product in the Stripe dashboard, grab your API keys, and:
Twenty minutes, and you've got working payments. Ship it, right?
Don't ship it. There are about fifteen things that will break in production.
Why do Stripe webhooks fire the same event twice?
Stripe webhooks fire more than once because Stripe retries any delivery it cannot confirm. As of Stripe's webhook documentation in July 2026, live-mode deliveries are retried for up to three days with exponential backoff, and sandbox deliveries three times over a few hours. Stripe's own wording is that an endpoint "might occasionally receive the same event more than once". I saw the same invoice.paid event hit my endpoint three times in one minute.
When I first wrote this I described the retry policy as a vague maybe. It isn't. It's a documented three-day window, which means a handler that isn't idempotent has three days to do damage.
Stripe communicates back to your app this way. Customer subscribes? Webhook. Payment fails? Webhook. Customer cancels? Webhook. Which means the reliability of your billing is really the reliability of one HTTP handler.
The fix is idempotency. Store the event ID and skip duplicates.
That processedEvents collection has saved me from double-charging customers at least a dozen times. All four event names in that switch — checkout.session.completed, invoice.paid, invoice.payment_failed and customer.subscription.deleted — are still current, non-deprecated types in Stripe's event reference as of July 2026.
What breaks between Stripe test mode and live mode?
Stripe test mode and live mode are separate environments with separate webhook endpoints, separate customer IDs, separate rate limits, and test clocks that exist in only one of them. All four differences cost me something. One cost me a whole day.
| What differs | Test mode (sandbox) | Live mode | What it cost me |
|---|---|---|---|
| Webhook endpoints | Configured and verified here first | Must be configured again, separately | A day of untracked production subscriptions |
| Test clocks | Fast-forward a trial in seconds — the Dashboard now files these under Simulations | Do not exist — sandbox only, so you wait actual human days | Any trial-expiry bug you miss ships, then hides for a week |
| Customer IDs | A cus_… ID that looks exactly like a live one | A different cus_… ID entirely | Stored IDs that resolve to nothing in the other environment |
| Rate limits | 25 requests/second overall | 100 requests/second overall, 25 per endpoint | Limits I hit at production volume that low-traffic testing never reached |
The rate-limit row is the one I had backwards when I first wrote this. I assumed live mode was the tighter environment; Stripe's rate-limit docs in July 2026 say the opposite, with sandboxes capped at a quarter of live mode's throughput. A load test that throttles in a sandbox may be telling you nothing about production.
The webhook endpoint is the embarrassing one. I set up the test endpoint, tested everything, shipped, then spent a day wondering why subscriptions weren't being tracked in production. The events were firing. Nothing was listening.
What should happen when a Stripe trial ends without a card?
When a Stripe trial ends and the customer has no saved payment method, cancel the subscription instead of invoicing it. Codelit gives users Pro features free for 7 days, and trial_settings.end_behavior decides what happens to everyone who never came back. As of July 2026 the missing_payment_method field still takes three values: cancel, create_invoice and pause.
Cancelling is the clean outcome. Failed invoices clutter your Stripe dashboard and create confusing states in your app.
How should a SaaS handle a failed subscription payment?
A failed subscription payment should degrade access in stages, not revoke it. Stripe's Smart Retries re-attempt the charge over the days that follow — as of July 2026 the recommended default policy is eight attempts inside a two-week window — and a card declined for insufficient funds on Monday is often a successful charge later that week.
First instinct: payment fails, lock the user out immediately.
Don't do this. You'd be cutting a customer off during the exact window in which Stripe is still trying to get them to pay you.
This is the paragraph that aged worst. I originally wrote that Stripe retried immediately and then at 3, 5 and 7 days, which is the shape of a fixed custom schedule, not Smart Retries. Smart Retries doesn't work that way: you choose a retry count and a window (1 week through 2 months) under Billing → Revenue recovery → Retries in the Dashboard, and Stripe's model picks the moments inside it. If you want fixed days, you turn Smart Retries off and configure up to three retries by hand.
Here's the ladder Codelit uses instead.
| Stage | What Stripe is doing | What Codelit does | What the user sees |
|---|---|---|---|
| First failure | Smart Retries begins — Stripe picks the attempt times inside the window you configured | Marks the account past_due, keeps full Pro access | A banner asking them to update their card |
| After 3 days | Still retrying somewhere inside that window | Degrades the account to free-tier features | A more urgent banner |
| Once retries are exhausted | Subscription moves to canceled or unpaid, or stays past_due, depending on the setting you chose | Downgrades to the free plan | Subscription paused until the card is updated |
This approach recovers about 30% of failed payments without any manual intervention. Locking users out immediately would've lost all of those.
Keeping Stripe and Firestore in sync
Codelit stores subscription state in Firestore and treats Stripe as the writer of record: a Stripe webhook hits a Cloud Function, the function updates the user document, and the client reads plan status from Firestore rather than asking Stripe anything directly.
Firebase for auth and Firestore for data is part of the stack I walk through in the full story of building Codelit.
The real-time listener is the part worth copying. When a user completes checkout and is redirected back to Codelit, the Firestore update from the webhook usually beats the redirect by a few hundred milliseconds. The user lands on the success page and their Pro features are already enabled. No polling, no "please wait while we activate your subscription" spinner — the same instinct that runs through DateSafe, where the whole design rests on a millisecond of extra spinner mattering.
Is the Stripe Customer Portal worth adding?
The Stripe Customer Portal is worth adding on day one. It's a page Stripe hosts and maintains where users manage their own billing — update card, view invoices, cancel, switch plans — and wiring it up is one call against a stored customer ID.
One caveat that still holds in July 2026: you define what the portal is allowed to do in the Dashboard before the API call works, under Settings → Billing → Customer portal, and that configuration is set separately for sandboxes and for live mode. Same trap as the webhook endpoints.
One line of code on top of that config, and you eliminate 80% of billing-related support emails. Before I added the portal, I was getting 3-4 emails a week asking to update cards, cancel subscriptions, or get invoice copies. Now I get maybe one a month, and it's usually something the portal can't handle (like a refund request).
The pricing mistake
Codelit launched at $19/month and converted 1.2% of free users to paid. The price wasn't wrong for the product. It was wrong for the audience: indie developers and students, a huge part of my user base, don't spend $19/month on tools they're still evaluating.
$19 felt right when I set it. Competitive with similar tools, covers the AI API costs, leaves margin for growth. Every one of those reasons was about my costs, not about the person deciding whether to get their card out.
They'd use the free tier, hit the limits, and bounce. The ones who stuck around were team leads at companies with tool budgets.
I restructured:
- Free tier: 10 generations/day signed in, the free model pool, all templates, every export format
- Pro: $12/month, unlimited runs, premium models, all exports, all templates
- Team: $10/user/month, shared workspaces, admin controls
Dropping to $12 helped, but the real unlock came later when I took Pro down to $5/month with a 7-day trial. That's low enough that indie devs don't think twice, and the trial lets them feel the Pro models before the card gets charged. Conversion jumped to 4.8%.
| Price point | Free-to-paid conversion | What I got wrong |
|---|---|---|
| $19/month at launch | 1.2% | Priced for team leads with a tool budget. My users were indie devs and students. |
| $12/month | Better, but not the unlock | Cheaper softened the decision without changing it. |
| $5/month with a 7-day trial | 4.8% | Nothing — this is the one that worked. The trial proves the Pro models before the charge. |
Pricing is one of several things I got wrong in public, and the rest of them are in the honest growth story of what worked and what flopped. Check the current pricing at the pricing page.
What actually makes a free user upgrade?
Three things drive Codelit upgrades, and after tracking hundreds of conversions they haven't moved: hitting the daily generation limit, wanting Terraform and Kubernetes export, and wanting the premium models.
- Hitting the daily generation limit. This is the #1 trigger. Users run out of their 10 free generations and want to keep going.
- Wanting Terraform/K8s export. Pro-only feature. Engineers who want to turn diagrams into real infrastructure code pay for it.
- Wanting the premium models. The free tier runs on the free model pool. Pro unlocks premium models like GPT-5 and Claude, served through the multi-provider fallback layer that stops one dead provider becoming a dead product, and they produce noticeably better architecture diagrams.
Notice what's not on the list: feature gates, nag screens, countdown timers. The product sells itself when users hit a natural boundary. No dark patterns needed.
The proration problem
Proration is Stripe charging or crediting the difference when a subscription changes mid-period — monthly to annual, Pro to Team. Stripe calculates it for you if you set proration_behavior, then hands you webhook events that look nothing like a normal billing cycle. As of July 2026 that parameter still takes create_prorations, always_invoice or none.
You'll get invoice.created, invoice.finalized, and invoice.paid events that don't look like normal subscription invoices. Worth knowing about the timing, which I originally glossed over: create_prorations is the default, and per Stripe's July 2026 proration docs it writes proration invoice items that are only billed immediately under specific conditions — otherwise they ride along on the next scheduled invoice. If you want the charge to land the moment the plan changes, that's always_invoice.
I added explicit handling for prorated invoices so that a mid-cycle plan change doesn't get read as a renewal and quietly rewrite the user's plan status.
What I'd tell past me
If I rebuilt Codelit's billing tomorrow, six decisions would come before any product code — and all six are cheap on day one and expensive in month three.
- Set up webhooks in both test and live mode on day one.
- Implement idempotency before anything else.
- Don't lock users out on first payment failure.
- Start with a lower price point and increase later.
- Customer Portal from day one. It's one line of code plus a Dashboard configuration, in both environments.
- Store the Stripe customer ID and subscription ID on your user record. You'll query them constantly.
Stripe is genuinely good software. But the gap between "working checkout" and "production-ready billing" is wider than you'd expect. Budget a week for it, and spend most of that week on the paths where something has already gone wrong.
Questions people actually ask
- How long does it take to integrate Stripe into a SaaS product?
- A basic Stripe Checkout session takes about twenty minutes to build. Production billing takes far longer. Across three iterations of Codelit's integration, the first took twenty minutes, the second took a week, and the third, which existed only to fix edge cases, took another three weeks. Budget a week if you want checkout, trials, webhook retries, card declines and proration all handled properly.
- Why do Stripe webhooks need an idempotency check?
- Stripe retries deliveries it cannot confirm. As of July 2026 its webhook documentation says live mode retries run for up to three days with exponential backoff, and that an endpoint might occasionally receive the same event more than once. On Codelit I watched a single invoice paid event arrive three times in one minute. Without an idempotency check that stores every processed event ID and skips repeats, a retried event can double-charge a customer or apply the same subscription change twice.
- What should happen when a Stripe trial ends and there is no payment method?
- Cancel the subscription. As of July 2026 Stripe's subscription API offers three outcomes for a missing payment method at trial end — cancel, create an invoice, or pause — and cancelling is far cleaner than invoicing. Choosing invoice creation leaves you with failed invoices from people who never added a card, and every one of them fires a payment failure webhook and retry emails to users who already forgot your product exists.
- Should you cut off access when a subscription payment fails?
- No. Stripe Smart Retries re-attempt a failed charge over the days that follow, and as of July 2026 the recommended default policy is eight attempts inside a two-week window, with a model picking the timing rather than a fixed schedule. A card declined on Monday often succeeds later that week. Codelit shows a banner on the first failure while keeping full access, degrades to free-tier features after three days, and downgrades only once the retries are exhausted. That recovers about 30% of failed payments with no manual work.
- Is the Stripe Customer Portal worth setting up?
- Yes, and once it is configured it costs one API call. The Customer Portal is a page Stripe hosts and maintains where customers update their card, download invoices, cancel, and change plans. As of July 2026 you still define what it allows in the Stripe Dashboard first, separately for sandbox and live mode. Adding it to Codelit removed around 80% of billing-related support email. Before the portal I answered three or four of those emails a week. Afterwards it dropped to roughly one a month.
- How much should a developer tool charge per month?
- Price for the audience, not for your cost base. Codelit launched at $19 a month and converted 1.2% of free users, because indie developers and students do not spend that on a tool they are still evaluating. Moving Pro to $5 a month with a 7-day trial took free-to-paid conversion to 4.8%. The trial mattered as much as the price cut, because it let people feel the paid models before the card was charged.