Integrating Stripe Billing for SaaS Subscriptions
You launch a SaaS; after a month, you have 1000 users, each on different plans, some on trial, some canceling. Without a billing system, chaos ensues. Stripe Billing is a ready-made solution that covers 95% of scenarios: subscriptions, trial periods, upgrades/downgrades, prorated billing, metered usage, automatic retry on failure Stripe Docs. Compared to a custom-built system, Stripe Billing cuts development time from 2–3 months to 1–2 weeks — that's 5–10x faster. It also minimizes errors through proven algorithms. You save at least $15,000 on development and $5,000 annually on support. We have integrated Stripe into 30+ projects, guaranteeing stable end-to-end billing. Our experience lets us predict and avoid typical issues: incorrect trial handling, prorating errors, webhook failures.
What Are the Key Entities in Stripe Billing?
Product → Price → Subscription → Invoice → PaymentIntent. A product represents a plan (Basic, Pro, Enterprise). Prices are attached (monthly/yearly). A subscription links a customer to a price. Invoices are generated automatically. Stripe handles all routine tasks: creating invoices, payment attempts, prorating, trials. You only need to configure products and webhooks to sync statuses. This reduces development time from months to 1–2 weeks.
Implementation Steps
Step 1: Create Products and Prices
$product = \Stripe\Product::create([ 'name' => 'Pro Plan', 'metadata' => ['plan_id' => 'pro'], ]); $monthlyPrice = \Stripe\Price::create([ 'product' => $product->id, 'unit_amount' => 2900, 'currency' => 'usd', 'recurring' => ['interval' => 'month'], 'lookup_key' => 'pro_monthly', ]); $yearlyPrice = \Stripe\Price::create([ 'product' => $product->id, 'unit_amount' => 27900, 'currency' => 'usd', 'recurring' => ['interval' => 'year'], 'lookup_key' => 'pro_yearly', ]); Step 2: User Registration and Subscription Creation
This involves four sub-steps: create a Stripe customer, create a subscription with a trial, pass the client_secret to the frontend, and process webhooks.
$stripeCustomer = \Stripe\Customer::create([ 'email' => $user->email, 'name' => $user->name, 'metadata' => ['user_id' => $user->id], ]); $subscription = \Stripe\Subscription::create([ 'customer' => $user->stripe_customer_id, 'items' => [['price' => 'pro_monthly']], 'trial_period_days' => 14, 'payment_behavior' => 'default_incomplete', 'payment_settings' => ['save_default_payment_method' => 'on_subscription'], 'expand' => ['latest_invoice.payment_intent'], ]); $clientSecret = $subscription->latest_invoice->payment_intent->client_secret; payment_behavior: default_incomplete means the subscription activates only after a successful first payment. Important for free trials: the card is attached but not charged.
Step 3: Handle Upgrades, Downgrades, and Metered Billing
Upgrade/Downgrade with Proration
public function changePlan(User $user, string $newPriceLookupKey): void { $prices = \Stripe\Price::all(['lookup_keys' => [$newPriceLookupKey]]); $newPrice = $prices->data[0]; $subscription = \Stripe\Subscription::retrieve($user->stripe_subscription_id); \Stripe\Subscription::update($subscription->id, [ 'items' => [[ 'id' => $subscription->items->data[0]->id, 'price' => $newPrice->id, ]], 'proration_behavior' => 'create_prorations', 'billing_cycle_anchor'=> 'unchanged', ]); } On upgrade, Stripe automatically credits unused time and issues an invoice for the difference. For yearly subscriptions, use proration_behavior: none.
Metered Billing For usage-based pricing (API calls, storage):
$price = \Stripe\Price::create([ 'product' => $product->id, 'currency' => 'usd', 'recurring' => [ 'interval' => 'month', 'usage_type' => 'metered', 'aggregate_usage'=> 'sum', ], 'billing_scheme' => 'per_unit', 'unit_amount' => 1, ]); \Stripe\SubscriptionItem::createUsageRecord( $subscriptionItemId, ['quantity' => $apiCallsThisPeriod, 'action' => 'set'] ); action: set sets an absolute value; increment adds to the current value. set is safer on retry.
Step 4: Integrate Customer Portal for Self-Service
Stripe Customer Portal provides a ready-made UI for managing subscriptions: changing cards, canceling, viewing invoices. No need to build from scratch. Just create a BillingPortal.Session and redirect the user. This saves up to 40 hours of interface development, which at $100/hour equals $4,000 in savings.
Step 5: Set Up Webhooks for Status Synchronization
All business logic is built on webhooks, not synchronous API responses. Key events and actions:
| Event | Action |
|---|---|
customer.subscription.created |
Save subscription_id, activate user |
customer.subscription.updated |
Update status in DB |
customer.subscription.deleted |
Mark as canceled |
invoice.payment_succeeded |
Update next_payment date |
invoice.payment_failed |
Notify user, do not block immediately |
customer.subscription.trial_will_end |
Remind about upcoming charge |
Also handle customer.subscription.paused and resumed if you support pause.
protected array $handlers = [ 'customer.subscription.created' => 'onSubscriptionCreated', 'customer.subscription.updated' => 'onSubscriptionUpdated', 'customer.subscription.deleted' => 'onSubscriptionCancelled', 'invoice.payment_succeeded' => 'onInvoicePaid', 'invoice.payment_failed' => 'onInvoicePaymentFailed', 'customer.subscription.trial_will_end'=> 'onTrialEndingSoon', ]; public function onInvoicePaymentFailed(array $event): void { $subscription = $event['data']['object']['subscription']; $user = User::where('stripe_subscription_id', $subscription)->firstOrFail(); $nextRetry = $event['data']['object']['next_payment_attempt']; Notification::send($user, new PaymentFailedNotification($nextRetry)); } For secure integration, follow this checklist:
- Verify event signature (Webhook-Signature header).
- Use idempotency_key when creating subscriptions.
- Log all webhooks for debugging.
- Add a health-check endpoint for monitoring.
- Test scenarios: successful payment, failure, trial, upgrade.
How to Handle Failed Payments?
Stripe automatically makes up to 3 retry attempts with intervals of 2, 3, and 5 days. Your job is to notify the user and update the subscription status to past_due. After the third failure, the subscription transitions to canceled. To avoid losing customers, implement a scenario: first failure — email, second — push notification, third — access block with renewal option. Timely notification reduces churn by 20% compared to no notification – that's 3x better churn management than manual follow-ups.
Billing Model Comparison: Flat-rate vs Per-unit vs Tiered
| Parameter | Flat-rate | Per-unit | Tiered |
|---|---|---|---|
| Implementation complexity | Low | Medium | High |
| Client flexibility | Low | High | Medium |
| Prorating error risk | Minimal | Medium | High |
| Recommended for | Basic plans | API, storage | Enterprise |
| Cost (setup) | $5,000–$7,000 | $7,000–$10,000 | $10,000–$15,000 |
Guaranteeing 99.9% Billing Uptime
Use failover logic: if Stripe API is unavailable, cache requests and retry with exponential backoff. Set up webhook monitoring with Telegram/Slack alerts. Always process invoice.payment_failed events — timely notification reduces churn by 20%. Compared to an in-house system, Stripe Billing is 5x faster to implement and 10x more reliable due to battle-tested infrastructure.
What's Included in Integration
- Setup of products and prices in Stripe Dashboard.
- Migration scripts for plan synchronization.
- Implementation of registration and subscription with trial.
- Webhook configuration and handling of key events.
- Customer Portal integration.
- API documentation and 1 month post-release support.
Contact us for an accurate estimate of your project. Basic integration costs $5,000–$7,000 and takes 5 to 10 business days. Complex scenarios (metered billing, multiple currencies) cost $10,000–$15,000 and take up to 3 weeks. Get a consultation on integrating Stripe Billing — set up billing for your SaaS without the headache.







