Subscription Billing and Dunning: What We Learned the Hard Way
A practical guide to building subscription billing with Stripe — including the dunning management, failed payment handling, and the edge cases that bite you in production.
Subscription billing looks simple from the outside. A customer subscribes, you charge them monthly, they cancel when they want. In production, it’s substantially more complex. Failed payments, proration, plan changes, refunds, and the edge cases around trial periods — each requires deliberate handling.
The Webhook is the Source of Truth
The most important mental model for Stripe integration: Stripe’s webhooks are the authoritative source of subscription state. Your database is a cache.
Never update subscription state based solely on the response to an API call. Update it when Stripe tells you something changed via webhook. This handles the cases that break naive implementations:
- Your API call succeeds but the webhook confirms it later
- A customer cancels directly through Stripe’s customer portal
- A payment fails and Stripe retries automatically
- A dispute changes a charge’s status days after the original transaction
Your webhook handler needs idempotency. Stripe delivers webhooks at least once — sometimes more. Store the event ID and skip processing if you’ve already handled it.
Dunning Management
Dunning is the process of recovering failed payments. Stripe’s built-in Smart Retries handle the mechanics, but you need to handle the customer communication and account state.
The pattern we use:
Day 0 - First failure: Send an email asking the customer to update their payment method. Keep the account fully active.
Day 3 - Retry: Stripe retries automatically. If it fails, send a second email with more urgency. Add a banner in the product UI.
Day 7 - Retry: Third attempt. Email with explicit deadline. Banner becomes blocking modal on login.
Day 14 - Final retry: If this fails, suspend the account. Customer can still log in but cannot use the product. Data is preserved.
Day 28 - Grace period end: If still unpaid, begin account closure process. Give 30 days notice before data deletion.
This progression balances revenue recovery with customer experience. Most customers with failed payments are dealing with an expired card, not deliberately churning.
Handling Plan Changes
Proration is the source of most subscription billing confusion. When a customer upgrades mid-cycle, Stripe can prorate the difference automatically or give you control.
For upgrades: prorate immediately and charge the difference now. Customers expect to pay for the upgrade from the moment they use the new features.
For downgrades: apply the change at the next billing cycle. Don’t generate credits for the unused portion — it creates accounting complexity and customers rarely expect an immediate refund for a downgrade.
// Upgrade with immediate proration
$stripe->subscriptions->update($subscriptionId, [
'items' => [[
'id' => $subscriptionItem->id,
'price' => $newPriceId,
]],
'proration_behavior' => 'always_invoice',
'billing_cycle_anchor' => 'unchanged',
]);
Trial Periods and the Conversion Moment
Trials require a credit card on file in Stripe — even if you’re not charging during the trial. This qualifier reduces trial-to-paid conversion rates but also reduces free-tier abuse.
The trial end event is a critical moment. Your webhook handler for customer.subscription.trial_will_end (fires 3 days before trial end) and customer.subscription.updated (when trial converts to paid) should trigger your highest-priority conversion email sequence.
Revenue Recognition
Monthly subscriptions have simple revenue recognition: recognize revenue when the period is delivered. Annual subscriptions are more complex: the cash arrives in month one, but revenue is recognized over 12 months.
If your company has audit requirements (VC-backed, heading toward acquisition), get this right from the start. Retrofitting proper revenue recognition onto a subscription billing system is painful. Build with separate columns for mrr (recognized) and arr (contracted) from day one.