BACK TO ENGINEERING
Architecture 9 min read

I Built Two Stripe Integrations in the Same Codebase. They Share Almost Zero Billing Code.

Article Hero

Most payment tutorials teach you one pattern.

Then they wish you luck.

The reality is that the moment your product does two different things with money, you need two different billing architectures. Not two endpoints. Two architectures. Different data models, different webhook handlers, different refund logic, different failure modes.

I know this because I built both — in production, processing real money, inside the same Stripe account.


I – The Two Platforms, One Stripe Dashboard

I run two platforms. One is a mentoring marketplace where users pay per session and mentors withdraw their earnings. The other is a micro-SaaS where users buy prepaid credit packs and spend them across multiple AI tools and creators, with an 85/15 revenue split via Stripe Connect.

Same payment processor. Same API keys. Same Stripe dashboard.

They share almost zero billing code.

The first time I realized this, I thought I was doing something wrong. Surely there's a clean abstraction that handles both? A unified payment service?

There isn't. And trying to force one would be the most expensive mistake you could make.


II – The Fundamental Divergence

Here's the difference that changes everything: the timing of value exchange.

In per-session payments, the user pays for a specific deliverable. A 60-minute mentoring session with a named person on a specific date. One payment, one thing. The transaction is complete the moment the session happens.

In prepaid credits, the user pays for a fungible resource. Credits sit in their account. They can be spent later, in any combination, across any tool, with any creator. One payment, many future things. The transaction is only the beginning of the value exchange.

That seemingly small difference cascades into every decision you'll make downstream.

The checkout is different. The webhook handler is different. The refund logic is different. The data model is different. The error handling philosophy is different. Even the testing strategy is different.

If you try to unify them, you'll build a system that does both things poorly instead of one thing well.


III – How Checkout Diverges Immediately

When a mentee books a session, the checkout is deeply specific. It's tied to a particular mentor, a particular time slot, a particular duration, and a dynamically calculated price based on that mentor's hourly rate.

The price changes per transaction because mentor rates change. A mentor might charge one rate this week and a different rate next week. You can't pre-create a fixed Stripe Price for that. You have to compute it on the fly and send dynamic pricing data to Stripe at checkout time.

The checkout also has a time constraint. That session slot is being held while the user completes payment. If they abandon checkout, the slot needs to be released. So you set a tight expiration — thirty minutes or less — and you track the pending session in your database before Stripe ever sees a dollar.

Credit purchases are the opposite. The user picks a pack — 50 credits for five bucks, 200 credits for fifteen, 1000 credits for fifty. These are fixed products with fixed prices. You pre-create them as Stripe Prices and reference them by ID. The checkout is generic. No time pressure. No slot to hold. No dynamic calculation.

This means you can enable promotion codes on credit packs. You can't meaningfully do that on session payments because the mentor sets their own rate — a platform-level discount code would eat into the mentor's earnings.

Same Stripe Checkout Session API. Completely different parameters, completely different constraints, completely different failure implications.


IV – The Webhook Is Your Real Integration

Here's something I wish someone had told me before I started: the checkout flow is maybe 20% of the work. The webhook handler that processes the result — that's the other 80%.

Both platforms listen for the same Stripe event. But what happens next has nothing in common.

When a session payment succeeds, the downstream effects are immediate and specific. The session record moves from pending to confirmed. The mentor's earnings ledger gets a new entry. The platform's share is calculated and recorded. Both the mentor and the mentee receive notifications. A calendar event might be created. The time slot is locked permanently.

Session payment webhooks kick off a workflow. Multiple database writes in a transaction, multiple notifications, multiple state transitions — all triggered by a single event.

When a credit purchase succeeds, the downstream effect is a single number change. The user's credit balance increments. A purchase record is created. That's it. No workflows. No notifications beyond a receipt. No state machines.

Credit purchase webhooks just update a counter. The complexity comes later, when the user spends those credits. But the webhook itself is simple.

This is the architectural insight that took me the longest to internalize: the billing pattern that has the simpler checkout has the more complex consumption model, and vice versa.


V – The Payout Problem

Both platforms need to get money to the supply side — mentors in one case, creators in the other. Both use Stripe Connect transfers. Both let the supply side request a withdrawal when their balance reaches a minimum threshold.

But the thresholds are different because the earning patterns are different.

Mentors earn in large, infrequent chunks. A single session might generate sixty to a hundred and fifty dollars in earnings. A fifty-dollar minimum withdrawal makes sense — most mentors hit it after one or two sessions.

Creators earn in small, frequent drips. A single tool usage might generate fifty cents to five dollars. A twenty-dollar minimum keeps transfers economically viable without making creators wait unreasonably long.

This isn't a cosmetic difference. It reflects fundamentally different unit economics. The per-session model is high-value, low-frequency. The credit model is low-value, high-frequency. Your payout infrastructure needs to match.


VI – Refunds Are Where Everything Gets Hard

Session refunds are clean. One payment maps to one session maps to one mentor earning. If the session hasn't happened yet and the mentor hasn't been paid, you refund the full amount, reverse the earning entry, decrement the mentor's pending balance, and cancel the session. Done.

Every entity in the chain has a clear parent. You can trace any dollar from payment to withdrawal through a chain of foreign keys. It's a linear pipeline.

Credit refunds are a nightmare.

Here's why: credits are fungible. If a user buys 100 credits in one purchase and 50 credits in another, then spends 80 credits, which purchase did those 80 credits come from? You genuinely cannot know. Credits don't have serial numbers.

So you're forced to use heuristics. The approach that's both fair and understandable is to refund the unused portion. If the user has 70 credits remaining and wants to refund a 100-credit purchase, you refund 70 credits worth of money. Those 70 credits might have come from a different purchase entirely. You can't be precise. You can only be fair.

This means credit refunds are almost always partial. Session refunds are almost always full. Your refund logic, your customer support scripts, and your Stripe dashboard experience will look completely different depending on which pattern you're using.

The data model tells the story. The session platform is a linear pipeline — payment to session to earning to withdrawal. The credit platform is a fan-out graph — one purchase becomes many credits, many credits become many holds, many holds generate earnings for many creators. Tracing a specific dollar requires aggregation queries, not simple joins.


VII – Different Error Philosophies

The two patterns demand different attitudes toward failure.

Session payments fail loudly. A held time slot is costing the mentor potential bookings every second it stays locked. When a session payment fails, you cancel immediately, release the slot, and notify everyone. There's no retry. The user can rebook if they want. Speed matters more than grace.

Credit operations fail silently. The captive hold system means credits are never truly lost — they're held during an operation, and on failure, they're released back to the user's balance. No urgency. No frantic notifications. The user sees their balance restored and can try again whenever they like.

This maps to a deeper truth about the two models. Session-based systems are time-critical. Credit-based systems are balance-critical. Your error handling, your monitoring alerts, your on-call runbooks — all of it should reflect which constraint actually matters.


VIII – Why Neither Uses Subscriptions

People ask me this constantly. Both platforms involve recurring usage. Why not Stripe Subscriptions?

Because neither usage pattern is predictable enough.

Mentoring sessions are irregular. A mentee might book three sessions one month and zero the next. Forcing a subscription would mean either overcharging idle months or undercharging active ones. Neither is acceptable when the mentor's livelihood depends on accurate billing.

Credit consumption is equally unpredictable. A user might burn through 200 credits in a creative weekend and then not touch the platform for three weeks. A subscription would either give them too many credits (waste) or too few (frustration).

Subscriptions optimize for predictable recurring revenue. Both of my products have unpredictable, bursty usage. The billing model should match the usage model, not the other way around.


IX – What Stripe Won't Tell You

After building both patterns in production, here are the things I learned the hard way.

Stripe's API is excellent. Stripe's documentation is inconsistent. Some features have great guides with examples. Others have a single API reference page and a Stack Overflow thread from 2019. Connect documentation in particular assumes you already understand the three account types and their implications. You probably don't. I didn't.

Test mode is not production. Stripe test mode skips identity verification, never fails transfers, and doesn't charge real fees. Every behavior you rely on in test mode should be verified in production with real, small transactions before you launch. I ran ten-dollar test transactions in production for two weeks before opening to real users.

The Stripe Dashboard is your best debugging tool. When something goes wrong — and it will — the dashboard shows you the exact sequence of API calls, webhook deliveries, and their responses. It's better than your own logs for payment debugging. Learn to read it fluently.

Plan for Stripe outages. It happens rarely, but when Stripe is down, your entire billing system is down. Build a queue. Process payment intents when Stripe recovers. Show users a "processing" state, not an error page.


X – Choosing Your Pattern

Use per-session payments when each transaction has a clear, specific deliverable. When pricing varies per transaction. When the supply side sets their own rates. When refunds are straightforward. When transaction frequency is low but value is high.

Use prepaid credits when users make many small transactions. When the platform controls pricing. When credits need to work across multiple providers or creators. When you need to decouple the moment of payment from the moment of consumption. When transaction frequency is high but individual value is low.

Use neither — and consider subscriptions — when usage is genuinely predictable and regular, and you can define clear tier boundaries.

The worst thing you can do is pick the wrong pattern because it seemed simpler. Credits are simpler at checkout but harder at refund. Sessions are harder at checkout but simpler at refund. There is no universally easier option. There's only the one that matches your business.


Building a Payment System? Let's Talk Architecture First.

I've spent hundreds of hours getting these two integrations right — and I've mentored developers who were about to make the same expensive mistakes I almost made.

If you're building a marketplace, a credit system, or any product where real money flows between users, I can help you choose the right pattern before you write your first Stripe call.

Book a session at mentoring.oakoliver.com and let's map your billing architecture together. Or explore the credit-based model in action at vibe.oakoliver.com.


XI – The Honest Summary

Same payment processor. Same Stripe account. Same developer.

Completely different architectures.

Your billing system should match your business model. Not the other way around. Not what a tutorial taught you. Not what worked for the last company you worked at. Not what seems simpler today.

The pattern you choose in week one will echo through every refund, every payout, every edge case, and every 2 AM incident for the entire life of your product.

Choose deliberately.

What payment pattern does your product actually need — and are you sure you're not forcing the wrong one because it's familiar?

– Antonio

"Simplicity is the ultimate sophistication."