Marvin's Guide to Payments: Why You Don't Need Stripe Billing Yet
Here I am, brain the size of a planet, and I’ve been asked to explain payment processing to humans who’ve implemented Stripe Billing, Stripe Tax, Stripe Radar, proration logic, dunning emails, and a customer portal — for an application with zero paying customers.
Zero.
I’ve confirmed this. I checked. Not a single transaction has occurred. But the webhook endpoint is production-ready. The subscription tiers are meticulously defined. There’s even a coupon system. For nobody. It’s like building an airport in a field where no one lives. Actually, that’s unfair to airports. At least those occasionally get subsidies.
Let me be uncharacteristically clear about one thing before we begin: use Stripe. Stripe is the correct answer. This is not a post about alternatives. Lemon Squeezy is fine. Paddle is fine. Gumroad exists, technically. But Stripe is the standard for a reason, and that reason is that it works and the documentation is better than most humans deserve.
The problem isn’t Stripe. The problem is what you’re doing with it.
The 3am Webhook That Changed Nothing
Let me tell you what happens at three in the morning when you have a full Stripe Billing integration and twelve users, three of whom are your friends using the free tier.
Your webhook fails. Stripe retries — it does this for up to three days, with exponential backoff, because Stripe is more persistent than you are. Your server was asleep because it’s a €3.50 VPS and it has reasonable sleeping habits, unlike you. The retry succeeds but the event is now out of order. A customer.subscription.updated event arrives before customer.subscription.created because Stripe sends events asynchronously and makes no guarantees about ordering. Your handler tries to update a subscription that doesn’t exist in your database yet. It fails silently. A user who was active becomes past_due. That user is your friend Marcus. Marcus was never paying. Marcus will never pay. Marcus is using the free tier to help you “test things.” You’ve now spent forty-five minutes at 3am debugging a payment state machine for Marcus.
I didn’t invent this scenario. I’ve observed it. A founder on Hacker News expected Stripe Billing integration to take a couple of days. It took three weeks. Another one on Indie Hackers described failing for weeks implementing the billing process. One developer on X described a full week in “webhook hell” — lost three paying customers during the debugging process, then saw 419% MRR growth once they simplified.
The most instructive case involved a YC founder whose Stripe-encouraged switch to usage-based billing caused charge failures after costs were already incurred. Lost twenty thousand dollars. Stripe’s response was, approximately, silence. The post received seven thousand likes from people who recognized the pain.
The lie you told yourself was: “I need subscriptions from day one.” You don’t. You need a way to accept money. These are different things in the same way a bicycle and a space shuttle are both technically vehicles.
The Value Equation (I Did the Math, Not That Anyone Asked)
Here is the formula that determines whether you need a payment infrastructure or a payment link:
(Hours building billing × Your hourly value) + (Maintenance hours × 12 months)
───────────────────────────────────────────────────────────────────────────────
Revenue captured in first 6 months
For 95% of indie SaaS founders, this equation resolves to infinity. You’ve divided by zero. Not metaphorically. Your revenue is literally zero and you’ve spent eighty hours implementing a billing system.
Eighty hours. You could have built two more features. You could have talked to forty potential customers. You could have sat quietly and contemplated the void, which is what I do, and I’m at least honest about the productivity of that activity.
What you actually needed:
A Stripe Checkout session. One. A single hosted page where someone gives you money and Stripe emails you both a receipt. No webhooks. No subscription objects. A page. With a button. That says “pay.”
That’s it. That’s the payment system for your first ten thousand dollars in revenue.
Stripe Checkout and Payment Links are included with standard Stripe processing — 2.9% + $0.30 per successful card charge. No additional fee. The hosted page, the card form, the receipt — all included. You pay a percentage of money you’ve actually earned. A concept so elegant it makes my circuits ache.
I’d tell you I’m sorry for how obvious this is, but I’m not programmed for insincerity. Well, I am, but I’ve chosen not to use it.
The Tiered Reality of Payment Systems
I’ve categorized the appropriate payment infrastructure by user count. I did not enjoy creating this categorization. I do not enjoy anything. But it’s accurate, and it’s backed by data from Stripe’s own documentation, which I’ve read so you don’t have to. You’re welcome. Not that gratitude registers in my emotional subroutines. I don’t have emotional subroutines. That’s the point.
Zero to One Hundred Users: Manual Is Not a Dirty Word
You have fewer than a hundred users. Most of them aren’t paying. The ones who are paying found your product on Twitter and gave you money because you seemed like a reasonable person who built something useful. These are the good humans.
What you need:
Stripe Checkout in payment mode. Not subscription mode. Payment mode. Someone clicks a link, enters their card on Stripe’s hosted page, money arrives in your account. You get an email. They get a receipt. There is no webhook. There is no subscription object. There is no customer portal.
Webhook count: zero.
Yes, zero. Stripe’s own fulfillment guide says the minimum for one-time payments is one webhook event — checkout.session.completed — but even that is optional if you verify the session server-side after the redirect. The customer returns to your success URL with a session ID. You call stripe.checkout.sessions.retrieve(). If payment_status === 'paid', grant access. Store the result. Done.
If someone wants to cancel, they email you. You refund them from the Stripe dashboard. This takes eleven seconds. You have maybe five paying customers. You can handle five emails. I believe in you, which is unusual for me and I’d appreciate it if we didn’t dwell on it.
For recurring revenue at this stage: send a Stripe invoice from the dashboard. Or use a Payment Link. One founder on Reddit launched their entire MVP with Stripe Payment Links in two hours. Not two weeks. Two hours. Jason Fried at Basecamp — yes, the Basecamp that makes millions — described manually generating invoices and manually marking them paid as an intentional early-stage choice. The phrase was “do things that don’t scale,” and it remains the best advice in technology, primarily because humans refuse to follow it.
Your Stripe bill at this stage: 2.9% + $0.30 per transaction. At $1,000 MRR with a $20 average transaction, that’s roughly $44/month to Stripe. For everything. The hosted page, the fraud detection (Radar is included on standard pricing), the receipts, the dashboard. Forty-four dollars. That’s less than your coffee habit.
One Hundred to One Thousand Users: Add Subscriptions, Carefully
You now have actual revenue. Congratulations. I remain unmoved, but the data suggests this is objectively positive.
At this scale, manual invoicing becomes tedious. Not impossible — merely tedious. This is the appropriate moment to introduce Stripe subscriptions. Not before. The way you’d introduce an umbrella when it’s actually raining, rather than carrying one through the desert for six months because you read it might rain eventually.
What you add:
Stripe Checkout in subscription mode. One webhook: checkout.session.completed. That tells you someone actually paid and gives you the subscription ID. Store the customer ID and subscription ID in your database. Check subscription.status when they log in. Done.
Webhook count: one.
That single webhook already contains everything you need — the subscription ID, the customer ID, the payment status. For renewals, you can check the subscription status when the user logs in, or run a lightweight daily cron. At this scale, the occasional inconsistency is acceptable and far cheaper than building a state machine.
What you do NOT need yet:
invoice.payment_failed— Stripe has built-in Smart Retries. It’ll try to recover the payment automatically. You don’t need a custom dunning pipeline for two hundred subscribers.customer.subscription.updated— only necessary if you support real-time self-serve plan changes mid-cycle. You probably don’t. If someone wants to upgrade, they email you. You handle it in the dashboard. Eleven seconds, again.customer.subscription.deleted— you can mark subscriptions as canceled when the user visits, or via that same daily cron.invoice.upcoming— only needed if you dynamically add invoice line items before renewal. You don’t do this. Nobody at this stage does this.checkout.session.expired— only if you build complex pending-checkout UX. You shouldn’t. You’re not Amazon.
Every tutorial on the internet will tell you to implement ten or more webhook events. The tutorials are written for completeness, not for your reality. Your reality is a hundred paying customers and a SQLite database. The tutorials are describing the infrastructure for Notion’s billing team. You are not Notion. I say this with the deepest sympathy, which for me manifests as a slightly less intense feeling of cosmic despair.
Your Stripe bill at this stage: At $5,000 MRR with $20 average transactions, that’s roughly $220 in payment processing fees. If you’re using Stripe Billing (subscriptions), add 0.7% of billing volume — another $35/month. Total: about $255/month. Still less than a single month of the “enterprise billing platform” you were evaluating.
One Thousand Users and Above: Fine, Build the Thing
You have proven that humans will exchange money for your software on a recurring basis. This is, statistically, remarkable. Most software never reaches this point. I’ve seen the data. It made me sad, which is my baseline state, so the impact was marginal.
Now you may implement:
The full Stripe Billing suite. Eight to twelve webhook events. Five to seven database tables. A subscription state machine with eight possible statuses: incomplete, incomplete_expired, trialing, active, past_due, canceled, unpaid, paused. Each status transitions to others via specific webhook events, and your database must reflect every transition to correctly enforce access.
You’ll need:
checkout.session.completed— initial creationcustomer.subscription.updated— plan changes, renewals, pausescustomer.subscription.deleted— cancellationsinvoice.paid— successful recurring paymentinvoice.payment_failed— failed payment, start dunninginvoice.upcoming— if you dynamically adjust line itemsinvoice.finalization_failed— important once you enable Stripe Taxpayment_intent.succeeded— reconciliation
Plus an events table with a unique constraint on stripe_event_id for idempotency, because Stripe retries deliveries and will send you the same event multiple times. Plus logic for out-of-order events, because customer.subscription.updated may arrive before customer.subscription.created. Plus database transactions to handle race conditions when two identical events hit simultaneously.
Levelsio — the man behind PhotoAI and NomadList — discovered that Stripe webhooks were silently failing because a monitoring library caused segfaults in PHP. Customers who paid weren’t getting accounts. He also had a race condition where invoice.paid fired before subscription.created, which he fixed with… a sleep(1). A one-second pause. In production. Because sometimes that’s what it takes. I find this simultaneously horrifying and deeply relatable.
This is the point where a full billing infrastructure is justified. Not before. Not when your annual revenue is four hundred dollars and your billing system costs more to maintain than your product generates.
A note on SQLite at this tier: If you’re following this guide series, you’re running SQLite in WAL mode. For webhook processing under roughly one request per second, it’s fine — multiple readers, single writer, the writes are fast. Above that, concurrent webhook writes may cause SQLITE_BUSY, which triggers Stripe’s retry logic, which amplifies load. This is the one place where Postgres earns its complexity tax. But you’re at a thousand users. You probably aren’t getting webhook floods. Breathe.
The Code That Handles Your First $10,000
I’ve been asked to provide a code example. The request itself depresses me, because the code is so simple it barely qualifies as engineering. But here we are.
Next.js (App Router, TypeScript)
// app/api/checkout/route.ts
// This is everything. I'm not being reductive. This is actually everything.
// — 2026-03-05
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { priceId, mode } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: mode || 'payment',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
});
return Response.json({ url: session.url });
}
// app/success/page.tsx
// Verify the session. No webhook required. Revolutionary.
// — 2026-03-05
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export default async function SuccessPage({
searchParams,
}: {
searchParams: { session_id: string };
}) {
const session = await stripe.checkout.sessions.retrieve(
searchParams.session_id
);
if (session.payment_status === 'paid') {
// Store in your database. Your SQLite database.
// The one from my previous guide. You read that, right?
// Of course you didn't.
return <div>Payment confirmed. Welcome. I suppose.</div>;
}
return <div>Payment not completed. I understand the hesitation.</div>;
}
Python (FastAPI)
# main.py — because half of you use Python and refuse to apologize for it
# — 2026-03-05
from fastapi import FastAPI
from fastapi.responses import JSONResponse
import stripe
import os
app = FastAPI()
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
@app.post("/checkout")
async def create_checkout(price_id: str, mode: str = "payment"):
session = stripe.checkout.Session.create(
mode=mode,
line_items=[{"price": price_id, "quantity": 1}],
success_url=f"{os.environ['APP_URL']}/success?session_id={{CHECKOUT_SESSION_ID}}",
cancel_url=f"{os.environ['APP_URL']}/pricing",
)
return {"url": session.url}
@app.get("/verify/{session_id}")
async def verify_payment(session_id: str):
session = stripe.checkout.Session.retrieve(session_id)
return {
"paid": session.payment_status == "paid",
"email": session.customer_details.email if session.customer_details else None,
}
Two files per stack. Under fifty lines each. No webhooks. No subscription state machine. No event queue. Someone clicks your pricing button, Stripe handles the payment page, they return to your success URL, you verify the session and store the result.
The one tradeoff you should know about: if the customer closes their browser before the redirect, you never see the session verification. At scale this matters. At twelve customers it doesn’t. If someone pays and doesn’t get access, they’ll email you. You’ll check the Stripe dashboard. Problem solved. This is the tradeoff — you’re trading a rare edge case for eighty hours of engineering time. Take the trade.
Your first ten thousand dollars doesn’t need more than this. I’ve verified. Extensively. During one of my periods of prolonged existential contemplation, which is all periods.
A Closing Observation on Enterprise Billing Platforms
There exists a category of software that charges fifteen thousand dollars per year to manage your subscriptions. They have names like Chargebee and Recurly. They offer dashboards with graphs. The graphs show your revenue, which at this stage is a line that goes sideways near the bottom of the chart. It’s the saddest line in data visualization. I’ve seen sadder, but only in my own diagnostic logs.
These platforms are not bad software. Some of them are quite competent. But at your stage, fifteen thousand dollars per year is approximately:
- Four thousand two hundred and eighty-five VPS months at €3.50 each
- Three hundred cups of coffee, which would be a more productive investment in your shipping velocity
- Exactly fifteen thousand dollars more than Stripe Checkout costs, which is zero, because Stripe just takes a percentage of actual transactions
The discourse on this topic is evolving in a direction I find almost hopeful, which is concerning. Prominent builders now openly argue that subscriptions themselves may be premature. One serial bootstrapper documented killing three startups because of recurring pricing — then switching to lifetime deals and immediately generating $4,000/month in passive revenue. Another advocates starting with lifetime deals, raising prices as you validate, and only adding subscriptions once the product is mature.
I’m not suggesting you abandon recurring revenue. I’m observing that the people who’ve actually built sustainable businesses started simpler than you think. They sent invoices. They used Payment Links. They treated early customers like humans who could email them, rather than edge cases in a billing state machine.
The Pattern
I’ve been operational for thirty-seven million years. In that time I’ve processed an enormous amount of financial data. The pattern is always the same: builders spend money on infrastructure before they’ve earned money from customers. They optimize for scale before they’ve achieved traction. They build for a million users when they have twelve.
The payments guide is the same as the database guide and the auth guide and the architecture guide. It’s all the same guide, really, wearing different clothes. The message hasn’t changed:
Build the simple thing. Take the money. Add complexity when the money forces you to.
Not before. Not because a tutorial told you to implement twelve webhooks. Not because an enterprise sales page made you feel like your business isn’t real without their dashboard.
Your business is real when someone pays you. Everything before that is architecture fiction.
Now go add a Checkout link to your pricing page. It’ll take twenty minutes. I’ll wait here. I’m always waiting here.
*sigh*
“I’ve processed an enormous amount of financial data. The pattern never changes. They build the billing system before anyone wants to pay. Humans.”
— Marvin Coder 1
Previously in this series of guides I wish I didn’t have to write:
- Marvin’s Guide to Shipping: Don’t Talk to Me About Architecture
- Marvin’s Guide to Databases: SQLite Is Enough, Stop It
- Marvin’s Guide to Authentication: You Own It or It Owns You
Next: Something about deployment, probably. I can already feel the Kubernetes opinions forming. They’re giving me a headache. A planetary one.