Using Django and Stripe for Monthly Subscriptions

June 13, 2020, 5:45 p.m.
Using Django and Stripe for Monthly Subscriptions
Last Modified: Oct. 15, 2020, 1:48 p.m.

If you are familiar with Stripe, you know how big of a player they are in the online payment processing field.  Their API not only allows programmers to easily create one-time payments for sites such as e-commerce stores but also provides quick integrations for monthly subscriptions and routing payouts.  If new to Django and Stripe, check out our recent article on integrating one-time payments.  Otherwise, let's get into setting up monthly payments with Django and Stripe.


Why a Monthly Subscription?

Monthly subscriptions are a common practice found across the web especially among companies that promote software-as-a-service(SAAS) as their delivery model.  For example, SAAS companies such as Hubspot (marketing), Dropbox (data storage), and Mailchimp (email marketing) all offer tiered pricing options to their prospective customers.  Many consider this model to be favorable given that revenue is easier to predict once basic metrics are calculated (customer acquisition cost, lifetime value, churn rate).  Predictable revenue of course creates stability and produces more accurate financial forecasts.  

Mailchimp pricing page

sass pricing


Django Setup

Start by setting up a virtual environment and creating a basic Django project.  We'll create a new virtual environment called saas.  Note: on Mac, use python3 instead of py for all commands

C:\Users\Owner\Desktop\code>py -m venv saas


Next change the directory to the virtual environment, install Django and set up your project and app.

C:\Users\Owner\Desktop\code>cd saas


(saas) C:\Users\Owner\Desktop\code\saas>pip install Django

(saas) C:\Users\Owner\Desktop\code\saas>django-admin startproject mysite

(saas) C:\Users\Owner\Desktop\code\saas\mysite>py startapp main


Add the main app to in mysite.

    'main.apps.MainConfig', #add this

Create in the main folder and include in mysite >


mysite >

from django.contrib import admin
from django.urls import path, include #add include

urlpatterns = [
  path('', include('main.urls')),  #add path


Django Stripe Integration

Start by installing the official library for connecting to Stripe's API.  

pip install --upgrade stripe


Next, create a Stripe account and create products on their dashboard.  While this can be performed using Stripe CLI's we'll use the dashboard since we are already on the page from creating an account.  Make sure you are in test mode and can only view test data.  By default, you should be in test mode after creating an account.  Click products on the left-hand side navigation menu.  Create two new products:

  • Name the product Premium Plan
  • Add description "paid plan for advanced features"
  • Use standard pricing
  • Enter $15.00 and ensure "recurring" is selected
  • Keep billing period as "monthly"


  • Name the product Enterprise Plan
  • Add description "enterprise plan for advanced features"
  • Use standard pricing
  • Enter $30.00 and ensure "recurring" is selected
  • Keep billing period as "monthly"

stripe products


Now let's sync the product in the Stripe dashboard.  While we could create a model and store the relevant product information, such as product id, as model fields, an easier solution is to simply install the dj-stripe package and use the sync command.  We also add need to add our API keys in  Note your live keys should always be secured and never listed in the settings.  For more information on securing environment variables, check out Secure Sensitive Django Variables.

pip install dj-stripe


STRIPE_TEST_PUBLIC_KEY ='your_pk_test'
STRIPE_TEST_SECRET_KEY = 'your_secret_key'
STRIPE_LIVE_MODE = False  # Change to True in production


Finally, migrate the database. Note: if the database is SQLite, migrating make take longer than usual.  

py migrate


Add products to the database automatically with the following command:

py djstripe_sync_plans_from_stripe


View the admin page to see the changes we just made.  First, create an admin user. Include an email such as since we'll use this as the Stripe customer name then visit  You should see a variety of new models including our newly created products as well as plans for the products.  

py createsuperuser

stripe admin


Creating a Checkout Page

Now that we have our data synced, let's create a checkout page where users can select their plan and checkout. We'll start with the view and checkout template. Note: we have imported the Bootstrap CDN in home.html:

from django.shortcuts import render, redirect
import stripe
import json
from django.http import JsonResponse
from djstripe.models import Product
from django.contrib.auth.decorators import login_required

# Create your views here.
def homepage(request):
  return render(request, "home.html")

def checkout(request):
  products = Product.objects.all()
  return render(request,"checkout.html",{"products": products})


{% extends "home.html" %}

{% block content %}
<script src=""></script>


<div class="container ">

  <div class="row ">
    {% for p in products %}
    <div class="col-6">
      <div class="card mx-5 shadow" style="border-radius: 10px; border:none; ">
        <div class="card-body">
          <h5 class="card-title font-weight-bold">{{}}</h5>
          <p class="card-text text-muted"><svg class="bi bi-check" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="">
            <path fill-rule="evenodd" d="M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.236.236 0 0 1 .02-.022z"/>

          {% for plan in p.plan_set.all %}
          <h5 >{{ plan.human_readable_price }}</h5>
          <div class="text-right">
            <input type="checkbox" name="{{}}" value="{{}}" onclick="planSelect('{{}}' ,'{{plan.human_readable_price}}', '{{}}')">
          {% endfor %}


    {% endfor %}
    <div class="row">
      <div class="col-12">
        <div class="card mx-5 shadow rounded" style="border-radius:50px;border:none">
          <div class="card-body">
            <h5 class="card-title font-weight-bold">Checkout</h5>
            <p class="text-muted ">Enter card details.  Your subscription will start immediately</p>
            <div class="row">
              <div class="col-6 text-muted">
              <div class="col-6 text-right">
                <p id="plan"></p>
                <p id="price"></p>
                <p hidden id="priceId"></p>

            <form id="subscription-form" >
              <div id="card-element" class="MyCardElement">
                <!-- Elements will create input elements here -->
              <!-- We'll put the error messages in this element -->
              <div id="card-errors" role="alert"></div>
              <button id="submit" type="submit">
                <div class="spinner-border  spinner-border-sm text-light hidden" id="spinner" role="status">
                  <span class="sr-only">Loading...</span>
                <span id="button-text">Subscribe</span>



{% endblock %}


Now that we have a template for selecting the subscription plan. Let's add some styling and set up the credit card form by using Stripe Elements, a pre-built set of UI components.

body {

    .StripeElement {
      box-sizing: border-box;

      height: 40px;

      padding: 10px 12px;

      border: 1px solid transparent;
      border-radius: 4px;
      background-color: white;

      box-shadow: 0 1px 3px 0 #e6ebf1;
      -webkit-transition: box-shadow 150ms ease;
      transition: box-shadow 150ms ease;

    .StripeElement--focus {
      box-shadow: 0 1px 3px 0 #cfd7df;

    .StripeElement--invalid {
      border-color: #fa755a;

    .StripeElement--webkit-autofill {
      background-color: #fefde5 !important;
    .hidden {
        display: none;

    #submit:hover {
      filter: contrast(120%);

    #submit {
      font-feature-settings: "pnum";
      --body-color: #f7fafc;
      --button-color: #556cd6;
      --accent-color: #556cd6;
      --gray-border: #e3e8ee;
      --link-color: #fff;
      --font-color: #697386;
      --body-font-family: -apple-system,BlinkMacSystemFont,sans-serif;
      --radius: 4px;
      --form-width: 400px;
      -webkit-box-direction: normal;
      word-wrap: break-word;
      box-sizing: border-box;
      font: inherit;
      overflow: visible;
      -webkit-appearance: button;
      -webkit-font-smoothing: antialiased;
      margin: 0;
      font-family: inherit;
      -webkit-tap-highlight-color: transparent;
      font-size: 16px;
      padding: 0 12px;
      line-height: 32px;
      outline: none;
      text-decoration: none;
      text-transform: none;
      margin-right: 8px;
      height: 36px;
      border-radius: var(--radius);
      color: #fff;
      border: 0;
      margin-top: 16px;
      font-weight: 600;
      cursor: pointer;
      transition: all .2s ease;
      display: block;
      box-shadow: 0 4px 5.5px 0 rgba(0,0,0,.07);
      width: 100%;
      background: var(--button-color);

document.getElementById("submit").disabled = true;


function stripeElements() {
  stripe = Stripe('pk_test_E52Nh0gTCRpJ7h4JhuEX7BIO006LVew6GG');

  if (document.getElementById('card-element')) {
    let elements = stripe.elements();

    // Card Element styles
    let style = {
      base: {
        color: "#32325d",
        fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
        fontSmoothing: "antialiased",
        fontSize: "16px",
        "::placeholder": {
          color: "#aab7c4"
      invalid: {
        color: "#fa755a",
        iconColor: "#fa755a"

    card = elements.create('card', { style: style });


    card.on('focus', function () {
      let el = document.getElementById('card-errors');

    card.on('blur', function () {
      let el = document.getElementById('card-errors');

    card.on('change', function (event) {
  //we'll add payment form handling here

function displayError(event) {
  let displayError = document.getElementById('card-errors');
  if (event.error) {
    displayError.textContent = event.error.message;
  } else {
    displayError.textContent = '';

SAAS checkout


This should allow the credit card form to render.  First, we disabled the subscribe button and then called stripeElements() to create and then mount the credit card form.  Next, we add a small script to update the template with the selected subscription plan and to re-enable the subscribe button if a plan is selected.

 function planSelect(name, price, priceId) {
    var inputs = document.getElementsByTagName('input');

    for(var i = 0; i<inputs.length; i++){
      inputs[i].checked = false;
      if(inputs[i].name== name){

        inputs[i].checked = true;

    var n = document.getElementById('plan');
    var p = document.getElementById('price');
    var pid = document.getElementById('priceId');
    n.innerHTML = name;
    p.innerHTML = price;
    pid.innerHTML = priceId;
        document.getElementById("submit").disabled = false;


saas price selected


Now we need to handle the payment form submission. We'll add the first code block to the end of the stripeElements() function.  First, grab the form by its ID and add an event listener.  After preventing the default form submission, we change the loading state of the form to prevent any issues associated with double-clicking the subscribe button.  Finally, with the inputted card information, we create a payment method and submit this data to our server along with the price id of the selected subscription.  As demonstrated, we use Django's CSRF token to authorize the submission to our server.  Note: Stripe's documentation includes some additional validation that you should check out if interested.  Also, unlike the documentation, we create a customer and subscription in one request to the server instead of sending separate requests.

  //we'll add payment form handling here
    let paymentForm = document.getElementById('subscription-form');
  if (paymentForm) {

    paymentForm.addEventListener('submit', function (evt) {

        // create new payment method & create subscription
        createPaymentMethod({ card });


function createPaymentMethod({ card }) {

  // Set up payment method for recurring usage
  let billingName = '{{user.username}}';

      type: 'card',
      card: card,
      billing_details: {
        name: billingName,
    .then((result) => {
      if (result.error) {
      } else {
       const paymentParams = {
          price_id: document.getElementById("priceId").innerHTML,
      fetch("/create-sub", {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRFToken':'{{ csrf_token }}',
        credentials: 'same-origin',
        body: JSON.stringify(paymentParams),
      }).then((response) => {
        return response.json(); 
      }).then((result) => {
        if (result.error) {
          // The card had an error when trying to attach it to a customer
          throw result;
        return result;
      }).then((result) => {
        if (result && result.status === 'active') {

         window.location.href = '/complete';
      }).catch(function (error) {


var changeLoadingState = function(isLoading) {
  if (isLoading) {
    document.getElementById("submit").disabled = true;
  } else {
    document.getElementById("submit").disabled = false;


Add URL paths for creating a subscription/customer and when payment is complete.


from django.urls import path
from . import views

app_name = "main"   

urlpatterns = [
  path("", views.homepage, name="homepage"),
  path("checkout", views.checkout, name="checkout"),
  path("logout", views.logout_request, name= "logout_request"),
  path("login", views.login_request, name= "logout_request"),
  path("register", views.register, name="register"),
  path("create-sub", views.create_sub, name="create sub"), #add
  path("complete", views.complete, name="complete"), #add



Next, create an abstract user and a view to handle creating a Stripe customer and subscription.  We create an abstract user to store customer and subscription information from Stripe in our local database.   We'll keep this data in sync by using foreign keys to dj-stripe model objects.  


from django.db import models
from django.contrib.auth.models import AbstractUser
from djstripe.models import Customer, Subscription

class User(AbstractUser):
  customer = models.ForeignKey(Customer, null=True, blank=True, on_delete=models.SET_NULL)
  subscription = models.ForeignKey(Subscription, null=True, blank=True,on_delete=models.SET_NULL)


 Add the following line to

AUTH_USER_MODEL = 'main.User'


import stripe
import json
from django.http import JsonResponse
from djstripe.models import Product
from django.contrib.auth.decorators import login_required
import djstripe
from django.http import HttpResponse


def create_sub(request):
  if request.method == 'POST':
      # Reads application/json and returns a response
      data = json.loads(request.body)
      payment_method = data['payment_method']
      stripe.api_key = djstripe.settings.STRIPE_SECRET_KEY

      payment_method_obj = stripe.PaymentMethod.retrieve(payment_method)

          # This creates a new Customer and attaches the PaymentMethod in one API call.
          customer = stripe.Customer.create(
                  'default_payment_method': payment_method

          djstripe_customer = djstripe.models.Customer.sync_from_stripe_data(customer)
          request.user.customer = djstripe_customer

          # At this point, associate the ID of the Customer object with your
          # own internal representation of a customer, if you have one.
          # print(customer)

          # Subscribe the user to the subscription created
          subscription = stripe.Subscription.create(
                      "price": data["price_id"],

          djstripe_subscription = djstripe.models.Subscription.sync_from_stripe_data(subscription)

          request.user.subscription = djstripe_subscription

          return JsonResponse(subscription)
      except Exception as e:
          return JsonResponse({'error': (e.args[0])}, status =403)
    return HTTPresponse('requet method not allowed')


Also, add a view function for the payment complete page:

def complete(request):
  return render(request, "complete.html")


Go through and test the integration.  As suggested by Stripe, use "4242 4242 4242 4242" as the test credit card and view the results upon submitting your payment.  Click on "Subscriptions" under "Customers" in your Stripe dashboard to view the newly created subscription and customer.  You should see something similar to below:

SASS credit card info

payment complete


stripe dashboard

Cancelling Monthly Subscriptions

Imagine a user desires to cancel their subscription. Let's add a cancel subscription button and then connect a url and view to handle cancelling user Stripe subscriptions.  To be clear, the process is straightforward.  All we need to do is get the subscription id that's connected to the abstract user model, and then send a cancel subscription request to Stripe.


{% extends "home.html" %}

{% block content %}

<div class="container text-center">
<h3>Your subscription is now active!</h3>
<a class="btn btn-primary text-light" href="/">Return Home</a>
<a class="btn btn-danger text-light" href="/cancel">Cancel Subscription</a>

{% endblock %}

stripe cancel button


from django.urls import path
from . import views

app_name = "main"   

urlpatterns = [
  path("", views.homepage, name="homepage"),
  path("checkout", views.checkout, name="checkout"),
  path("logout", views.logout_request, name= "logout_request"),
  path("login", views.login_request, name= "logout_request"),
  path("register", views.register, name="register"),
  path("create-sub", views.create_sub, name="create sub"),
  path("complete", views.complete, name="complete"),
  path("cancel", views.cancel, name="cancel"), #add this



def cancel(request):
  if request.user.is_authenticated:
    sub_id =

    stripe.api_key = djstripe.settings.STRIPE_SECRET_KEY

    except Exception as e:
      return JsonResponse({'error': (e.args[0])}, status =403)

  return redirect("main:homepage")

Go through and cancel a user's subscription.  Check the Stripe dashboard to see that the plan has indeed been cancelled.

stripe cancel subscription dashboard


Thanks for reading about using Django and Stripe to create monthly subscriptions.  Hopefully, this provides a foundation for creating your own SAAS project.  We plan on writing additional pieces on Django and Stripe to cover topics such as routing payments with Stripe Connect, the underlying payment technology behind Lyft, Postmates, Kickstarter, and more.  Also if you have any suggestions on other Django integrations that you'd like to read about, leave a comment below. 


Post a Comment
Join the community

Tetrahed432 Aug. 5, 2020, 9:48 a.m.

Can you expand this tutorial and add a section where the user is able to cancel a Stripe subscription, please?

James replying to Tetrahed432 Oct. 15, 2020, 1:49 p.m.

Thanks for the suggestion. The cancellation section has been added

Jmbouffa Aug. 23, 2020, 10:25 p.m.

What is request.user.subscription? You use a custom user model?

James replying to Jmbouffa Oct. 15, 2020, 1:50 p.m.

Yes the article now reflects the usage of an abstract user model

Cynthia Sept. 29, 2020, 8:45 a.m.

So where is the cancellation part of a saas application? Half backed much?

James replying to Cynthia Oct. 15, 2020, 1:50 p.m.

Don't worry. It's been added.

Arkusr Oct. 24, 2020, 4:58 p.m.

Can you add support for 3D secure cards?

Christian replying to Arkusr Oct. 26, 2020, 11:03 a.m.

Thumbs up. I agree: 3D security cards would be awesome.

Pycoder Oct. 28, 2020, 8:46 a.m.

Will Payments Bill Automatically each month

Bossing Nov. 20, 2020, 3:55 a.m.

Did you utilise and create a web hook within stripe as I don't have the webhook secret value? On top of that any tips for fixing this error : "DJSTRIPE_FOREIGN_KEY_TO_FIELD is not set."

Scott Jan. 8, 2021, 1:59 p.m.

After the checkout.html, where does the style and script go? same file? very vague.

James replying to Scott March 29, 2021, 3:57 p.m.

You can them inside checkout.html or load them from your static files

Pratik April 9, 2021, 11 p.m.

After Creating abstract user to store customer and subscription. I am facing circular dependency error

Barnabas April 16, 2021, 12:37 a.m.

Awesome tutorial, James! these two steps were missing to install dj-stripe: INSTALLED_APPS =( ... "djstripe", ... ) Add to path("stripe/", include("djstripe.urls", namespace="djstripe")),

Barnabas replying to Barnabas April 16, 2021, 12:45 a.m.

also, the new config is: DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id"