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.







