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:
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! 👨🚀
pip install --upgrade django python-dotenv stripe
django-admin startproject core .
python manage.py startapp sim
INSTALLED_APPS
in settings.py:# settings.py
INSTALLED_APPS = [
'sim',
...
]
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()
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.)
endpoint secret
for the webhook (beginning with "whsec_". We'll add this to STRIPE_WEBHOOK_SECRET
in our environment variables.Here's a video of me doing the above:
.env
file in at core/.env
and add your Stripe keys:STRIPE_SECRET_KEY=<sk_test_51>
STRIPE_PUBLIC_KEY=<pk_test_51>
STRIPE_WEBHOOK_SECRET=<whsec_51>
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)
python manage.py makemigrations
python manage.py migrate
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>
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>
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>
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)
)
core/urls.py
:from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('sim.urls')),
]
urls.py
file in your sim
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'),
]
python manage.py runserver
http://localhost:8000/subscribe
4242 4242 4242 4242
and any future date and CVChttp://localhost:8000/success
to see your success pageAfter 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.
stripe login
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]
You've added Stripe subscriptions to your Django app. You can now manage subscriptions and payments in your Django app.
This is also quick to do. You'll simply need to:
.env
file to your live keysThat's it. You're now a step closer to making big bags of cash with your Django app 💰
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).