Using Django and Stripe for Monthly Subscriptions

ยท12 min read

Using Django and Stripe for Monthly Subscriptions

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

C:\Users\Owner\Desktop\code\saas>Scripts\activate

(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 manage.py startapp main

 

Add the main app to settings.py in mysite.

settings.py

INSTALLED_APPS = [
    'main.apps.MainConfig', #add this
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Create urls.py in the main folder and include in mysite > urls.py.

 

mysite > urls.py

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

urlpatterns = [
  path('', include('main.urls')),  #add path
  path('admin/', admin.site.urls),
]

 

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.  To do so, we need to add the package, our Stripe API keys, and set the dj-stripe foreign key to "id" in settings.py.  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

mysite/settings.py

INSTALLED_APPS = [
    'main.apps.MainConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'djstripe', #add this
]
...

STRIPE_TEST_PUBLIC_KEY ='your_pk_test'
STRIPE_TEST_SECRET_KEY = 'your_secret_key'
STRIPE_LIVE_MODE = False  # Change to True in production
DJSTRIPE_WEBHOOK_SECRET = "whsec_xxx"
DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id"

 

Also, let's include the dj-stripe URL paths in order to sync with Stripe.

mysite/urls.py

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

urlpatterns = [
  path('', include('main.urls')),  
  path('admin/', admin.site.urls),
  path("stripe/", include("djstripe.urls", namespace="djstripe")), #add this
]

 

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

py manage.py migrate

 

Add products to the database automatically with the following command:

py manage.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 test@example.com since we'll use this as the Stripe customer name then visit http://127.0.0.1:8000/admin/  You should see a variety of new models including our newly created products as well as plans for the products.  

py manage.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:

views.py

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")

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

checkout.html

{% extends "home.html" %}

{% block content %}
<script src="https://js.stripe.com/v3/"></script>

<br><br>

  
<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">{{p.name}}</h5>
          <p class="card-text text-muted"><svg class="bi bi-check" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
            <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"/>
          </svg>{{p.description}}</p>

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

    </div>

    {% endfor %}
  </div>
  <br><br><hr><br><br>
  <div>
    <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">
                <p>Plan:</p>
                <p>Total:</p>
              </div>
              <div class="col-6 text-right">
                <p id="plan"></p>
                <p id="price"></p>
                <p hidden id="priceId"></p>
              </div>

            </div>
            <br>
            <form id="subscription-form" >
              <div id="card-element" class="MyCardElement">
                <!-- Elements will create input elements here -->
              </div>
              
              <!-- 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>
                </div>
                <span id="button-text">Subscribe</span>
              </button>
            </form>
          </div>
        </div>

      </div>
    </div>
  </div>

</div>

{% 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.

<style>
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);
    }

</style>
document.getElementById("submit").disabled = true;

stripeElements();

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.mount('#card-element');

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

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

    card.on('change', function (event) {
      displayError(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) {
      evt.preventDefault();
      changeLoadingState(true);


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

}



function createPaymentMethod({ card }) {

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

  stripe
    .createPaymentMethod({
      type: 'card',
      card: card,
      billing_details: {
        name: billingName,
      },
    })
    .then((result) => {
      if (result.error) {
        displayError(result);
      } else {
       const paymentParams = {
          price_id: document.getElementById("priceId").innerHTML,
          payment_method: result.paymentMethod.id,
      };
      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) {
          displayError(result.error.message);

      });
      }
    });
}


var changeLoadingState = function(isLoading) {
  if (isLoading) {
    document.getElementById("submit").disabled = true;
    document.querySelector("#spinner").classList.remove("hidden");
    document.querySelector("#button-text").classList.add("hidden");
  } else {
    document.getElementById("submit").disabled = false;
    document.querySelector("#spinner").classList.add("hidden");
    document.querySelector("#button-text").classList.remove("hidden");
  }
};

 

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

main/urls.py

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.  

main/models.py

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)

mysite/settings.py

 Add the following line to settings.py.

AUTH_USER_MODEL = 'main.User'

main/views.py

...
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

...

@login_required
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)
      djstripe.models.PaymentMethod.sync_from_stripe_data(payment_method_obj)


      try:
          # This creates a new Customer and attaches the PaymentMethod in one API call.
          customer = stripe.Customer.create(
              payment_method=payment_method,
              email=request.user.email,
              invoice_settings={
                  '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(
              customer=customer.id,
              items=[
                  {
                      "price": data["price_id"],
                  },
              ],
              expand=["latest_invoice.payment_intent"]
          )

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

          request.user.subscription = djstripe_subscription
          request.user.save()

          return JsonResponse(subscription)
      except Exception as e:
          return JsonResponse({'error': (e.args[0])}, status =403)
  else:
    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.

complete.html

{% extends "home.html" %}

{% block content %}


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

{% endblock %}

stripe cancel button

main/urls.py

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

]

main/views.py

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

    stripe.api_key = djstripe.settings.STRIPE_SECRET_KEY

    try:
      stripe.Subscription.delete(sub_id)
    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

Conclusion

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.