Skip to content

Stripe Subscriptions — Recurring Billing and Plans

DodaTech Updated 2026-06-28 4 min read

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

  1. Not handling proration correctly: Upgrades should prorate immediately. Downgrades typically change at period end. Configure proration_behavior.

  2. Forgetting to set trial periods: Without trials, customers are charged immediately. Set trial_period_days or trial_end.

  3. Not using incomplete status: Subscriptions start as incomplete until the initial payment is confirmed. Handle payment collection before marking active.

  4. Ignoring failed payment Webhooks: Listen to invoice.payment_failed to notify customers and update payment methods.

  5. Not testing the full subscription lifecycle: Test signup, renewal, upgrade, downgrade, cancellation, and failed payment scenarios.

Practice Questions

  1. 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.

  2. 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.

  3. 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.

  4. What is a trial period? A free period before the first payment is collected. The subscription is active during the trial.

  5. 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

Can I create custom billing intervals?

Yes. Support for daily, weekly, monthly, yearly, or custom intervals like every 3 months.

How does Stripe handle multi-currency subscriptions?

Define prices in different currencies for the same product. Stripe charges in the customer's currency.

What is a usage-based subscription?

Metered billing where customers are charged based on usage, tracked via metered prices.

Can customers have multiple subscriptions?

Yes. A customer can have multiple active subscriptions, each with their own billing schedule.

How do I pause a subscription?

Use subscription.pause or set status to paused. Paused subscriptions do not generate invoices.

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