This tutorial looks at how to handle subscription payments with Django and Stripe.
Need to accept one-time payments? Check out Django Stripe Tutorial.
Contents
Stripe Subscription Payment Options
There are multiple ways to implement and handle Stripe subscriptions, but the two most well-known are:
In both cases, you can either use Stripe Checkout (which is a Stripe-hosted checkout page) or Stripe Elements (which are a set of custom UI components used to build payment forms). Use Stripe Checkout if you don't mind redirecting your users to a Stripe-hosted page and want Stripe to handle most of the payment process for you (e.g., customer and payment intent creation, etc.), otherwise use Stripe Elements.
The fixed-price approach is much easier to set up, but you don't have full control over the billing cycles and payments. By using this approach Stripe will automatically start charging your customers every billing cycle after a successful checkout.
Fixed-price steps:
- Redirect the user to Stripe Checkout (with
mode=subscription
) - Create a webhook that listens for
checkout.session.completed
- After the webhook is called, save relevant data to your database
The future payments approach is harder to set up, but this approach gives you full control over the subscriptions. You collect customer details and payment information in advance and charge your customers at a future date. This approach also allows you to sync billing cycles so that you can charge all of your customers on the same day.
Future payments steps:
- Redirect the user to Stripe Checkout (with
mode=setup
) to collect payment information - Create a webhook that listens for
checkout.session.completed
- After the webhook is called, save relevant data to your database
- From there, you can charge the payment method at a future date using the Payment Intents API
In this tutorial, we'll use the fixed-price approach with Stripe Checkout.
Billing Cycles
Before jumping in, it's worth noting that Stripe doesn't have a default billing frequency. Every Stripe subscription's billing date is determined by the following two factors:
- billing cycle anchor (timestamp of subscription's creation)
- recurring interval (daily, monthly, yearly, etc.)
For example, a customer with a monthly subscription set to cycle on the 2nd of the month will always be billed on the 2nd.
If a month doesn’t have the anchor day, the subscription will be billed on the last day of the month. For example, a subscription starting on January 31 bills on February 28 (or February 29 in a leap year), then March 31, April 30, and so on.
To learn more about billing cycles, refer to the Setting the subscription billing cycle date page from the Stripe documentation.
Project Setup
Let's start off by creating a new directory for our project. Inside the directory we'll create and activate a new virtual environment, install Django, and create a new Django project using django-admin:
$ mkdir django-stripe-subscriptions && cd django-stripe-subscriptions
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install django
(env)$ django-admin startproject djangostripe .
After that, create a new app called subscriptions
:
(env)$ python manage.py startapp subscriptions
Register the app in djangostripe/settings.py under INSTALLED_APPS
:
# djangostripe/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'subscriptions.apps.SubscriptionsConfig', # new
]
Create a new view called home
, which will serve as our main index page:
# subscriptions/views.py
from django.shortcuts import render
def home(request):
return render(request, 'home.html')
Assign a URL to the view by adding the following to subscriptions/urls.py:
# subscriptions/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='subscriptions-home'),
]
Now, let's tell Django that the subscriptions
app has its own URLs inside the main application:
# djangostripe/urls.py
from django.contrib import admin
from django.urls import path, include # new
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('subscriptions.urls')), # new
]
Finally, create a new template called home.html inside a new folder called "templates". Add the following HTML to the template:
<!-- templates/home.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Subscriptions</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<button type="submit" class="btn btn-primary" id="submitBtn">Subscribe</button>
</div>
</body>
</html>
Make sure to update the settings.py file so Django knows to look for a "templates" folder:
# djangostripe/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates'], # new
...
Finally run migrate
to sync the database and runserver
to start Django's local web server.
(env)$ python manage.py migrate
(env)$ python manage.py runserver
Visit http://localhost:8000/ in your browser of choice. You should see the "Subscribe" button which we'll later use to redirect customers to the Stripe Checkout page.
Add Stripe
With the base project ready, let's add Stripe. Install the latest version:
(env)$ pip install stripe
Next, register for a Stipe account (if you haven't already done so) and navigate to the dashboard. Click on "Developers" and then from the list in the left sidebar click on "API keys":
Each Stripe account has four API keys: two for testing and two for production. Each pair has a "secret key" and a "publishable key". Do not reveal the secret key to anyone; the publishable key will be embedded in the JavaScript on the page that anyone can see.
Currently the toggle for "Viewing test data" in the upper right indicates that we're using the test keys now. That's what we want.
At the bottom of your settings.py file, add the following two lines including your own test secret and publishable keys. Make sure to include the ''
characters around the actual keys.
# djangostripe/settings.py
STRIPE_PUBLISHABLE_KEY = '<enter your stripe publishable key>'
STRIPE_SECRET_KEY = '<enter your stripe secret key>'
Finally, you'll need to specify an "Account name" within your "Account settings" at https://dashboard.stripe.com/settings/account.
Create a Product
Next, let's create a subscription product to sell.
Click "Products" and then "Add product".
Add a product name and description, enter a price, and select "Recurring":
Click "Save product".
Next, grab the API ID of the price:
Save the ID in the settings.py file like so:
# djangostripe/settings.py
STRIPE_PRICE_ID = '<enter your stripe price id>'
Authentication
In order to associate Django users with Stripe customers and implement subscription management in the future, we'll need to enforce user authentication before allowing customers to subscribe to the service. We can achieve this by adding a @login_required
decorator to all views that require authentication.
Let's first protect the home
view:
# subscriptions/views.py
from django.contrib.auth.decorators import login_required # new
from django.shortcuts import render
@login_required # new
def home(request):
return render(request, 'home.html')
Now, when non-authenticated users try to access the home
view, they will be redirected to the LOGIN_REDIRECT_URL
defined in settings.py.
If you have a preferred authentication system, set that up now and configure the LOGIN_REDIRECT_URL
, otherwise hop to the next section to install django-allauth.
django-allauth (Optional)
django-allauth is one of the most popular Django packages for addressing authentication, registration, account management, and third-party account authentication. We'll use it to configure a simple register/login system.
First, install the package:
(env)$ pip install django-allauth
Update the INSTALLED_APPS
in djangostripe/settings.py like so:
# djangostripe/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites', # new
'allauth', # new
'allauth.account', # new
'allauth.socialaccount', # new
'subscriptions.apps.SubscriptionsConfig',
]
Next, add the following django-allauth config to djangostripe/settings.py:
# djangostripe/settings.py
AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, regardless of `allauth`
'django.contrib.auth.backends.ModelBackend',
# `allauth` specific authentication methods, such as login by e-mail
'allauth.account.auth_backends.AuthenticationBackend',
]
# We have to set this variable, because we enabled 'django.contrib.sites'
SITE_ID = 1
# User will be redirected to this page after logging in
LOGIN_REDIRECT_URL = '/'
# If you don't have an email server running yet add this line to avoid any possible errors.
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
Register the allauth URLs:
# djangostripe/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('subscriptions.urls')),
path('accounts/', include('allauth.urls')), # new
]
Apply the migrations:
(env)$ python manage.py migrate
Test out auth by running the server and navigating to http://localhost.com:8000/. You should be redirected to the sign up page. Create an account and then log in.
Database Model
In order to handle customers and subscriptions correctly we'll need to store some information in our database. Let's create a new model called StripeCustomer
which will store Stripe's customerId
and subscriptionId
and relate it back the Django auth user. This will allow us to fetch our customer and subscription data from Stripe.
We could theoretically fetch the
customerId
andsubscriptionId
from Stripe every time we need them, but that would greatly increase our chance of getting rate limited by Stripe.
Let's create our model inside subscriptions/models.py:
# subscriptions/models.py
from django.contrib.auth.models import User
from django.db import models
class StripeCustomer(models.Model):
user = models.OneToOneField(to=User, on_delete=models.CASCADE)
stripeCustomerId = models.CharField(max_length=255)
stripeSubscriptionId = models.CharField(max_length=255)
def __str__(self):
return self.user.username
Register it with the admin in subscriptions/admin.py:
# subscriptions/admin.py
from django.contrib import admin
from subscriptions.models import StripeCustomer
admin.site.register(StripeCustomer)
Create and apply the migrations:
(env)$ python manage.py makemigrations && python manage.py migrate
Get Publishable Key
JavaScript Static File
Start by creating a new static file to hold all of our JavaScript:
(env)$ mkdir static
(env)$ touch static/main.js
Add a quick sanity check to the new main.js file:
// static/main.js
console.log("Sanity check!");
Then update the settings.py file so Django knows where to find static files:
# djangostripe/settings.py
STATIC_URL = 'static/'
# for django >= 3.1
STATICFILES_DIRS = [Path(BASE_DIR).joinpath('static')] # new
# for django < 3.1
# STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # new
Add the static template tag along with the new script tag inside the HTML template:
<!-- templates/home.html -->
{% load static %} <!-- new -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Subscriptions</title>
<script src="https://js.stripe.com/v3/"></script> <!-- new -->
<script src="{% static 'main.js' %}"></script> <!-- new -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<button type="submit" class="btn btn-primary" id="submitBtn">Subscribe</button>
</div>
</body>
</html>
Run the development server again. Navigate to http://localhost:8000/, and open up the JavaScript console. You should see the sanity check inside your console.
View
Next, add a new view to subscriptions/views.py to handle the AJAX request:
# subscriptions/views.py
from django.conf import settings # new
from django.contrib.auth.decorators import login_required
from django.http.response import JsonResponse # new
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt # new
@login_required
def home(request):
return render(request, 'home.html')
# new
@csrf_exempt
def stripe_config(request):
if request.method == 'GET':
stripe_config = {'publicKey': settings.STRIPE_PUBLISHABLE_KEY}
return JsonResponse(stripe_config, safe=False)
Add a new URL as well:
# subscriptions/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='subscriptions-home'),
path('config/', views.stripe_config), # new
]
AJAX Request
Next, use the Fetch API to make an AJAX request to the new /config/
endpoint in static/main.js:
// static/main.js
console.log("Sanity check!");
// new
// Get Stripe publishable key
fetch("/config/")
.then((result) => { return result.json(); })
.then((data) => {
// Initialize Stripe.js
const stripe = Stripe(data.publicKey);
});
The response from a fetch
request is a ReadableStream. result.json()
returns a promise, which we resolved to a JavaScript object -- i.e., data
. We then used dot-notation to access the publicKey
in order to obtain the publishable key.
Create Checkout Session
Moving on, we need to attach an event handler to the button's click event which will send another AJAX request to the server to generate a new Checkout Session ID.
View
First, add the new view:
# subscriptions/views.py
@csrf_exempt
def create_checkout_session(request):
if request.method == 'GET':
domain_url = 'http://localhost:8000/'
stripe.api_key = settings.STRIPE_SECRET_KEY
try:
checkout_session = stripe.checkout.Session.create(
client_reference_id=request.user.id if request.user.is_authenticated else None,
success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
cancel_url=domain_url + 'cancel/',
payment_method_types=['card'],
mode='subscription',
line_items=[
{
'price': settings.STRIPE_PRICE_ID,
'quantity': 1,
}
]
)
return JsonResponse({'sessionId': checkout_session['id']})
except Exception as e:
return JsonResponse({'error': str(e)})
Here, if the request method is GET
, we defined a domain_url
, assigned the Stripe secret key to stripe.api_key
(so it will be sent automatically when we make a request to create a new Checkout Session), created the Checkout Session, and sent the ID back in the response. Take note of the success_url
and cancel_url
. The user will be redirected back to those URLs in the event of a successful payment or cancellation, respectively. We'll set those views up shortly.
Don't forget the import:
import stripe
The full file should now look like this:
# subscriptions/views.py
import stripe
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http.response import JsonResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
@login_required
def home(request):
return render(request, 'home.html')
@csrf_exempt
def stripe_config(request):
if request.method == 'GET':
stripe_config = {'publicKey': settings.STRIPE_PUBLISHABLE_KEY}
return JsonResponse(stripe_config, safe=False)
@csrf_exempt
def create_checkout_session(request):
if request.method == 'GET':
domain_url = 'http://localhost:8000/'
stripe.api_key = settings.STRIPE_SECRET_KEY
try:
checkout_session = stripe.checkout.Session.create(
client_reference_id=request.user.id if request.user.is_authenticated else None,
success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
cancel_url=domain_url + 'cancel/',
payment_method_types=['card'],
mode='subscription',
line_items=[
{
'price': settings.STRIPE_PRICE_ID,
'quantity': 1,
}
]
)
return JsonResponse({'sessionId': checkout_session['id']})
except Exception as e:
return JsonResponse({'error': str(e)})
AJAX Request
Register the checkout session URL:
# subscriptions/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='subscriptions-home'),
path('config/', views.stripe_config),
path('create-checkout-session/', views.create_checkout_session), # new
]
Add the event handler and subsequent AJAX request to static/main.js:
// static/main.js
console.log("Sanity check!");
// Get Stripe publishable key
fetch("/config/")
.then((result) => { return result.json(); })
.then((data) => {
// Initialize Stripe.js
const stripe = Stripe(data.publicKey);
// new
// Event handler
let submitBtn = document.querySelector("#submitBtn");
if (submitBtn !== null) {
submitBtn.addEventListener("click", () => {
// Get Checkout Session ID
fetch("/create-checkout-session/")
.then((result) => { return result.json(); })
.then((data) => {
console.log(data);
// Redirect to Stripe Checkout
return stripe.redirectToCheckout({sessionId: data.sessionId})
})
.then((res) => {
console.log(res);
});
});
}
});
Here, after resolving the result.json()
promise, we called redirectToCheckout with the Checkout Session ID from the resolved promise.
Navigate to http://localhost:8000/. On button click you should be redirected to an instance of Stripe Checkout (a Stripe-hosted page to securely collect payment information) with the subscription information:
We can test the form by using one of the several test card numbers that Stripe provides. Let's use 4242 4242 4242 4242
. Make sure the expiration date is in the future. Add any 3 numbers for the CVC and any 5 numbers for the postal code. Enter any email address and name. If all goes well, the payment should be processed and you should be subscribed, but the redirect will fail since we have not set up the /success/
URL yet.
User Redirect
Next, we'll create success and cancel views and redirect the user to the appropriate page after checkout.
Views:
# subscriptions/views.py
@login_required
def success(request):
return render(request, 'success.html')
@login_required
def cancel(request):
return render(request, 'cancel.html')
Create the success.html and cancel.html templates as well.
Success:
<!-- templates/success.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Subscriptions</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<p>You have successfully subscribed!</p>
<p><a href="{% url "subscriptions-home" %}">Return to the dashboard</a></p>
</div>
</body>
</html>
Cancel:
<!-- templates/cancel.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Subscriptions</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<p>You have cancelled the checkout.</p>
<p><a href="{% url "subscriptions-home" %}">Return to the dashboard</a></p>
</div>
</body>
</html>
Register the new views inside subscriptions/urls.py:
# subscriptions/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='subscriptions-home'),
path('config/', views.stripe_config),
path('create-checkout-session/', views.create_checkout_session),
path('success/', views.success), # new
path('cancel/', views.cancel), # new
]
The user should now be redirected to /success
if the payment goes through and cancel/
if the payment fails. Test this out.
Stripe Webhooks
Our app works well at this point, but we still can't programmatically confirm payments. We also haven't added a new customer to the StripeCustomer
model when a customer subscribes successfully. We already redirect the user to the success page after they check out, but we can't rely on that page alone (as confirmation) since payment confirmation happens asynchronously.
There are two types of events in Stripe and programming in general. Synchronous events, which have an immediate effect and results (e.g., creating a customer), and asynchronous events, which don't have an immediate result (e.g., confirming payments). Because payment confirmation is done asynchronously, the user might get redirected to the success page before their payment is confirmed and before we receive their funds.
One of the easiest ways to get notified when the payment goes through is to use a callback or so-called Stripe webhook. We'll need to create a simple endpoint in our application, which Stripe will call whenever an event occurs (e.g., when a user subscribes). By using webhooks, we can be absolutely sure that the payment went through successfully.
In order to use webhooks, we need to:
- Set up the webhook endpoint
- Test the endpoint using the Stripe CLI
- Register the endpoint with Stripe
Endpoint
Create a new view called stripe_webhook
which will create a new StripeCustomer
every time someone subscribes to our service:
# subscriptions/views.py
@csrf_exempt
def stripe_webhook(request):
stripe.api_key = settings.STRIPE_SECRET_KEY
endpoint_secret = settings.STRIPE_ENDPOINT_SECRET
payload = request.body
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
event = None
try:
event = stripe.Webhook.construct_event(
payload, sig_header, endpoint_secret
)
except ValueError as e:
# Invalid payload
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return HttpResponse(status=400)
# Handle the checkout.session.completed event
if event['type'] == 'checkout.session.completed':
session = event['data']['object']
# Fetch all the required data from session
client_reference_id = session.get('client_reference_id')
stripe_customer_id = session.get('customer')
stripe_subscription_id = session.get('subscription')
# Get the user and create a new StripeCustomer
user = User.objects.get(id=client_reference_id)
StripeCustomer.objects.create(
user=user,
stripeCustomerId=stripe_customer_id,
stripeSubscriptionId=stripe_subscription_id,
)
print(user.username + ' just subscribed.')
return HttpResponse(status=200)
stripe_webhook
now serves as our webhook endpoint. Here, we're only looking for checkout.session.completed
events which are called whenever a checkout is successful, but you can use the same pattern for other Stripe events.
Make the following changes to the imports:
# subscriptions/views.py
import stripe
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User # new
from django.http.response import JsonResponse, HttpResponse # updated
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from subscriptions.models import StripeCustomer # new
The only thing left do do to make the endpoint accessible is to register it in urls.py:
# subscriptions/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='subscriptions-home'),
path('config/', views.stripe_config),
path('create-checkout-session/', views.create_checkout_session),
path('success/', views.success),
path('cancel/', views.cancel),
path('webhook/', views.stripe_webhook), # new
]
Testing the webhook
We'll use the Stripe CLI to test the webhook.
Once downloaded and installed, run the following command in a new terminal window to log in to your Stripe account:
$ stripe login
This command should generate a pairing code:
Your pairing code is: peach-loves-classy-cozy
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser (^C to quit)
By pressing Enter, the CLI will open your default web browser and ask for permission to access your account information. Go ahead and allow access. Back in your terminal, you should see something similar to:
> Done! The Stripe CLI is configured for Django Test with account id acct_<ACCOUNT_ID>
Please note: this key will expire after 90 days, at which point you'll need to re-authenticate.
Next, we can start listening to Stripe events and forward them to our endpoint using the following command:
$ stripe listen --forward-to localhost:8000/webhook/
This will also generate a webhook signing secret:
> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (^C to quit)
In order to initialize the endpoint, add the secret to the settings.py file:
# djangostribe/settings.py
STRIPE_ENDPOINT_SECRET = '<your webhook signing secret here>'
Stripe will now forward events to our endpoint. To test, run another test payment through with 4242 4242 4242 4242
. In your terminal, you should see the <USERNAME> just subscribed.
message.
Once done, stop the stripe listen --forward-to localhost:8000/webhook/
process.
Register the endpoint
Finally, after deploying your app, you can register the endpoint in the Stripe dashboard, under Developers > Webhooks. This will generate a webhook signing secret for use in your production app.
For example:
Fetch Subscription Data
Our app now allows users to subscribe to our service, but we still have no way to fetch their subscription data and display it.
Update the home
view:
# subscriptions/views.py
@login_required
def home(request):
try:
# Retrieve the subscription & product
stripe_customer = StripeCustomer.objects.get(user=request.user)
stripe.api_key = settings.STRIPE_SECRET_KEY
subscription = stripe.Subscription.retrieve(stripe_customer.stripeSubscriptionId)
product = stripe.Product.retrieve(subscription.plan.product)
# Feel free to fetch any additional data from 'subscription' or 'product'
# https://stripe.com/docs/api/subscriptions/object
# https://stripe.com/docs/api/products/object
return render(request, 'home.html', {
'subscription': subscription,
'product': product,
})
except StripeCustomer.DoesNotExist:
return render(request, 'home.html')
Here, if a StripeCustomer
exists, we use the subscriptionId
to fetch the customer's subscription and product info from the Stripe API.
Modify the home.html template to display the current plan to subscribed users:
<!-- templates/home.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Subscriptions</title>
<script src="https://js.stripe.com/v3/"></script>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
<script src="{% static 'main.js' %}"></script>
</head>
<body>
<div class="container mt-5">
{% if subscription.status == "active" %}
<h4>Your subscription:</h4>
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">{{ product.name }}</h5>
<p class="card-text">
{{ product.description }}
</p>
</div>
</div>
{% else %}
<button type="submit" class="btn btn-primary" id="submitBtn">Subscribe</button>
{% endif %}
</div>
</body>
</html>
Our subscribed customers will now see their current subscription plan, while others will still see the subscribe button:
Restricting user access
If you want to restrict access to specific views to only subscribed users, you can fetch the subscription as we did in the previous step and check if subscription.status == "active"
. By performing this check you will make sure the subscription is still active, which means that it has been paid and hasn't been cancelled.
Other possible subscription statuses are
incomplete
,incomplete_expired
,trialing
,active
,past_due
,canceled
, orunpaid
.
Conclusion
We have successfully created a Django web application that allows users to subscribe to our service and view their plan. Our customers will also be automatically billed every month.
This is just the basics. You'll still need to:
- Allow users to manage/cancel their current plan
- Handle future payment failures
You'll also want to use environment variables for the domain_url
, API keys, and webhook signing secret rather than hardcoding them.
Grab the code from the django-stripe-subscriptions repo on GitHub.