I'll show you the fastest way to add stripe subscriptions to your Django app.
Here's a video of the final product we'll build, with Stripe subscriptions to slot into your Django app:
Here's an optional video guide (featuring me 🙂) walking through the written guide:
Start: Choose your approach 🚡
- Option 1: Keep an id to the stripe objects. Use this to get your Stripe info when you need it.
- Option 2: Keep copies of the stripe objects in the database, synchronized with Stripe.
Flaws with option 2
1. Duplication: By needing to keep your database synchronised with Stripe's, you:- Add the risk of synchronisation errors, and showing wrong payment data to your users (which they won't like)
- Duplicate Stripe's data (which is easy to access)
3. Users don't care: Users don't care about you having a more sophisticated payment system. Start with the simplest system, and then add complexity if it is clearly needed.
-> For me, option 1 (keeping Stripe ids in your database and then fetching payment and subscription data when needed) clearly wins.
Edit: There's a good guide here from Zach about how to use dj-stripe if you're keen on adding a larger solution immediately.
-> I want to maximise development speed. So, we'll use option 1 below.
Side note: I used this approach for subscriptions for my product Photon Designer:
Edit: Thanks to TwilightOldTimer on Reddit for carefully reading the guide and pointing out two corrections. Now corrected ⭐
Enough chitter chatter. Let's start! 👨🚀
0. Create your Django project
pip install --upgrade django python-dotenv stripe
django-admin startproject core .
python manage.py startapp sim
- Add our app sim to the
INSTALLED_APPS
in settings.py:
# settings.py
INSTALLED_APPS = [
'sim',
...
]
- Add this to the top of your
settings.py
to load your environment variables, including the Stripe keys:
# settings.py
from pathlib import Path
import os
from dotenv import load_dotenv
load_dotenv()
1. Create product on Stripe dashboard
We need to create a test product on the Stripe dashboard to test our subscription.
(Although many, these steps are all simple. See me doing them in 1 min in the below video.)
- Go to the Stripe dashboard
- Toggle the Test mode switch
- Go to the Product catalogue section (Search 'Product catalogue' in the search bar)
- Click 'Add product'
- Set your price lookup key to a unique value
- Activate your customer portal here
- Visit Stripe test webhook to point to your local server here
- Install the Stripe CLI (We'll use this later)
- Note your
endpoint secret
for the webhook (beginning with "whsec_". We'll add this toSTRIPE_WEBHOOK_SECRET
in our environment variables.
Here's a video of me doing the above:
Add your Stripe API key to your environment
- Visit the Developers section of the Stripe dashboard to get your other API keys (we got the webhook secret earlier)
- Create a
.env
file in atcore/.env
and add your Stripe keys:
STRIPE_SECRET_KEY=<sk_test_51>
STRIPE_PUBLIC_KEY=<pk_test_51>
STRIPE_WEBHOOK_SECRET=<whsec_51>
2. Add your model to store the necessary Stripe data
- Add the following to your
models.py
:
from django.db import models
from django.contrib.auth.models import User
class CheckoutSessionRecord(models.Model):
user = models.ForeignKey(
User, on_delete=models.CASCADE, help_text="The user who initiated the checkout."
)
stripe_customer_id = models.CharField(max_length=255)
stripe_checkout_session_id = models.CharField(max_length=255)
stripe_price_id = models.CharField(max_length=255)
has_access = models.BooleanField(default=False)
is_completed = models.BooleanField(default=False)
- Run your migrations:
python manage.py makemigrations
python manage.py migrate
Create subscription page
-
Create a templates folder in
sim
-
Create a file at
sim/templates/subscribe.html
:
<!DOCTYPE html>
<html>
<head>
<title>Subscribe to a cool new product</title>
<script src="https://js.stripe.com/v3/"></script>
</head>
<body>
<header>
<p>
Logged in as {{ request.user.email }}
</p>
</header>
<section>
<!-- Show product details-->
<div class="product">
<div class="description">
<h3>Starter - Monthly tennis ball delivery 🎾</h3>
<h5>$20.00 / month</h5>
</div>
</div>
<!-- Go to checkout button -->
<form class="checkout-form" action="{% url 'create-checkout-session' %}" method="POST">
{% csrf_token %}
<!-- Add a hidden field with the lookup_key of your stripe Price -->
<input type="hidden" name="price_lookup_key" value="standard_monthly" />
<button id="checkout-and-portal-button" type="submit">Checkout</button>
</form>
</section>
</body>
</html>
<style>
.product {
display: flex;
justify-content: center;
padding: 20px 10px;
border: 1px dashed lightgreen;
}
.checkout-form {
display: flex;
justify-content: center;
padding: 20px 10px;
}
</style>
Add a success page with a customer portal
- Create a file
sim/templates/success.html
containing:
<!DOCTYPE html>
<html>
<head>
<title>Thanks for your order!</title>
<link rel="stylesheet" href="style.css">
<script src="client.js" defer></script>
</head>
<body>
<section>
<div class="product Box-root">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="16px" viewBox="0 0 14 16" version="1.1">
<defs/>
<g id="Flow" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="0-Default" transform="translate(-121.000000, -40.000000)" fill="#E184DF">
<path d="M127,50 L126,50 C123.238576,50 121,47.7614237 121,45 C121,42.2385763 123.238576,40 126,40 L135,40 L135,56 L133,56 L133,42 L129,42 L129,56 L127,56 L127,50 Z M127,48 L127,42 L126,42 C124.343146,42 123,43.3431458 123,45 C123,46.6568542 124.343146,48 126,48 L127,48 Z" id="Pilcrow"/>
</g>
</g>
</svg>
<div class="description Box-root">
<h3>Subscription to Starter plan successful!</h3>
</div>
</div>
<div> User = {{request.user}} </div>
<!-- Go to stripe customer portal to let user manage subscription. -->
<form action="{% url 'direct-to-customer-portal' %}" method="POST">
{% csrf_token %}
<input type="hidden" id="session-id" name="session_id" value="" />
<button id="checkout-and-portal-button" type="submit">Manage your billing information</button>
</form>
</section>
</body>
</html>
Add a cancel page
- Create a file
sim/templates/cancel.html
containing:
<!DOCTYPE html>
<html>
<head>
<title>Checkout canceled</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<section>
<p>Picked the wrong subscription? Shop around then come back to pay!</p>
</section>
</body>
</html>
3. Add your views, including the endpoint for the Stripe webhook
- Add the following to your
views.py
(Scroll down to the copy button to copy the whole thing):
import os
import json
from django.shortcuts import render, redirect, reverse
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
import stripe
from django.contrib.auth import login
from django.contrib.auth.models import User
from . import models
DOMAIN = "http://localhost:8000" # Move this to your settings file or environment variable for production.
stripe.api_key = os.environ['STRIPE_SECRET_KEY']
def subscribe(request) -> HttpResponse:
# We login a sample user for the demo.
user, created = User.objects.get_or_create(
username='AlexG', email="alexg@example.com"
)
if created:
user.set_password('password')
user.save()
login(request, user)
request.user = user
return render(request, 'subscribe.html')
def cancel(request) -> HttpResponse:
return render(request, 'cancel.html')
def success(request) -> HttpResponse:
print(f'{request.session = }')
stripe_checkout_session_id = request.GET['session_id']
return render(request, 'success.html')
def create_checkout_session(request) -> HttpResponse:
price_lookup_key = request.POST['price_lookup_key']
try:
prices = stripe.Price.list(lookup_keys=[price_lookup_key], expand=['data.product'])
price_item = prices.data[0]
checkout_session = stripe.checkout.Session.create(
line_items=[
{'price': price_item.id, 'quantity': 1},
# You could add differently priced services here, e.g., standard, business, first-class.
],
mode='subscription',
success_url=DOMAIN + reverse('success') + '?session_id={CHECKOUT_SESSION_ID}',
cancel_url=DOMAIN + reverse('cancel')
)
# We connect the checkout session to the user who initiated the checkout.
models.CheckoutSessionRecord.objects.create(
user=request.user,
stripe_checkout_session_id=checkout_session.id,
stripe_price_id=price_item.id,
)
return redirect(
checkout_session.url, # Either the success or cancel url.
code=303
)
except Exception as e:
print(e)
return HttpResponse("Server error", status=500)
def direct_to_customer_portal(request) -> HttpResponse:
"""
Creates a customer portal for the user to manage their subscription.
"""
checkout_record = models.CheckoutSessionRecord.objects.filter(
user=request.user
).last() # For demo purposes, we get the last checkout session record the user created.
checkout_session = stripe.checkout.Session.retrieve(checkout_record.stripe_checkout_session_id)
portal_session = stripe.billing_portal.Session.create(
customer=checkout_session.customer,
return_url=DOMAIN + reverse('subscribe') # Send the user here from the portal.
)
return redirect(portal_session.url, code=303)
@csrf_exempt
def collect_stripe_webhook(request) -> JsonResponse:
"""
Stripe sends webhook events to this endpoint.
We verify the webhook signature and updates the database record.
"""
webhook_secret = os.environ.get('STRIPE_WEBHOOK_SECRET')
signature = request.META["HTTP_STRIPE_SIGNATURE"]
payload = request.body
try:
event = stripe.Webhook.construct_event(
payload=payload, sig_header=signature, secret=webhook_secret
)
except ValueError as e: # Invalid payload.
raise ValueError(e)
except stripe.error.SignatureVerificationError as e: # Invalid signature
raise stripe.error.SignatureVerificationError(e)
_update_record(event)
return JsonResponse({'status': 'success'})
def _update_record(webhook_event) -> None:
"""
We update our database record based on the webhook event.
Use these events to update your database records.
You could extend this to send emails, update user records, set up different access levels, etc.
"""
data_object = webhook_event['data']['object']
event_type = webhook_event['type']
if event_type == 'checkout.session.completed':
checkout_record = models.CheckoutSessionRecord.objects.get(
stripe_checkout_session_id=data_object['id']
)
checkout_record.stripe_customer_id = data_object['customer']
checkout_record.has_access = True
checkout_record.save()
print('🔔 Payment succeeded!')
elif event_type == 'customer.subscription.created':
print('🎟️ Subscription created')
elif event_type == 'customer.subscription.updated':
print('✍️ Subscription updated')
elif event_type == 'customer.subscription.deleted':
checkout_record = models.CheckoutSessionRecord.objects.get(
stripe_customer_id=data_object['customer']
)
checkout_record.has_access = False
checkout_record.save()
print('✋ Subscription canceled: %s', data_object.id)
)
Add your urls
- Add the following to your
core/urls.py
:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('sim.urls')),
]
- Create a
urls.py
file in yoursim
app and add the following:
from django.contrib import admin
from django.urls import path, include
from sim import views
urlpatterns = [
path('subscribe/', views.subscribe, name='subscribe'),
path('cancel/', views.cancel, name='cancel'),
path('success/', views.success, name='success'),
path('create-checkout-session/', views.create_checkout_session, name='create-checkout-session'),
path('direct-to-customer-portal/', views.direct_to_customer_portal, name='direct-to-customer-portal'),
path('collect-stripe-webhook/', views.collect_stripe_webhook, name='collect-stripe-webhook'),
]
4. Run your server
- Run your server with
python manage.py runserver
- Visit
http://localhost:8000/subscribe
- Click the subscribe button and complete the payment form with the test card number
4242 4242 4242 4242
and any future date and CVC - Then visit
http://localhost:8000/success
to see your success page - Also, click to visit the customer portal to see your subscription
5. Test your endpoint that collect the stripe webhook
After a successful payment, Stripe will send a webhook to your endpoint. This includes events noting the payment and subscription.
We'll test this in development by running the Stripe CLI to send a synthetic event to your endpoint.
- Install the Stripe CLI if you haven't already. This is easy with Homebrew. See the instructions in the link.
- Login to the Stripe CLI
stripe login
- Set your webhook to forward events to our local server. Change this if you're not using port 8000.
stripe listen --forward-to localhost:8000/collect-stripe-webhook/
Then run your server in a separate terminal, make a payment, and see the webhook event in your terminal. You should see something like this.
2050-09-29 15:00:00 <-- [200] POST http://localhost:8000/collect-stripe-webhook/ [evt_1J4]
Complete ✅ You've added Stripe subscriptions to your Django app
You've added Stripe subscriptions to your Django app. You can now manage subscriptions and payments in your Django app.
How to deploy to production with real money 💵
This is also quick to do. You'll simply need to:
- Change your Stripe keys in your
.env
file to your live keys - Add a new product on the Stripe dashboard in live mode
- Update your DOMAIN to your production domain (Use an environment variable or settings file to vary this)
That's it. You're now a step closer to making big bags of cash with your Django app 💰
P.S Photon Designer
Do you dream of creating Django products so quickly they break the space-time continuum? I'm building: Photon Designer. It lets you create Django UI faster than a cat jumps away from a cucumber 🥒
If you'd like to create your Django UI faster, check out Photon Designer - and prepare to build your UI faster than a photon escaping a black hole (In a friendly way).