Django Stripe Subscriptions

Last updated October 5th, 2020

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:

  1. Fixed-price subscriptions
  2. Future payments

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:

  1. Redirect the user to Stripe Checkout (with mode=subscription)
  2. Create a webhook that listens for checkout.session.completed
  3. After webhook is called, save relevant data to your database

The future payments approach is harder to set up, but this approach give 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:

  1. Redirect the user to Stripe Checkout (with mode=setup) to collect payment information
  2. Create a webhook that listens for checkout.session.completed
  3. After webhook is called, save relevant data to your database
  4. 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:

  1. billing cycle anchor (timestamp of subscription's creation)
  2. 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.8 -m venv env
$ source env/bin/activate

(env)$ pip install django
(env)$ django-admin.py 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

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" in the left sidebar and then from the dropdown list click on "API keys":

Stripe Developers Key

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" in the left sidebar and then "Add product".

Add a product name and description, enter a price, and select "Recurring":

Stripe Add Product

Click "Save product".

Next, grab the API ID of the price:

Stripe Add Product

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 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 and subscriptionId 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 -- e.g., 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:

Stripe Checkout

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 (i.e., 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:

  1. Set up the webhook endpoint
  2. Test the endpoint using the Stripe CLI
  3. 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:

Django webhook

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:

Subscribed View

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, or unpaid.

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.

Nik Tomazic

Nik Tomazic

Nik is a software developer from Slovenia. He's interested in object-oriented programming and web development. He likes learning new things and accepting new challenges. When he's not coding, Nik's either swimming or watching movies.

Share this tutorial

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.