
The complete blueprint for handling subscriptions, customer billing portals, and edge-case webhooks without losing your sanity.*
Every developer building a SaaS eventually runs into the same wall: Monetization Architecture.
Building the core features of an application is exciting. Writing billing logic, managing recurring cycles, handling failed credit card charges, generating PDFs, and wiring up webhook endpoints is usually where momentum slows down.
In this article, we'll walk through the architecture of a production-ready SaaS billing engine and examine the critical patterns needed to accept recurring payments reliably.
1. The Billing State Problem
Handling payments is not just a single API call to charges.create(). A production billing system must manage state transitions across multiple scenarios:
- What happens when a renewal payment fails at 3:00 AM?
- How do you handle plan upgrades with prorated charges mid-cycle?
- What happens if a user cancels their subscription before the cycle ends?
The Core Subscription State Machine
A resilient subscription model requires tracking distinct statuses:
trialingactivepast_duecanceled-
incomplete
[ Checkout ] ──► [ Active ] ──► (Cycle Renewal)
│ │
▼ ▼
[ Canceled ] [ Past Due ] ──► [ Inactive ]
2. Webhooks: The Single Source of Truth
Never rely solely on client-side redirect URLs (e.g., /checkout/success) to grant access to paid tiers. Network disconnects, closed browser tabs, or client-side crashes can prevent access from being granted.
Recommended Webhook Pipeline
- Verify Signature: Ensure the payload originated from your payment gateway.
-
Idempotency Check: Store incoming
event_idvalues to prevent processing duplicate events. -
Queue Processing: Immediately return a
200 OKresponse to the payment provider and dispatch the actual processing task to a background worker.
// Example: Verifying and dispatching webhooks asynchronously
public function handleWebhook(Request $request)
{
$event = $this->verifySignature($request);
if ($this->hasBeenProcessed($event->id)) {
return response()->json(['status' => 'already_processed'], 200);
}
ProcessBillingEventJob::dispatch($event);
return response()->json(['status' => 'queued'], 200);
}
3. Customer Self-Service: The Billing Portal
Modern users expect to manage their own billing:
- Updating expired credit cards
- Downloading past VAT / Tax invoices
- Upgrading or downgrading plans
Using hosted billing portals (like Stripe Customer Portal) eliminates the need to build custom PCI-compliant credit card updating forms on your frontend.
4. Get the Starter Kit
Rather than spending 40+ hours wiring up database schemas, webhook listeners, environment configs, and test suites, you can grab the complete SaaS Billing Engine Starter Kit.
Includes:
- Complete source code with local DDEV / Docker support
- Production-tested webhook handlers
- Seeders, migrations, and plan definitions
- Detailed documentation
Top comments (0)