9:00 PM — Monday.

VS Code glowing on my screen. I’d just used Claude Code to build the entire core of my new SaaS — a subscription management tool for Vietnamese creators. Next.js running smooth. Database schema clean. Auth flow working. Landing page ready.

The feeling? Euphoric. I told myself: “This is it. Just plug in payment tomorrow and we’re live.”

“Just plug in payment and we’re live.”

If you’re an indie hacker and you’ve ever said this — you know what’s coming.


The Abyss Called Stripe Docs

I opened the Stripe documentation. 50 tabs within the first 10 minutes.

Checkout Sessions. Webhooks. Product IDs. Price API. Customer Portal. Billing Meter. Subscription Lifecycle. Invoice Events.

I’m a Software Engineer with over 5 years of experience. I’m not afraid of hard code. But I hate — truly hate — repetitive, tedious work where one wrong step breaks everything.

And payment integration is exactly that.


The “Pay” Button Is Not a Button

Here’s what nobody tells you when you start building a SaaS: the “Pay” button is not a button. It’s a system.

To make that button work, I had to:

1. Set up Stripe CLI to catch webhooks locally.

Terminal window
stripe listen --forward-to localhost:3000/api/webhooks/stripe

Sounds simple? Sure, until you realize the CLI needs login, login needs an API key, API keys have test mode and live mode, and if you mix them up everything runs but no events get sent. You spend 40 minutes debugging only to discover you were using the wrong key.

2. Write the webhook handler — where everything actually happens.

Payment succeeded? Update the database. Card expired? Send a reminder email. Subscription cancelled? Revoke access. Refund? Restore credits.

Each event is an if block. Each if block needs its own logic. Each logic needs its own tests.

// This is just checkout.session.completed
// There's also invoice.paid, invoice.payment_failed,
// customer.subscription.updated,
// customer.subscription.deleted...
// Each one is its own story.

I counted 12 event types that need handling for a basic subscription flow. Twelve.

3. Configure environment variables.

STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_ID_MONTHLY=price_...
STRIPE_PRICE_ID_YEARLY=price_...

Five variables. Get one character wrong — no clear error message. Stripe just returns 400 Bad Request, or worse — 200 OK but no session created. You have no idea what’s wrong.


Day Two — Webhook Hell

Tuesday. I started writing the webhook handler.

First problem: signature verification. Stripe sends events with a signature so you can verify they’re real, not someone spoofing them. The verification logic is different between App Router and Pages Router in Next.js. I use App Router. Stripe’s example docs use Pages Router.

Copy-paste didn’t work. Stack Overflow answers from 2 years ago. Blog posts using a different library version.

I spent 3 hours just to parse the request body correctly.

Second problem: idempotency. Stripe can send the same event multiple times. If you don’t handle duplicates, a user pays once but your database records it twice. Or worse — sends two confirmation emails. Or even worse — creates two subscription records.

I never thought about this edge case until it happened on staging.


Day Three — The Real Cost

Wednesday. Day three.

The webhook handler was working. Database updating correctly. But I hadn’t handled:

  • Customer Portal — where users self-manage cancel/upgrade
  • Proration — calculating charges when users upgrade mid-cycle
  • Trial periods — 7-day free trial before charging
  • Tax calculation — different taxes by country
  • Failed payment retry — Stripe auto-retries but you need to track status

Each item on that list was another 2-4 hours. And I hadn’t even started testing.

But what really killed me wasn’t the volume of code. It was the context switching. Stripe Dashboard to create a product. Back to VS Code to write the handler. Jump to terminal to run the CLI. Open the browser to test checkout. Back to Dashboard to check the event log. Back to VS Code to fix the bug.

Every switch cost 5-10 minutes to “reload” my brain. The flow state I had while building features? Gone completely. Payment integration doesn’t just cost time — it kills your ability to focus.

I stared at VS Code at 11:30 PM Wednesday. The actual product — the core I was excited to build — had been done since Monday night. Three days had passed and I hadn’t written a single line of feature code. Three days just to make a “Pay $9/month” button work.

That’s when I thought about quitting.


Not the First Time

And here’s the most painful part: this wasn’t the first time.

SaaS project number one — a small analytics tool. I spent 4 days on payment. Launch delayed by a week. Momentum lost. Project died.

Project number two — a marketplace for Vietnamese freelancers. This time I was “smarter,” copying payment code from the previous project. But the old project used Pages Router, the new one used App Router. Copy-paste introduced bugs. Lost another 2 days fixing them. Then discovered Stripe had deprecated an API the old code relied on. Rewrote from scratch.

Three projects. Same pattern every time:

Week 1: Build features at lightning speed with AI
Week 2: Stuck on payment integration
Week 3: Exhausted, lost momentum, quit

The problem isn’t Stripe. Stripe is an excellent product. Well-designed API. Comprehensive documentation. Responsive support team.

The problem is boilerplate. It’s having to write the same webhook handlers, checkout flows, and customer portal code — for every single project. It’s making new mistakes every time because the context has changed — new framework version, deprecated API, different patterns.

AI helped me code features in hours. But AI couldn’t help me skip 12 webhook events to handle, 5 ENV vars to get right, and 20 edge cases to test.


The Question I Wish Someone Had Asked Me Sooner

At 11:30 PM Wednesday, before I closed my laptop, a question popped into my head:

“If this happens every single project — why am I still doing it by hand?”

I build features with AI. I generate tests with AI. I write docs with AI. But payment — the most complex, most tedious, most error-prone part — I was still hand-writing every webhook handler line by line.

Not because I wanted to. Because I hadn’t found another way.


Until I Did

3 days. That’s how long I spent on payment integration for project number 3.

For project number 4, it took me 15 minutes.

Not because I got better. Not because Stripe got simpler. Those 15 minutes weren’t spent copy-pasting old code — they were 15 minutes of Claude Code understanding the project structure and setting up the entire payment system from A to Z, thanks to something I had just finished “training.”

I’ll tell you exactly what it is in the next post.


If you’re sitting at that 11:30 PM Wednesday right now — staring at a half-finished webhook handler and wondering if it’s worth it — you’re not alone. And there is a way out.

Want to be the first to experience the “15-minute” solution? Sign up here to get notified when the next post drops.

→ Part 2: The End-to-End Solution I Wish I Had From Day One (coming soon)