Updated: September, 2021
Software-as-a-service (SaaS) subscription businesses are among the fastest-growing companies in the world today. Every day, developers and aspiring entrepreneurs break code on a new subscription SaaS product. But what do these apps look like under the hood?
This guide will cover all the technical details of creating a subscription SaaS business using the Python-based Django web framework and Stripe payment processor.
Here's an interactive demo of what we'll be building.
Contents
- Contents
- Who should read this
- What you'll need
- An overview of subscriptions
- Subscription data modeling
- Setting up your Stripe billing models
- Syncing your Stripe billing data to your Django application
- Working with Products and Prices (Plans)
- Setting up your Subscription Data Models
- Creating your first Subscription
- (Upcoming) Keeping things in Sync with Webhooks
- (Upcoming) Working with Subscriptions in your Django application
By the time you've finished the article you should have solid foundational knowledge of everything needed to build a subscription SaaS application—and if you follow along—a fully-functional implementation of subscriptions in your own Django project.
Who should read this
This guide is written primarily for developers who want to add paid subscriptions to their application.
It is specifically focused on the Django web framework and Stripe subscriptions.
If you're a developer using a different technology stack you'll still benefit from the high-level modeling and architectural sections, but may struggle to follow along with some of the code examples, as we assume basic Python and Django knowledge. We also focus heavily on a Stripe integration—though much of the guide holds for Paypal, Paddle or other payment gateways.
What you'll need
In order to follow along you'll need:
- A basic Django project. The examples use Django 3.2 and Python 3.8.
- A free Stripe account. Test mode is fine.
An overview of subscriptions
Before getting into the details of the integration, we'll first take a moment to cover what subscriptions are and why you would want to use them.
If you're already familiar with subscriptions and convinced you want them, feel free to skim this section and/or skip ahead!
What are subscriptions?
Most businesses that sell software operate in one of two ways:
- Charge a single amount for a product or access to a service. This is also known as a one-time sale model. Historically, this was the most common way to sell software, and the model is still common in a lot of desktop software, mobile apps, and games.
- Charge a recurring amount on a recurring basis, typically each month or year. This is a subscription model—often also referred to as as a software as a service (SaaS) businesses. Spotify, Netflix, Salesforce, and Zoom are all subscription businesses.
There are other software business models—including advertising-based (e.g. Google, Facebook), or marketplace/transaction-fee based (e.g. AirBNB, Stripe)—but in this post we're focusing on the subscription model.
Why would you want to use subscriptions?
As loads of startup advice will tell you, subscription revenue is the holy grail of business models. Because, instead of collecting a one-time sale from your customers, or relying on consistent traffic for advertising, you collect predictable, recurring revenue from your customers.
Recurring revenue is the only way Jason Cohen, Founder, WP Engine, Designing the Ideal Bootstrapped Business
Having subscription revenue makes it easier to model and forecast your business, since you can quantitatively learn how many new customers you acquire in a month and how many will will cancel (a.k.a. churn). This allows you to very reliably understand how much money you can expect to earn next month, next quarter, and next year.
Subscriptions are also generally a way to increase a customer's life time value (LTV)—the amount of money the they pay you to use your product over time. By charging a smaller amount on a recurring basis you will typically, over the lifetime of a customer, be able to collect substantially more total revenue than from a single-time purchase.
Even software products that have historically had a very successful one-time-sale model like Adobe Photoshop have now switched to a subscription model.
How should you structure your subscriptions?
Ok, so you're convinced you want to offer subscriptions to your product. The next question you'll face, is how to set them up. There are a number of choices you'll have to make, including:
- How many different pricing tiers will the product have and what will the prices be?
- Will there be a free tier? What about a trial?
- What will be included in each tier? Common options include limiting access to certain features, as well as setting limits on usage—e.g. only allowing a certain number of users or events on a particular tier.
- What billing options will you offer? Most apps offer at least a monthly plan and a discounted annual plan.
- Will you offer a single pricing structure, or charge based on usage? For example, most email sending platforms charge based on the number of mails you send.
Unfortunately there is no one-size-fits-all answer to these questions. Most of the answers will be specific to the application that you are developing, and as the business owner you are hopefully more qualified than anyone else (certainly this article) to make those choices.
For the purposes of this post, we'll go with one of the most common options: a freemium application with multiple pricing tiers and monthly and annual billing options.
Subscription data modeling
Subscription billing systems are complicated.
They involve many moving parts, both on the setup side (what subscriptions you offer) and on the payment side (the details required to collect a payment from a customer and sign them up to a plan).
On the setup side you need to model what tiers exist, how they map to different features in your application, and how much they cost for various time intervals (e.g. monthly, annual, etc.).
And on the payment side you need to model the individual payment details as well as information about the customer and subscription, including the plan they are on, payment status, and renewal details.
It's a lot of stuff!
Thankfully Stripe has thought this problem through for us and created Stripe billing to model everything we'll need. Therefore, we'll largely be relying on Stripe's billing models and just annotating and referencing them a bit in our Django application. This drastically simplifies the amount of modeling we have to do on our own.
It also means that by and large Stripe will be the primary source of truth for most information, and our Django application will (mostly) just grab a read-only copy of whatever it needs from Stripe.
The Stripe Billing models we'll be using
Stripe's payment and billing system is large and complex, but for the most part we'll be able to focus on four key models—two on the setup side and two on the payment side.
In setup we'll primarily be using Products and Prices.
From Stripe's documentation:
"Subscriptions consist of two core models: Products and Prices. A Product defines the product or service you offer, while a Price represents how to charge for that Product. The product can be anything you charge for on a recurring basis, such as a digital SaaS product, a base fee for phone service, or a service that comes to your home and washes your car every week.
Products have no pricing information. Instead, the Product has one or more prices that define how much and how often to bill for the product. Creating more than one Price for a Product makes it possible to vary pricing by billing interval (e.g., monthly or quarterly billing) or currency."
In the SaaS world (and in the rest of this example) Products are the primary model that map to your pricing tiers / feature sets—e.g. "Pro" and "Enterprise", and then Prices are the options users have for signing up for those products, e.g. "Monthly", "Annual", or "Student Rate".
SaaS applications often refer to the things that Stripe calls "Products" as "Plans"—another Stripe model. To help mitigate this confusion, this guide uses the word tiers when referring to the Products/Plans that you offer your end-users.
For payment-processing we'll focus on Subscriptions and Customers.
The main model we'll be referencing is the Subscription—which will allow you to charge a Customer on a recurring basis. However, creating Subscriptions and collecting payments will require also working with Customers and so we'll cover those too.
Some other models will be needed to collect card and payment details, but they're not critical to the architecture of our system and we'll discuss them when we encounter them.
Modeling and syncing data between your application and Stripe
So, at a high level we're going to keep our Products, Prices, Subscriptions and Customers in Stripe. But we still need to be able to reference them in our own application so we can do things like:
- Show a pricing page with our different pricing tiers on it
- Determine whether a user has an active subscription so they can access a particular feature
- Send invoices or payment reminders to our customers
So how will we link them up?
As we mentioned above, we'll mostly thinking of Stripe as the "master" copy of our data and treat our application as a read-only replica.
What does that look like, practically?
There are two possible approaches.
Approach 1: Store the Stripe IDs of the various objects we'll be using.
In this approach, all we ever store is the Stripe ID of the object in question. Then, whenever we need more information about that object—say to find out the amount of a particular Price—we'd query the Stripe API with the appropriate ID and get back the information we need.
class MyStripeModel(models.Model):
name = models.CharField(max_length=100)
stripe_subscription_id = models.CharField(max_length=100)
This keeps things quite simple on our side—we don't need to maintain any local state or worry about keeping data in sync apart from the IDs. Any time we need data, we get it from Stripe and we are guaranteed that the information is up-to-date.
The main problem with this approach is performance. Remote requests are expensive—and so if you're trying to keep your page load times down, minimizing the number of external API calls your application makes can be important. Performance issues can be mitigated with caching, but this isn't always an easy option.
Approach 2: Keep a copy of the Stripe models in your local application database.
In this approach we use code that keeps our Stripe data in sync with our local application database. Then, rather than going back to Stripe every time we need information about a particular piece of data, we can just look it up in our application DB. This solves the performance issue above, and makes it much easier to work with Stripe data—we can just use the ORM we use for the rest of our data.
class StripeSubscription(models.Model):
start_date = models.DateTimeField(help_text="The start date of the subscription.")
status = models.CharField(max_length=20, help_text="The status of this subscription.")
# other data we need about the Subscription from Stripe goes here
class MyStripeModel(models.Model):
name = models.CharField(max_length=100)
stripe_subscription = models.ForeignKey(StripeSubscription, on_delete=models.SET_NULL)
The problem with this approach is that, data synchronization is hard.
If we're not careful, our local copy of the data can get out of sync with Stripe, and then bad things can happen to our users. Imagine if someone signed up for a $10/month plan and then got billed $20 for the first month because our data was out of sync! They'd probably be pretty unhappy.
So, which approach is better?
For simple setups it's probably better to go with Approach 1 and only store Stripe IDs. This can get you pretty far and you can always change plans if performance becomes a problem or you encounter workflows that require having more data in your application.
However, specifically for Django applications, we recommend Approach 2. This is primarily because of the great dj-stripe library that handles keeping our data in sync with Stripe with very little effort, allows us to reap the performance benefit of having the data locally, and lets us interface with our Stripe data through the ORM.
If dj-stripe
didn't exist, we'd recommend Approach 1 for getting off the ground,
but since it does, we'll go with Approach 2 and use it throughout the rest of this guide.
Setting up your Stripe billing models
Ok with our big-picture modeling out of the way we can finally start getting our hands dirty.
The first thing we're going to do is set up our Products and Prices in Stripe. You may want to follow Stripe's guide to setting up a Subscription as we get started. We'll reference this page heavily throughout this article.
This guide will use a relatively generic set of Subscription options: three Products named "Starter", "Standard", and "Premium", with two Prices each ("Monthly" and "Annual").
So, starting with Steps 1 and 2 in the guide, go ahead and create three Products with the names above (or use your own if you prefer). For each Product, add two Prices—one billed monthly and one billed annually. Set your prices up however you like. In our example, we've made the Annual plan cost 10x the monthly (so you get two months free by opting for annual billing—a common SaaS pricing model).
You can use the CLI or the Stripe dashboard to do this.
When you're done your Product list should look something like this:
And in each Product the list of Prices should look something like this:
Done? Great!
Let's get coding!
Syncing your Stripe billing data to your Django application
Now that your data is in Stripe it's time to sync it to your Django application.
Remember that library dj-stripe that we mentioned above? This is where it starts to come in handy.
Setting up and configuring dj-stripe
First we'll need to setup dj-stripe
.
Follow the instructions on their installation documentation,
by running pip install dj-stripe
(and/or adding it to your requirements.txt
file)
and adding the "djstripe"
app to your INSTALLED_APPS
in settings.py
like below.
INSTALLED_APPS =(
# other apps here
"djstripe",
)
You will also need to set the API keys in your settings.py
.
As already mentioned, we'll be using test mode, so make sure at least the variables below are set.
You can your keys from this page.
STRIPE_TEST_PUBLIC_KEY = os.environ.get("STRIPE_TEST_PUBLIC_KEY", "<your publishable key>")
STRIPE_TEST_SECRET_KEY = os.environ.get("STRIPE_TEST_SECRET_KEY", "<your secret key>")
STRIPE_LIVE_MODE = False
DJSTRIPE_WEBHOOK_SECRET = "whsec_xxx" # We don't use this, but it must be set
This example allows you to use os environment variables so you don't have to store your secrets in a .py file.
However, if you're not familiar with environment variables and are setting things up locally with your test account
it's fine to add the keys directly where it says "<your key>"
.
Once you've added your keys, you will need to create the dj-stripe
database tables:
./manage.py migrate
If this command fails it's likely that something isn't set up properly (the command should provide more details). If that happens, double check your setup and make sure it's working before continuing on.
Bootstrapping your initial Products and Prices in Django
With dj-stripe
set up, syncing our Products and Prices is now trivial.
Just run the following built-in command:
python manage.py djstripe_sync_plans_from_stripe
If everything is setup properly you should see output that looks like this:
Synchronized plan plan_GzAdqfExNKGmPz
Synchronized plan plan_GzAbnphUgi7vLI
Synchronized plan plan_GqvX7B8467f2Cj
Synchronized plan plan_GqvXkzAvxlF0wR
Synchronized plan plan_GqvV8KsEKyjzSN
Synchronized plan plan_GqvV4aKw0sh0Za
You should see one Plan ID per pricing plan you set up (6 total if you used the suggested setup above).
What just happened?
Behind the scenes dj-stripe
looked into your Stripe account, found all your Products and Prices and synced them to your local database.
If you go to your local Django Admin UI (by default at http://localhost:8000/admin/djstripe/product/)
you should now see the Stripe Products you set up earlier.
Why was this useful?
Well, now that we have the data in our database we can start using it in our Django application! Let's do that next.
Working with Products and Prices (Plans)
To get started we're going to run through a few examples using just Products and Prices (called Plans in dj-stripe
).
Once that's out of the way we'll move on to Subscriptions.
Creating a Pricing Page
The first thing we might want to do is create a pricing page. This is where our potential customers can see our different tiers, how much they cost, and what's in them. It's also the place where—eventually—they'll be able to subscribe to a plan.
Let's start by setting up the UI.
Since all our data is now synchronized from Stripe, we won't have to go back to Stripe to get the data but
can just inspect our local dj-stripe
models.
At a very basic level, that might look something like the below.
1. Set up a URL route
In urls.py
:
urlpatterns = [
path(pricing_page/', views.pricing_page, name='pricing_page'),
]
2. Create the View
In views.py
:
from django.shortcuts import render
from djstripe.models import Product
def pricing_page(request):
return render(request, 'pricing_page.html', {
'products': Product.objects.all()
})
Notice how we are just grabbing the Products
from our database using the ORM instead
of hitting the Stripe API.
We can do this because we have synced our Stripe data to our application DB.
3. Create the Template
In pricing_page.html
:
<section>
<p class="title">Pricing Plans</p>
<div class="columns">
{% for product in products %}
<div class="column">
<p class="subtitle">{{ product.name }}</p>
{% for plan in product.plan_set.all %}
<div>
<p class="heading">{{ plan.nickname }}</p>
<p>{{ plan.human_readable_price }}</p>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</section>
If you've set things up properly this should render a page that looks something like the below (you'll need to have Bulma CSS on the page for the styling to work).
Not bad for a few lines of code!
But—we probably want to display a bunch more stuff than just the names and prices.
A real SaaS pricing page might look more like this.
Here we've added three pieces of information to each tier:
- A description/tagline saying more about the tier and who it's for.
- A list of features that are available.
- Whether the tier is the default—which is highlighted in the UI.
To make this page we're going to need to create some additional metadata around the Products and Plans.
Adding metadata to your Stripe objects
Ok, so we want to attach the above information to our pricing tiers.
How should we do that?
Once again, we are faced with several options:
- We could store this information in Stripe. Stripe allows arbitrary "metadata" to be attached to objects, so we could store it there, sync it to our database, and then display it, similar to the other pieces of information.
- We could store the information in our application database. Since all of this data is application-specific there's not really a need for it to be in Stripe. So we could just create a local Database model to store it. Then we don't have to use Stripe's clunky metadata UI or worry about sync issues. This seems more appealing.
- We could store the information in code. If this data is coupled with our application anyways, we could bypass the database entirely and just keep it in our source code. This simplifies things even more—though comes with the downside of requiring code changes to make changes.
This guide recommends keeping additional metadata in your code.
Why? Well the main reason is that your Django application code is ultimately going to be coupled with this data in some way, so you might as well do it all that way.
Let's look at this by example. In the second pricing page above there's a feature called "Ludicrous Mode" that should only be available on a Premium subscription.
One thing we needed to do is show "Ludicrous Mode" on the pricing page. That could be done easily with all three options above.
But, we also want to have logic that only allows our users to enter Ludicrous Mode if they are subscribed to the right plan.
Unless the entire permission matrix of your application lives in a database (and good luck with that if it does)—you'll end up with code like the following:
if ludicrous_mode_enabled(user):
do_ludicrous_stuff()
So invariably your code will be coupled with your pricing tiers, anyway. Therefore, might as well commit to maintaining this logic in code and then at least there's fewer ways for your code and data to get out of sync.
Keeping this logic in your code also makes it easier to keep your different environments in sync, write automated tests for feature-gating logic, and roll-out and (and rollback) changes to production.
There cons of this setup—the largest being that it require developers and a deploy to production to make any changes—but by and large we've found it to be the simplest and easiest to maintain for small-to-medium sized applications and teams.
So we're going to add some code to augment our Stripe Product data. Here's what that might look like:
First we'll define our features as constants in a file called features.py
:
UNLIMITED_WIDGETS = 'Unlimited Widgets'
LUDICROUS_MODE = 'Ludicrous Mode'
PRIORITY_SUPPORT = 'Priority Support'
This step is optional—we could just use hard-coded strings—but having them as constants is a best-practice that allows us to easily reference them across our code without worrying about typos, etc. In this example the feature constants are just display names, but you could add arbitrary structure to them as well, similar to how we'll attach these to our Stripe products.
So how does that work? We can attach these in a file called metadata.py
:
@dataclass
class ProductMetadata(object):
"""
Metadata for a Stripe product.
"""
stripe_id: str
name: str
features: List[str]
description: str = ''
is_default: bool = False
PREMIUM = ProductMetadata(
stripe_id='prod_GqvWupK96UxUaG',
name='Premium',
description='For small businesses and teams',
is_default=False,
features=[
features.UNLIMITED_WIDGETS,
features.LUDICROUS_MODE,
features.PRIORITY_SUPPORT,
],
)
# other plans go here
In the above example, we've created a metadata class to associate with a Stripe product and manually linked it by stripe_id
.
We've added attributes for our description, list of features and (and any other information we want) entirely in code,
which allows us to more easily test, version control, and roll out changes that are specific to our application.
Now that we have this structure, we can easily write code like this to turn on/off particular features in our application.
Returning to our example above we can implement the ludicrous_mode_enabled
function:
def ludicrous_mode_enabled(user):
return features.LUDICROUS_MODE in user.product.metadata.features
This keeps the management of our subscriptions and features in one place.
We can also use this metadata structure to build out our new pricing page.
Here's a sketch of the unstyled HTML for that page, assuming that each of your stripe products has a .metadata
property
referencing the class above.
<div class="plan-interval-selector">
{% for plan in plans %}
<button class="button">{{ plan.name }}</button>
{% endfor %}
</div>
<div class="columns plan-selector">
{% for product in products %}
<div class="column">
<div {% if product.metadata.is_default %}class="is-selected"{% endif %}>
<span class="icon">
<i class="fa {% if product.metadata.is_default %}fa-check{% else %}fa-circle{% endif %}">
</i>
</span>
<p class="plan-name">{{ product.metadata.name }}</p>
<p class="plan-description">{{ product.metadata.description }}</p>
<div class="plan-price">
<span class="price">{{ product.metadata.monthly_price }}</span> / month
</div>
<ul class="features">
{% for feature in product.metadata.features %}
<li>
<span class="icon"><i class="fa fa-check"></i></span>
<span class="feature">{{ feature }}</span>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endfor %}
</div>
ProductMetadata
class attached to Stripe data.
This exercise of styling the HTML and making it interactive is left up to the reader.
Try a Demo Now
Setting up your Subscription Data Models
Phew! Ok, now we've got our Stripe Product and Plan data synchronized with our application and we are using the data—along with some additional metadata—to generate our grid of pricing tiers. It's a good start, but we still haven't done anything to allow our users to purchase and use our Products and Plans. So let's get into that.
The first thing we'll want to do is set up the data models.
And like the Products and Plans, we'll follow the same basic principle of making Stripe the source of truth,
and then mapping the Stripe data to our application models.
Once again we'll take advantage of dj-stripe
to handle a lot of the data synchronization.
The basic plan will be:
- A user goes through the subscription workflow on our site
- We create a subscription object in Stripe
- We sync that subscription to our application database
- Finally, we attach the subscription to our local data models (e.g. the logged-in acccount)
We'll cover steps 1-3 in depth when we go over creating your first subscription, but first we're going to discuss data modeling.
Choosing how to model Subscriptions in your Django application
As we mentioned above, we'll be focusing on the Subscription and Customer Stripe objects.
So let's assume we already have these objects synchronized to our database. How do these fit in to our application?
To decide this we'll have to answer two basic, but important questions:
- What is the unit of data in our application that should be associated with the Subscription object? The answer to this is typically, the primary unit that is associated with the tier itself.
- What is the unit of data in our application that should be associated with the Customer object? For this one, the right data model is typically associated with how the tier is managed.
The choice of how to manage these will often be application-specific, though there are a few common use-cases we can cover.
A user-based SaaS (typically B2C)
In a user-based SaaS each person has their own account and manages their own subscription. This is the most common model for business-to-consumer (B2C) apps like Spotify or Netflix (ignoring family plans).
For user-based SaaS applications the answer is likely that the Django User
model is the right place to associate
both your Subscription and Customer details.
Going back to the criteria above, the User
is associated with the subscription tier, and manages it too.
Assuming you have overridden the User
model (which is highly recommended), that would look something like this:
class CustomUser(AbstractUser):
customer = models.ForeignKey(
'djstripe.Customer', null=True, blank=True, on_delete=models.SET_NULL,
help_text="The user's Stripe Customer object, if it exists"
)
subscription = models.ForeignKey(
'djstripe.Subscription', null=True, blank=True, on_delete=models.SET_NULL,
help_text="The user's Stripe Subscription object, if it exists"
)
CustomUser
model, for a typical B2C SaaS application
A team-based SaaS (typically B2B)
Most SaaS applications are actually not consumer-facing, but instead target other businesses. For a business-to-business (B2B) SaaS it's more likely that you'll have the concept of "Teams" or "Organizations" that contain multiple users—typically mapping to a company or division of a company.
In this case you likely want to associate the Subscription with the Team model (or whatever you've named it), because that's the unit that the tier "belongs to".
That might look like this:
class Team(models.Model):
"""
A Team, with members.
"""
team_name = models.CharField(max_length=100)
members = models.ManyToManyField(
settings.AUTH_USER_MODEL, related_name='teams', through='Membership'
)
subscription = models.ForeignKey(
'djstripe.Subscription', null=True, blank=True, on_delete=models.SET_NULL,
help_text="The team's Stripe Subscription object, if it exists"
)
Team
model, for a typical B2B SaaS.
In this case all members of the team would have their subscription associated through the Team
.
Okay, that makes sense, but in this case where should the Customer association go?
Well, you probably don't want everyone in the Team to be able to modify/cancel the Subscription. That's likely something that only someone who's an administrator of some kind should be able to do.
Once again, there are a couple options.
The simplest one is to associate the Customer with the User
object again.
This often works, although can create problems in the rare case where someone is using the same User
account
and administering multiple Teams.
Often, a better option is to use the through model
to associate this information with the Team
membership.
In the Team
example above you can see this on the members
field.
That Membership
model then might look something like this:
class Membership(models.Model):
"""
A user's team membership
"""
team = models.ForeignKey(Team, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
role = models.CharField(max_length=100, choices=roles.ROLE_CHOICES)
customer = models.ForeignKey(
'djstripe.Customer', null=True, blank=True, on_delete=models.SET_NULL,
help_text="The member's Stripe Customer object for this team, if it exists"
)
Other models and key takeaways
Your application may require even more complicated set ups than this. For example, allowing multiple people to manage a Team's billing would probably have to be associated with a Role object of some kind. Or going back to Spotify—the Subscription might need to be optionally associated with a User or a "Team" (in the case of a family plan).
Ultimately, how you set this up is up to the details of your own application's data models, but the key takeaway is to associate the Subscription and Customer objects with the right application models according to this general principle:
If you follow that rule you should be good. Also, this guide recommends starting with the simplest model that works, and only expanding it when needed. You know, YAGNI and all.
For the rest of this example we'll use the B2C use case where the Subscription and the Customer
are both attached to our custom Django User
model.
Creating your first Subscription
Now that we've figured out how to model our data we can finally wire everything up. Time to finally get paid!
Thankfully Stripe already provides an incredible guide on setting up a Subscription that handles a lot of the details for us. This section will walk through that guide, providing details specific to our Django project, and focusing mostly on the sections tagged "server-side".
For Step 1, the Stripe Python package should have been installed already via the dj-stripe
dependency—though
if you use a requirements file it's good to explicitly add the stripe
package there since it is a true dependency.
We've already completed Step 2 by creating our Products and Plans above.
For now, we can skip Step 3: Create the Stripe Customer as we'll do this later.
So skip to Step 4: Collect payment information of the Stripe guide and follow the instructions there to collect and save card details on the front-end.
These steps do not have any backend-specific dependency and you can follow the instructions from Stripe
basically as-is.
However, we recommend (and assume) that the email you pass to Stripe is the same
as the logged-in User
's email, which you can grab in the Django template using {{ user.email }}
.
Save payment details
This corresponds to the client-side of Stripe's Step 5: Save payment details and create the subscription.
Assuming you've completed Steps 1-4, it's time to submit the newly created payment information to our server from the front end. We'll build off Stripe's example, but make a few changes.
Here's the basic JavaScript code—with the assumption that it has been written in the context of a Django template.
const createCustomerUrl = "{% url 'subscriptions:create_subscription' %}";
function stripePaymentMethodHandler(result, email) {
if (result.error) {
// Show error in payment form
} else {
const paymentParams = {
email: email,
plan_id: getSelectedPlanId(),
payment_method: result.paymentMethod.id,
};
fetch(createCustomerUrl, {
method: 'post',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'),
},
credentials: 'same-origin',
body: JSON.stringify(paymentParams),
}).then(function(response) {
return response.json();
}).then(function(result) {
// todo: check and process subscription status based on the response
}).catch(function (error) {
// more error handling
});
}
};
Let's walk through this in detail.
First we grab the URL of the customer/subscription creation URL that we're going to submit the data to
using the Django {% url %}
tag.
const createCustomerUrl = "{% url 'subscriptions:create_subscription' %}";
We'll define this URL in the next step, but for now just assume it exists.
Next we define the stripePaymentMethodHandler
function and error handling.
This bit is the same as the Stripe guide.
function stripePaymentMethodHandler(result, email) {
if (result.error) {
// Show error in payment form
} else {
// create customer and subscription
}
Then it's time to create the customer—via a POST
request to our backend.
Here we're going to make a few changes to the Stripe example—bringing in Django best-practices, and modifying the code to allow creating the Customer and Subscription object in a single request.
Here's the code.
// create customer and subscription
const paymentParams = {
email: email, // 1. assumed to be the logged-in user's email from Step 4
plan_id: getSelectedPlanId(), // 2. get the selected plan ID from your DOM / state
payment_method: result.paymentMethod.id,
};
fetch(createCustomerUrl, { // 3. use the url variable we defined earlier
method: 'post',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'), // 4. CSRF validation support
},
credentials: 'same-origin',
body: JSON.stringify(paymentParams),
}).then(function(response) {
// processing and error handling
});
A few notable changes that have been made from the Stripe version:
- We've continued the assumption that the Stripe subscription was set up with the logged-in User's email.
- Since we're also going to create the Subscription in this request, we need to pass a
plan_id
to our backend (to know what plan to subscribe the customer to). In this example we've made the assumption that theplan_id
is available from the DOM / state of your application and abstracted those details to thegetSelectedPlanId
helper function. This would be implemented by you according to how you've structured your pricing page. - Rather than hard-coding it, we've grabbed our
createCustomerUrl
from the variable set by the{% url %}
tag above. - We've added the
X-CSRFToken
header by pulling the cookie from Django's built-in Cross-Site Request Forgery (CSRF) protection. The Django docs on CSRF protection have more details on this approach, as well as the source of thegetCookie
function.
Ok, that wraps the front-end bits required to create the Customer and Subscription.
Now onto the backend!
Creating the Customer and Subscription Objects (server-side)
*This corresponds to Stripe's Step 3: Create the Stripe Customer and and the server-side part of Step 5 to create the subscription.
Here we'll define the backend views to create the Customer and Subscription objects, as well as associate them with the application models we chose above.
First we'll first create a URL for the endpoint.
In your app's urls.py
:
from django.urls import path
from . import views
app_name = 'subscriptions'
urlpatterns = [
# other URLs go here
path('create_subscription/', views.create_customer_and_subscription,
name='create_subscription'
),
]
The app_name
and url name
should match what we used in the {% url %}
tag
on the front-end.
Next we'll create the view in views.py
.
There's a lot going on here so we'll walk through it in detail but here's the complete view to start.
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.db import transaction
@login_required
@require_POST
@transaction.atomic
def create_customer_and_subscription(request):
"""
Create a Stripe Customer and Subscription object and map them onto the User object
Expects the inbound POST data to look something like this:
{
'email': '[email protected]',
'payment_method': 'pm_1GGgzaIXTEadrB0y0tthO3UH',
'plan_id': 'plan_GqvXkzAvxlF0wR',
}
"""
# parse request, extract details, and verify assumptions
request_body = json.loads(request.body.decode('utf-8'))
email = request_body['email']
assert request.user.email == email
payment_method = request_body['payment_method']
plan_id = request_body['plan_id']
stripe.api_key = djstripe_settings.STRIPE_SECRET_KEY
# first sync payment method to local DB to workaround
# https://github.com/dj-stripe/dj-stripe/issues/1125
payment_method_obj = stripe.PaymentMethod.retrieve(payment_method)
djstripe.models.PaymentMethod.sync_from_stripe_data(payment_method_obj)
# create customer objects
# This creates a new Customer in stripe and attaches the default PaymentMethod in one API call.
customer = stripe.Customer.create(
payment_method=payment_method,
email=email,
invoice_settings={
'default_payment_method': payment_method,
},
)
djstripe_customer = djstripe.models.Customer.sync_from_stripe_data(customer)
# create subscription
subscription = stripe.Subscription.create(
customer=customer.id,
items=[
{
'plan': plan_id,
},
],
expand=['latest_invoice.payment_intent'],
)
djstripe_subscription = djstripe.models.Subscription.sync_from_stripe_data(subscription)
# associate customer and subscription with the user
request.user.customer = djstripe_customer
request.user.subscription = djstripe_subscription
request.user.save()
# return information back to the front end
data = {
'customer': customer,
'subscription': subscription
}
return JsonResponse(
data=data,
)
Ok, let's walk through that in sections, starting with the declaration:
@login_required
@require_POST
@transaction.atomic
def create_customer_and_subscription(request):
# body
You can see we are requiring a login for this view.
That's because we are going to use the logged-in user to determine who associate the subscription with.
We're also requiring a POST
since that's what the front-end API should always do,
and we're wrapping everything in an @atomic
transaction, so we don't end up with our database in a partially-committed state.
Next we extract the data from the body of the POST and validate our assumptions:
request_body = json.loads(request.body.decode('utf-8'))
email = request_body['email']
assert request.user.email == email
payment_method = request_body['payment_method']
plan_id = request_body['plan_id']
In the last step we passed the POST
data to the backend as a JSON stringified set of data,
so we extract that and then pull out the individual parameters.
We also double-check our assumption that the email from the form match the logged-in User.
Next we initialize Stripe with the api_key
from our settings.py
.
stripe.api_key = djstripe_settings.STRIPE_SECRET_KEY
We then sync the PaymentMethod
object locally (note, this is just to workaround
this bug in dj-stripe).
payment_method_obj = stripe.PaymentMethod.retrieve(payment_method)
djstripe.models.PaymentMethod.sync_from_stripe_data(payment_method_obj)
Next we create the Customer
object in Stripe, using the Python API.
This part is similar to what's found in Step 6
of the Stripe guide.
# Create a new Customer in stripe and attach the default PaymentMethod in one API call.
customer = stripe.Customer.create(
payment_method=payment_method,
email=email,
invoice_settings={
'default_payment_method': payment_method,
},
)
Now the customer has now been created in Stripe, but we still need to sync it to our local
application DB which we can do with this line using the sync_from_stripe_data
helper function
available on every dj-stripe
model.
djstripe_customer = djstripe.models.Customer.sync_from_stripe_data(customer)
Now that we have a Customer we can create and sync the Subscription object in a similar way.
# create subscription
subscription = stripe.Subscription.create(
customer=customer.id,
items=[
{
'plan': plan_id,
},
],
expand=['latest_invoice.payment_intent'],
)
# and sync it to our application DB
djstripe_subscription = djstripe.models.Subscription.sync_from_stripe_data(subscription)
And then we can finally associate the new Customer and Subscription objects with our logged in User
.
request.user.customer = djstripe_customer
request.user.subscription = djstripe_subscription
request.user.save()
With that done, all that's left is to return some data to the front-end so that the next steps can be taken.
data = {
'customer': customer,
'subscription': subscription
}
return JsonResponse(
data=data,
)
If everything went well, our first Subscription should be created and associated with our logged in User!
Managing the Subscription Status (client-side)
This corresponds to Stripe's Step 8.
At this point we've created the Customer
and Subscription
objects and associated them with the appropriate
User
. So what's left?
Well, unfortunately, we aren't yet guaranteed that they've been created successfully. In many cases—the most common being a 3D-Secure workflow—there will be additional authentication steps required.
This part can follow the Stripe guide almost verbatim. Just insert the code from Stripe's guide into your front-end above where we wrote this.
// todo: check and process subscription status based on the response
The only thing you'll need to modify is extracting the subscription
variable details from the backend response.
That looks something like this.
const subscription = result.subscription;
const { latest_invoice } = subscription;
// continue with the rest of the Stripe example code
If you've made it this far you just created your very first Subscription. Congrats!
That's it for the current version of this guide. However, check back soon, as we plan to flesh out the below sections on using Webhooks to keep Subscription data in sync, and working with Subscription objects in your Django application.
If there's any other content you'd like to see added, please reach out to [email protected] and let me know!
Try a Demo Now
(Upcoming) Keeping things in Sync with Webhooks
This section is coming in the next revision.
(Upcoming) Working with Subscriptions in your Django application
This section is coming in the next revision.
Looking up the a user's Subscription status
This section is coming in the next revision.
Feature-gating (restricting access to content based on a user's Subscription)
This section is coming in the next revision.
Subscribe for Updates
Sign up to get notified when I publish new articles about building SaaS applications with Django.
I don't spam and you can unsubscribe anytime.