Django Stripe integration tutorial – high level

High level flow of recurring product

Python
[ User ]               [ Django Backend ]              [ Stripe ]
   |                           |                          |
   |  Clicks Upgrade Button    |                          |
   |-------------------------->|                          |
   |                           |  Create Stripe Customer  |
   |                           |------------------------->|
   |                           |                          |
   |                           |  Create Checkout Session |
   |                           |------------------------->|
   |                           |                          |
   |                           |<------ Session URL ------|
   |<----- Redirect -----------|                          |
   |--------------------------> Stripe-hosted Checkout UI |
   |                           |                          |
   |          Success/Cancel Redirect to Django           |
   |<--------------------------|                          |
   |                           |                          |
   |                           | <-- Stripe Webhooks ---->|

Django entities vs Stripe entities

On the django side you’ll need a user column to store the tier of your user

Python
user.tier: 'free' | 'pro'
# Allow users to upgrade to 'pro'
# Automatically downgrade to 'free' if the subscription ends


State changes:
  [*] --> Free : User registers
  Free --> Pro : Successful subscription
  Pro --> Free : Subscription cancelled/failed
  Pro --> Pro : Successful renewal
  Free --> Free : Payment declined

With stripe, you interact based on their entities. Note: below you only own “Django user”

ConceptPurpose
CustomerRepresents our Django user on Stripe
Product + PriceRecurring monthly/annual subscription tier
Checkout SessionStripe-hosted payment page
SubscriptionTied to a customer & price
WebhookStripe notifies us of lifecycle events
Python
[ Product: Pro Tier ]
        |
[ Price: $9/month ]
        |
[ Subscription ] <-- linked to -- [ Customer ] <-- maps to --> [ Django User ]

Explicit Action vs Automated Webhooks

Upgrade Flow (Explicit Action)
[User clicks “Upgrade”] → [Django backend creates Stripe customer]
→ [Django backend creates checkout session]
→ [User is redirected to Stripe]
→ [User completes payment]
→ [User redirected back to success URL]
→ [Stripe triggers webhook: payment_succeeded]

Automated webhooks

Python
[Renewal Day]

[Payment Attempt]

[Success?] → Yes → OK
     ↓ No
[Retry in 2 days]

[Retry in 4 days]

[Grace Ends (7 days)] → [Subscription canceled] → Webhook → Downgrade

Core Webhook Events

Webhook EventWhat It MeansAction in Django App
invoice.payment_succeededPayment confirmedUpgrade user to 'pro'
invoice.payment_failedPayment failedLog failure, show warning
customer.subscription.updatedTier changed (e.g. plan downgrade)Sync user’s tier
customer.subscription.deletedSubscription canceled (grace expired)Downgrade to 'free'

It is better not to mess up your users app so creating a payments app is neater

Python
$ python manage.py startapp payments

vocal_notepad/
├── users/
│   └── views.py         ← Handles /upgrade/, /success/
├── payments/
│   ├── stripe_utils.py  ← Stripe setup, customer & checkout
│   ├── webhooks.py      ← Webhook handlers
│   └── urls.py          ← /stripe/webhook/

Key URLs

PathPurpose
/users/upgrade/Kicks off Stripe checkout
/users/upgrade/success/Landing page after success
/payments/stripe/webhook/Stripe sends payment lifecycle info

With this integration:

  • Users start as 'free'
  • Can upgrade via Stripe securely
  • Your system listens to Stripe’s webhook events to keep user tier in sync
  • Grace periods prevent sudden access loss, improving retention

UPDATE; what is the difference between product_id and price_id?

You’ll notice the stripe checkout function takes a price_id and not product_id.
You can have one product and many prices!
Imagine a monthly price and annual price

Python
              [ Stripe Product: Pro Subscription ]
                          |
    ┌─────────────────────┴────────────────────┐
    ↓                                          ↓
[ Price A: $10/month ]                [ Price B: $100/year ]
  ID: price_monthly_123                ID: price_yearly_456
Python
stripe.checkout.Session.create(
    customer=customer_id,
    line_items=[
        {
            'price': 'price_abc123',  # ✅ Not the product ID!
            'quantity': 1,
        },
    ],
    mode='subscription',
    success_url='https://yourapp.com/success/',
    cancel_url='https://yourapp.com/cancel/',
)