Stripe Subscriptions: Recurring Billing & Subscription Lifecycle
In this tutorial, you will learn about Stripe Subscriptions: Recurring Billing & Subscription Lifecycle. We cover key concepts, practical examples, and best practices to help you master this topic.
Stripe Subscriptions handle recurring billing automatically — charging customers on schedule, managing plan changes with proration, retrying failed payments, and sending invoices.
What You'll Learn
How to create subscriptions, change plans with proration, handle payment failures (dunning), cancel and reactivate, manage trial periods, and monitor subscription lifecycle events via Webhooks.
Why It Matters
Recurring billing requires reliable scheduling, payment retries, and plan management. Stripe's subscription system handles this automatically. DodaTech manages 10K+ active subscriptions with Stripe's built-in dunning and proration.
Real-World Use
A Pro user upgrades to Enterprise. Stripe prorates the remaining Pro days, charges the difference, and updates the subscription. An invoice is generated automatically.
flowchart LR
A["Checkout\nSubscription"] --> B["Active\nStatus: active"]
B --> C["Payment\nSucceeds"]
C --> D["Invoice\nPaid"]
C --> E["Payment\nFails"]
E --> F["Dunning\nRetry 3x"]
F --> G["Past Due"]
G --> H["Unpaid\nCanceled"]
B --> I["Plan Change\nProrated"]
B --> J["Cancel\nat Period End"]
B --> K["Reactivate"]
style B fill:#bbf7d0,stroke:#16a34a
style E fill:#fef3c7,stroke:#d97706
style H fill:#fecaca,stroke:#dc2626
Creating a Subscription
import stripe
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
def create_subscription(customer_id, price_id):
subscription = stripe.Subscription.create(
customer=customer_id,
items=[{"price": price_id}],
trial_period_days=14,
metadata={"source": "pricing_page"},
payment_behavior="default_incomplete"
)
print(f"Subscription: {subscription.id}")
print(f"Status: {subscription.status}")
print(f"Current period: {subscription.current_period_start} to {subscription.current_period_end}")
print(f"Trial end: {subscription.trial_end}")
return subscription
sub = create_subscription("cus_abc123", "price_pro_monthly")
# Expected output:
# Subscription: sub_1MqwertyABC123
# Status: active
# Current period: 1719590400 to 1722268800
Upgrading/Downgrading Plans
def change_plan(subscription_id, new_price_id):
subscription = stripe.Subscription.retrieve(subscription_id)
updated = stripe.Subscription.modify(
subscription_id,
items=[{
"id": subscription["items"]["data"][0]["id"],
"price": new_price_id
}],
proration_behavior="always_invoice",
payment_behavior="pending_if_incomplete"
)
print(f"Plan changed: {updated.id}")
print(f"New price: {new_price_id}")
print(f"Proration: {updated.latest_invoice}")
print(f"Status: {updated.status}")
# Proration creates an invoice. If amount > 0, it's charged immediately.
return updated
change_plan("sub_abc123", "price_enterprise_monthly")
# Expected output:
# Plan changed: sub_abc123
# Proration: in_1MqwertyABC123
# Status: active
Handling Payment Failures
# Stripe automatically retries failed payments (dunning)
# Default: 3 retries over 5 days
# Configure in Dashboard: Settings > Billing > Email settings
def handle_invoice_payment_failed(invoice):
customer_id = invoice["customer"]
subscription_id = invoice["subscription"]
attempt_count = invoice["attempt_count"]
next_attempt = invoice["next_payment_attempt"]
print(f"Payment failed for subscription {subscription_id}")
print(f"Attempt {attempt_count}/3")
print(f"Next attempt: {next_attempt}")
if attempt_count >= 3:
print("All retries exhausted — subscription will cancel")
alert_team(customer_id, subscription_id)
else:
notify_customer(invoice["customer_email"])
Canceling Subscriptions
def cancel_subscription(subscription_id, at_period_end=True):
if at_period_end:
sub = stripe.Subscription.modify(
subscription_id,
cancel_at_period_end=True
)
print(f"Subscription will cancel at period end: {sub.cancel_at}")
else:
sub = stripe.Subscription.delete(subscription_id)
print(f"Subscription canceled immediately")
return sub
def reactivate_subscription(subscription_id):
sub = stripe.Subscription.modify(
subscription_id,
cancel_at_period_end=False
)
print(f"Subscription reactivated: {sub.id}")
return sub
Common Mistakes
1. Not Setting payment_behavior
Without payment_behavior: "default_incomplete", the subscription creates even if the initial payment fails. This leads to unpaid subscriptions.
2. Ignoring Proration Effects
Plan changes create invoices for prorated amounts. proration_behavior: "always_invoice" charges immediately. create_prorations waits until the next billing cycle.
3. Missing Webhook Handlers
Listen for customer.subscription.updated, customer.subscription.deleted, and invoice.payment_failed to keep your database in sync.
4. Not Handling Trial to Paid Transition
When a trial ends, the first payment runs. If it fails, the subscription becomes past_due. Notify customers before trial ends to update payment methods.
5. Forgetting to Update Local Database
Stripe events drive your subscription state. Don't just rely on the Stripe Dashboard — sync subscription status to your database via webhooks.
Practice Questions
- How does Stripe proration work when changing plans?
- What is the dunning Process for failed payments?
- How do you handle subscription cancellations?
- How do you upgrade a user from free to paid after a trial?
Answers:
- Stripe calculates the unused portion of the current plan and applies it to the new plan. The difference is invoiced immediately or credited.
- Stripe retries failed payments according to a schedule (default: 3 attempts over 5 days). After all retries fail, the subscription becomes
unpaidorcanceled. - Use
cancel_at_period_end: trueto cancel at the end of the billing period. Usedelete()for immediate cancellation with refund. - Use webhooks:
customer.subscription.updatedfires when trial ends and payment succeeds. Activate the account in your database.
Challenge: Build a subscription management system: create subscriptions with 14-day trial, handle plan upgrades with proration, implement payment failure dunning (3 retries), process cancellations (period-end), and sync subscription status via webhook events.
FAQ
Mini Project
Build a subscription lifecycle system: create subscriptions with trial, handle plan changes (upgrade/downgrade with proration), implement dunning for failed payments, cancel at period end, reactivate, and sync all events via webhooks to your database.
What's Next
Webhook Events — receive real-time payment events via Stripe webhooks.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro