Skip to content

How To Set Up Your Django Models For Stripe Subscriptions

March 05, 2020Arohan Subramonia8 min read

Laptop with Code

Stripe is a third party API that specialises in providing a quick and easy to scale payments infrastructure for the internet. As of December 2020, Stripe lets you implement payment processing on your platform in the form of one time payments, subscription services, billing services and much more.

Understanding Subscriptions

Stripe Subscription objects flow Subscription objects flowchart as shown on the Stripe website

Stripe has a great overview explaining how their subscriptions API works. On the sell side, every stripe Product is the good or service you want to sell, and every linked Price is a price tier for that Product. On the buy side, every Customer is the user or buyer, with a linked PaymentMethod (usually a credit/debit card). Finally, a Subscription is what ties a particular Customer to a particular Price. We don't need to worry about Invoices or PaymentIntents for now.

As an example, let's take the use case of MasterClass Online Classes - an online platform where you pay for high quality courses from some of the world's finest artists, chefs and entertainers. Assume that you can subscribe to one 'Master', at two price tiers, Basic and Premium. Let's say Basic gives you access to all of their lower quality courses, and Premium gives you access to more advanced courses with better learning support, unseen material, and so on. As a user then, I can sign up to one Master at a time, select Basic or Premium, and have access to all of their Basic/Premium courses.

In this model, we'd have one Product per Master, with two Prices - Basic and Premium, at different monthly rates. A Customer could then sign up to e.g. Gordon Ramsey's Basic tier with a PaymentMethod, and a Subscription would be created linking that specific Customer (with a default PaymentMethod) to the Basic Price. This would then give the user (i.e. buyer) access to all Basic content from Gordon Ramsey, for that month. If the user then wanted to upgrade to Premium, the Subscription would be updated and linked to the Premium Price instead. This would be reflected in our database, giving the user access to Premium services on our platform too.

Syncing Django with Stripe

A decision for the project architect to make then, is what information (if any) to store in our Django backend - and how to do so. All of these Stripe objects, when generated, have multiple keys (including keys that interlink different object types). What keys to store in your database then, depends on what type of subscription flow you're implementing.

Stripe offers two main methods to implement subscriptions on your platform - the simple Checkout system or the slightly more complex and customisable Elements API. We'll be focusing on Elements in this article.

Basic Django Models

With Elements, we'll be doing the heavy lifting ourselves - you'll see in the 'Fixed-price subscriptions with Elements' tutorial that some of the code snippets require you to individually create and retrieve Customers, Subscriptions and other objects. Continuing on with our MasterClass example above then, let's have a look at what the base Django models might look like for this website.

For a 'student' (i.e. the buyer), a simple Django model might be:

from django.db import models
from django.contrib.auth.models import User

# username, email, first name, last name all stored in User model and OneToOne linked
class Student(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    image = models.ImageField(default=None, null=True, blank=True)
    bio = models.CharField(max_length=280, blank=True, null=True)
    display_name = models.CharField(max_length=50, blank=True, null=True)

Similarly, for a 'master' (i.e. the seller), a simple Django model might be:

from django.db import models
from django.contrib.auth.models import User

# username, email, first name, last name all stored in User model and OneToOne linked
class Master(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    image = models.ImageField(default=None, null=True, blank=True)
    bio = models.CharField(max_length=280, blank=True, null=True)
    rating = models.DecimalField(default=0.0, max_digits=3, decimal_places=1)

So we've got our base models. Now let's see how we can add stripe.

Adding Stripe to Django

Now the interesting part - how do we add to these models to sync Django with Stripe?

First, let's consider the Master. Every Master has an associated stripe Product. Therefore, we should save this product's id into our database, to let us retrieve this product's other details from stripe whenever we need to. Furthermore, every Master has two price tiers - Basic and Premium - linked to this product. We'll need to store the id of these prices also, as we'll need them when creating initialising a Subscription. Our Master model then, will now look like this:

from django.db import models
from django.contrib.auth.models import User

# username, email, first name, last name all stored in User model and OneToOne linked
class Master(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    image = models.ImageField(default=None, null=True, blank=True)
    bio = models.CharField(max_length=280, blank=True, null=True)
    rating = models.DecimalField(default=0.0, max_digits=3, decimal_places=1)
    stripe_basic_id = models.CharField(max_length=50, blank=True, null=True)
    stripe_premium_id = models.CharField(max_length=50, blank=True, null=True)
    stripe_product_id = models.CharField(max_length=50, blank=True, null=True)

Now, onto the Student. Before any Student can make a transaction, they have to be linked to a stripe Customer, so the id of the stripe Customer object should be saved. Plus, once the Subscription is created, we'll want to have access to that throughout our platform, if we want to provision access to certain pages based on subscription status. This then gives us the new Student model:

from django.db import models
from django.contrib.auth.models import User

# username, email, first name, last name all stored in User model and OneToOne linked
class Student(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    image = models.ImageField(default=None, null=True, blank=True)
    bio = models.CharField(max_length=280, blank=True, null=True)
    display_name = models.CharField(max_length=50, blank=True, null=True)
    stripe_customer_id = models.CharField(max_length=50, blank=True, null=True)
    stripe_subscription_id = models.CharField(max_length=50, blank=True, null=True)

And now you have it! This is the basic minimum information that you need to store in your database, when implementing Stripe's Fixed-price subscriptions with Elements. With these keys, you can then retrieve more information about them using

    stripe.[item].retrieve(stripe_item_key)

where item is Customer, Subscription, Product or Price, to then get the full object and more information about it. The object structure can be found by clicking on one of the links in the last sentence, or from the Stripe API Docs.

If you prefer, you can store more information quite easily when first creating these objects, reducing the number of API calls you have to make to stripe. A good next step would be saving the stripe_default_payment_method_id in the Student model, as well as default_payment_method_last4, the last four digits of a user's card. On the Master model, you could save the actual values of the subscription using the unit_amount key found in the Price object - an integer value in your default currency's lowest unit (i.e. pence, not pounds for GBP). These could be stored as stripe_basic_unit_amount and stripe_premium_unit_amount respectively.

The main decision after implementing the basic minimum is then whether the trade off of storing more information in your database (and paying for more storage space) is worth the offset of the fewer API calls you'll have to make to stripe - which comes down to project specifics and personal preference - something to consider when scaling. Nonetheless, you should now be able to implement a subscription model in Django quite easily whilst following Stripe's Fixed-price subscriptions with Elements tutorial.

Good luck!

An Example with Code

The following code is a potential implementation of the above models. Given a Master and Student object, the function either tries to retrieve the correct stripe objects, and if not, creates them and stores them in our django database. Feel free to adapt this code to your own project!

For the Student object, creating a Customer and saving the id into our database would look like:

from .models import Master, Student
import stripe
import constants

stripe.api_key = constants.STRIPE_API_KEY

def get_or_create_customer_details(req_student):
    # Check if the student already has a customer id. If so, retrieve the object
    if req_student.stripe_customer_id and len(req_student.stripe_customer_id):
        req_customer = stripe.Customer.retrieve(req_student.stripe_customer_id)

    # If no customer id, create a customer and save the id in our database
    else:
        customer_name = req_student.user.first_name + " " + req_student.user.last_name
        req_customer = stripe.Customer.create(email=req_student.user.email, name=customer_name)
        req_student.stripe_customer_id = req_customer.id
        req_student.save()
    return req_customer

For the Master object, creating a Product and Prices and saving the ids into our database would look like:

def get_or_create_product_and_price_details(req_master):
    # Check if the master already has a product id. If so, retrieve the object
    if req_master.stripe_product_id and len(req_master.stripe_product_id):
        req_product = stripe.Product.retrieve(req_master.stripe_product_id)
    # If there's no product id, create a product and save the id to our database
    else:
        product_name = req_master.user.first_name + " " + req_master.user.last_name
        req_product = stripe.Product.create(name=name)
        req_master.stripe_product_id = req_product.id
        req_master.save()

    # Repeat the process for basic and premium prices
    # Make sure to link the created/retrieved product if creating the price

    if req_master.stripe_basic_id and len(req_master.stripe_basic_id):
        req_basic_price = stripe.Price.retrieve(req_master.stripe_basic_id)
    else:
        # default values for basic - can be changed on stripe dashboard,
        # but only before the Price is involved in any transactions
        req_basic_price = stripe.Price.create(
                unit_amount=500,
                currency="gbp",
                recurring={"interval": "month"},
                product=req_product.id,
            )
        req_master.stripe_basic_id = req_basic_price.id
        req_master.save()

    if req_master.stripe_premium_id and len(req_master.stripe_premium_id):
        req_premium_price = stripe.Price.retrieve(req_master.stripe_premium_id)
    else:
        # default values for premium - can be changed on stripe dashboard,
        # but only before the Price is involved in any transactions
        req_premium_price = stripe.Price.create(
                unit_amount=1000,
                currency="gbp",
                recurring={"interval": "month"},
                product=req_product.id,
            )
        req_master.stripe_premium_id = req_premium_price.id
        req_master.save()

    return req_product, req_basic_price, req_premium_price

(Cover photo by Sai Kiran Anagani on Unsplash)

Arohan Subramonia

Arohan Subramonia

Web Developer at Theodo