Stripe Subscriptions — Recurring Billing and Plans
In this tutorial, you will learn about Stripe Subscriptions. We cover key concepts, practical examples, and best practices to help you master this topic.
Stripe Subscriptions automate recurring billing for SaaS products, handling plan creation, trial periods, automatic renewals, failed payment retries, and plan changes.
What You'll Learn
By the end of this lesson you will understand how to create subscription products and prices, subscribe customers, handle trials, upgrade and downgrade plans, and manage the subscription lifecycle.
Why It Matters
Recurring billing is the most complex payment flow -- it involves scheduled charges, proration, failed payment handling, and plan changes. Stripe Subscriptions handle all of this automatically.
Real-World Use
DodaZIP uses subscriptions for its cloud backup service. Customers choose monthly or annual plans, get a 30-day free trial, and Stripe handles all renewals, failed payment retries, and plan changes.
flowchart LR
C[Customer] --> S[Subscribe]
S --> T[Trial Period]
T --> A[Active]
A -->|Renewal| R[Charge]
R -->|Success| A
R -->|Failed| F[Retry]
F -->|Exceeded| CA[Canceled]
A -->|Customer cancels| CA
A -->|Upgrade| U[New Plan]
style S fill:#6772e5,color:#fff
Creating Products and Prices
Products represent what you sell. Prices define how much and how often to charge.
import stripe
import os
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY", "sk_test_placeholder")
def create_subscription_product(name, description):
product = stripe.Product.create(name=name, description=description)
return product.id
def create_monthly_price(product_id, amount_cents):
price = stripe.Price.create(
product=product_id,
unit_amount=amount_cents,
currency="usd",
recurring={"interval": "month"},
)
return price.id
stripe.api_key = "sk_test_placeholder"
class MockProduct:
id = "prod_monthly_plan"
class MockPrice:
id = "price_monthly_1999"
stripe.Product.create = lambda **kwargs: MockProduct()
stripe.Price.create = lambda **kwargs: MockPrice()
prod_id = create_subscription_product("Monthly Premium", "Premium plan with all features")
price_id = create_monthly_price(prod_id, 1999)
print(f"Product: {prod_id}")
print(f"Monthly Price: {price_id} ($19.99/month)")
Expected output:
Product: prod_monthly_plan
Monthly Price: price_monthly_1999 ($19.99/month)
Creating a Subscription
Use Checkout Session with mode=subscription or create subscriptions directly with the API.
# create_subscription.py
# Create a customer subscription
import stripe
stripe.api_key = "sk_test_placeholder"
class MockSubscription:
id = "sub_abc123"
status = "active"
current_period_end = 1767225600
items = {"data": [{"price": {"id": "price_monthly_1999"}}]}
stripe.Subscription.create = lambda **kwargs: MockSubscription()
def subscribe_customer(customer_id, price_id, trial_days=0):
subscription = stripe.Subscription.create(
customer=customer_id,
items=[{"price": price_id}],
trial_period_days=trial_days,
payment_behavior="default_incomplete",
)
print(f"Subscription created: {subscription.id}")
print(f"Status: {subscription.status}")
print(f"Next billing: {subscription.current_period_end}")
return subscription
sub = subscribe_customer("cus_abc123", "price_monthly_1999", trial_days=14)
Plan Upgrades and Downgrades
When customers change plans, Stripe handles proration automatically.
# plan_changes.py
# Upgrading and downgrading subscriptions
def change_plan(subscription_id, new_price_id, proration=True):
print(f"Changing subscription {subscription_id}")
print(f"New price: {new_price_id}")
print(f"Proration: {'enabled' if proration else 'disabled'}")
if new_price_id == "price_pro_4999":
print(" -> UPGRADE: Immediate change, prorated credit")
print(" -> New amount: $49.99/month (billed now)")
elif new_price_id == "price_basic_999":
print(" -> DOWNGRADE: Change at period end")
print(" -> New amount: $9.99/month (next billing)")
print(f" -> Subscription updated successfully")
change_plan("sub_abc123", "price_pro_4999")
print()
change_plan("sub_abc123", "price_basic_999")
Common Mistakes
Not handling proration correctly: Upgrades should prorate immediately. Downgrades typically change at period end. Configure proration_behavior.
Forgetting to set trial periods: Without trials, customers are charged immediately. Set trial_period_days or trial_end.
Not using incomplete status: Subscriptions start as incomplete until the initial payment is confirmed. Handle payment collection before marking active.
Ignoring failed payment Webhooks: Listen to invoice.payment_failed to notify customers and update payment methods.
Not testing the full subscription lifecycle: Test signup, renewal, upgrade, downgrade, cancellation, and failed payment scenarios.
Practice Questions
How does Stripe handle subscription renewals? Stripe automatically creates an invoice at each billing cycle and attempts payment using the customer's default payment method.
What happens when a subscription payment fails? Stripe retries automatically based on smart retry logic, then marks the subscription as past_due, and eventually cancels.
How does proration work with plan changes? Stripe calculates the unused portion of the current plan and applies it as credit toward the new plan.
What is a trial period? A free period before the first payment is collected. The subscription is active during the trial.
Challenge: Design a subscription system with monthly ($9.99) and annual ($99.99) plans, 30-day free trial, and automatic plan upgrades with proration.
FAQ
Mini Project
Create a subscription management endpoint that handles creating a subscription, upgrading, downgrading, and cancellation.
import json
def subscription_handler(event, context):
body = json.loads(event.get("body", "{}"))
action = body.get("action")
customer_id = body.get("customer_id", "cus_test")
price_id = body.get("price_id", "price_monthly_1999")
if action == "create":
sub_id = f"sub_{hash(customer_id)}"
print(f"Created subscription {sub_id} for {customer_id}")
return {"statusCode": 201, "body": json.dumps({"subscription_id": sub_id, "status": "active"})}
if action == "change":
print(f"Changed {customer_id} to price {price_id}")
return {"statusCode": 200, "body": json.dumps({"status": "updated", "price": price_id})}
if action == "cancel":
print(f"Canceled subscription for {customer_id}")
return {"statusCode": 200, "body": json.dumps({"status": "canceled"})}
return {"statusCode": 400, "body": json.dumps({"error": "Invalid action"})}
print(subscription_handler({"body": json.dumps({"action": "create"})}, None)["body"])
What's Next
Next: Webhook Handling for server-side confirmation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro