Add Stripe subscriptions to Django in 7 minutes π΅
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
- Duplication: By needing to keep your database synchronized with Stripe's, you:
Add the risk of synchronization errors, and showing wrong payment data to your users (which they won't like)
- Duplicate Stripe's data (which is easy to access)
-
Extra code cost: You need to maintain extra code to synchronize the data. If you use dj-stripe or another third-party package to do this, then you rely on their extra code to synchronize correctly.
-
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 stripedjango-admin startproject core .python manage.py startapp sim
- Add our app sim to the
INSTALLED_APPS
in settings.py:
# settings.pyINSTALLED_APPS = ['sim',...]
- Add this to the top of your
settings.py
to load your environment variables, including the Stripe keys:
# settings.pyfrom pathlib import Pathimport osfrom dotenv import load_dotenvload_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 modelsfrom django.contrib.auth.models import Userclass 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 makemigrationspython 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 --><formclass="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"><svgxmlns="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 /><gid="Flow"stroke="none"stroke-width="1"fill="none"fill-rule="evenodd"><gid="0-Default"transform="translate(-121.000000, -40.000000)"fill="#E184DF"><pathd="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 osimport jsonfrom django.shortcuts import render, redirect, reversefrom django.http import HttpResponse, JsonResponsefrom django.views.decorators.csrf import csrf_exemptimport stripefrom django.contrib.auth import loginfrom django.contrib.auth.models import Userfrom . import modelsDOMAIN = "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 = userreturn 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_exemptdef 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.bodytry: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 signatureraise 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 = Truecheckout_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 = Falsecheckout_record.save()print('β Subscription canceled: %s', data_object.id))
Add your urls
- Add the following to your
core/urls.py
:
from django.contrib import adminfrom django.urls import path, includeurlpatterns = [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 adminfrom django.urls import path, includefrom sim import viewsurlpatterns = [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 π°