Django Stripe Tutorial
Updated
Table of Contents
In this tutorial, we will learn how to handle single payments with Stripe. As the first project in the course, it is intentionally basic in implementation, but don't worry: we will ramp up the features and complexity soon enough in later chapters.
One of the challenges of building a Django e-commerce website is that there is a lot more to it than simply accepting payments. In fact, that might be the easiest piece of the puzzle, which is why we start with it here.
You'll see that implementing a Payment Gateway like Stripe that accepts debit or credit card payments is relatively straightforward. However, using it correctly and securely on a website that is also juggling a host of other features such as user accounts, user permissions, a database full of products, shopping carts, anonymous users, and so on is quite complex and best approached incrementally.
That is why we start the course by focusing on a single payment, which requires just three pages: a payment page, a success page for when an order goes through, and a cancel page if the order fails. Let's begin!
Table of Contents
In this tutorial, we will build a basic e-commerce website in Django that uses Stripe to process single payments. The website will have a homepage for orders, a success page when an order goes through, and a cancel page if the order fails. This foundation can later be extended into full-featured e-commerce applications that store products and sales in a database and utilize webhooks to confirm payments, even subscriptions.
Initial Set Up
To start, open up your terminal and create a new Python virtual environment.
# Windows
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $
# macOS
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $
Then install Django
, create a new project called django_project
, and run migrate
to initialize the new SQLite local database. Execute the runserver
command to start the local web server and confirm everything works properly.
(.venv) $ python -m pip install django~=5.1.0
(.venv) $ django-admin startproject django_project .
(.venv) $ python manage.py migrate
(.venv) $ python manage.py runserver
In your web browser, navigate to 127.0.0.1:8000
to see the familiar Django welcome page.
Django Configuration
We will create a dedicated products
app for our logic now using the startapp
command. Before running this command, type Ctrl-C
to stop the local server in your terminal.
(.venv) $ python manage.py startapp products
Don't forget to register the app immediately in our settings.py
file so that Django knows to look for it.
# django_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"products", # new
]
Then, we need to create views, templates, and URLs for the three pages. We could do this in any order since all three are required for a page to render correctly. Let's start with the views. Update the products/views.py
file so each view returns a related template.
# products/views.py
from django.shortcuts import render
def home(request):
return render(request, "products/home.html")
def success(request):
return render(request, "products/success.html")
def cancel(request):
return render(request, "products/cancel.html")
Next, we can configure our URLs. Update the project-level django_project/urls.py
file so that it will look for URLs within the products
app. That means importing include
at the top and adding a dedicated URL path for the app at the empty string, ""
.
# django_project/urls.py
from django.contrib import admin
from django.urls import path, include # new
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("products.urls")), # new
]
Now create a new file, products/urls.py
, to import the three views and connect them to corresponding URL paths. We added a URL name for each as we will need it alongside reverse later. More on that soon.
# products/urls.py
from django.urls import path
from .views import home, success, cancel
urlpatterns = [
path("", home, name="home"),
path("success/", success, name="success"),
path("cancel/", cancel, name="cancel"),
]
Lastly, the template files. Create a templates
folder within the products
app, a folder called products
, and finally, add our template files.
(.venv) $ mkdir products/templates
(.venv) $ mkdir products/templates/products
The template structure should look like this now:
├── django_project
├── products
├── templates
├── products
In your text editor, create three new template files: home.html
, success.html
, and cancel.html
.
<!-- products/templates/products/home.html -->
<h1>Homepage</h1>
<!-- products/templates/products/success.html -->
<h1>Success page</h1>
<!-- products/templates/products/cancel.html -->
<h1>Cancel page</h1>
That's it! Make sure runserver
is running, and then confirm that all three webpages exist.
Configure Stripe
Log in to your existing Stripe account or register for a free one. If you only want to use test mode, where we can mock transactions, you don't need to complete the application with bank details. Once registered, head over to the dashboard. Click the toggle in the upper right-hand corner to activate "Test mode." This allows us to simulate transactions; no actual transactions or payments are made.
In test mode, a bright orange banner will appear at the top of the screen, indicating that we are now in test mode. This is very important, as you decidedly do not want to confuse real and simulated transactions! With test mode activated, click on the link for "Developers" in the upper right-hand corner, right next to the "Test mode" toggle.
Click on the "Developers" nav link in the upper right-hand corner.
This is the main Developer section of the dashboard, where we can see an overview of every API request, API keys, webhooks, events, and so on. We want to focus on API keys so click that link near the top.
Stripe provides a "Publishable key" and a "Secret key" that work together to process payment transactions. The publishable (or public) key is used to initiate a transaction and is visible to the client; the secret key is used on our server and should be kept confidential.
For this chapter, we will hardcode both keys into the bottom of our django_project/settings.py
file. Every set of keys is different, but if you were using my dashboard in the image where the "Publishable key" is "pk_test_hZFYGaBpYdraSagjw5X2RpQC", you'd enter that value within quotes, ""
, as the STRIPE_PUBLIC_KEY
. The "Secret key" is in the format "sk_test_xxxxx".
# django_project/settings.py
STRIPE_PUBLIC_KEY = "<your_test_public_api_key>" # pk_test_hZFYGaBpYdraSagjw5X2RpQC in this example
STRIPE_SECRET_KEY = "<your_test_secret_api_key>"
Now, we need to sell a product. This can be initially configured within Stripe. On the left sidebar, click on the link for "Product catalog."
This takes us to the Product catalog page.
It is currently empty since we do not have any products to sell. To create a product to sell, click on either the "+ Create product" button in the upper right-hand corner or the "+ Add a product" button in the bottom center. Both buttons open the same "Add a product" preview.
Several options can be set here. The required fields are name, one-off or recurring, and the amount. We are using the title "Sample Product" and the amount of "25.00". In a real-world project, adding a description and an image are very good ideas, but we will not do so here in the interest of simplicity. Notice that Stripe defaults to "Recurring" payments, but in this example, switch the toggle to "One-off" instead for a single payment. With the fields filled out, click on the "Add product" button at the bottom.
We are now redirected back to the main Product Catalog page where our new "Sample Product" is visible.
Click on the "Sample Product" to open up its dedicated page. At the moment we only have one price point, $25.00, for our product. But it's possible to have different prices. Click on the price we have ($25.00 USD) to open up its individual pricing page.
On the "Price for Sample Product" page we want to access the Price ID. It is located in the upper right-hand corner and in grey type. It is somewhat difficult to read!
In this case of my sample product, the price id is price_1QDnjBEeTBCIZYBI0zR1QoxS
. Yours will be different! Keep note of this value for later.
Now that we have a product to sell and its price ID, we can turn our focus back to the Django app. The general pattern for all items we sell is that we can configure them completely within Stripe, obtain a related id, and then reference that in our application.
Stripe Hosted Page
If you look at the Stripe docs, the Python example provided is for a Flask Stripe-hosted page. However, we can adapt this for our Django app. At a high level, we need to add an HTML form to the home.html
template and then update our views.py
file so that when a POST request is sent to Stripe, our server-side Stripe SDK and Stripe Private key are used to generate a new Checkout session. The user is redirected to this unique session and then, if payment is successful, redirected to our success.html
page. Otherwise, they will be sent to the cancel.html
page.
Let's start by installing the Python library for Stripe, which is available on Github.
(.venv) $ python -m pip install stripe==11.1.1
In our home.html
file, add a basic form using the POST method and Django's CSRF protection.
<!-- templates/home.html -->
<h1>Homepage</h1>
<form method="post">
{% csrf_token %}
<button type="submit">Purchase!</button>
</form>
Most of the magic happens in our views.py
file. The updated code is below, and we'll work through the changes line-by-line. But before getting into specifics, it's essential to understand that at a high level, we are loading in the Stripe Private Key from our settings.py
file and using it to create a new Checkout session. The user will be redirected to a custom URL for the Stripe-hosted checkout page and then, after submitting payment, either sent to a success or cancel page.
# products/views.py
from django.shortcuts import render, redirect # new
from django.conf import settings # new
from django.urls import reverse # new
import stripe # new
def home(request): # new
stripe.api_key = settings.STRIPE_SECRET_KEY
if request.method == "POST":
checkout_session = stripe.checkout.Session.create(
line_items=[
{
"price": "<price_API_ID_HERE>", # enter yours here!!!
"quantity": 1,
}
],
mode="payment",
success_url=request.build_absolute_uri(reverse("success")),
cancel_url=request.build_absolute_uri(reverse("cancel")),
)
return redirect(checkout_session.url, code=303)
return render(request, "products/home.html")
def success(request):
return render(request, "products/success.html")
def cancel(request):
return render(request, "products/cancel.html")
At the top, we've imported redirect
, settings
, reverse
, and stripe
. The first step in our home
function-based view is to set the Stripe api_key
to the Stripe Private Key for our account; it is stored in our settings.py
file for security and not visible to the user client-side. Next, if a POST
request is made a new checkout_session
is created and requires, at a minimum, the following attributes: line_items
, mode
, success_url
, and cancel_url
. Other attributes are available but are not necessary at this stage.
We define a product to sell with the line_items
attribute, using the exact Price ID for our product set on the Stripe website. Later on we might want to have the option of storing multiple IDs in our database to load in, but hardcoding it is fine for example purposes. We also specify the quantity
of 1
.
Next, we set a mode to "payment" for one-time purchases, but we could also do "subscription" for subscriptions or "setup" to collect customer payment details to reuse later.
After the user places an order, they are sent to either a success_url
or a cancel_url
. Django's reverse() function is used alongside the URL name of each; this is the name=""
bit added to each of our URL paths in products/urls.py
. We also take advantage of Django's [build_absolute_uri] method to send an absolute URL to Stripe.
The final bit is redirecting to Checkout. After our POST request is sent to Stripe, authenticated, and a new Checkout session is created, Stripe's API will send us a unique checkout_session.url
that the user is redirected to.
The full Stripe Checkout flow can take a while to sink in, but that's all the code we need!
Ensure the server is running by typing python manage.py runserver
in the terminal and go to the homepage at 127.0.0.1:8000
. Click on the "Purchase" button.
If you see an error it's likely because you didn't enter in your own Price ID in the products/views.py
file! But assuming it all works correctly, you'll be redirected to a unique Stripe Checkout session for the product.
Fill in the form. For the credit card information, use the number 4242 4242 4242 4242
, any date in the future for expiration, and any three digits for the CVC. Then click the "Pay" button at the bottom.
After successful payment, you will be redirected to the success page at http://127.0.0.1:8000/success/
.
You can view the confirmation of the payment on your Stripe dashboard. Click the "Home" link on the left sidebar to get there.
If you click on "Transaction" on the left sidebar you will see a list of all payments. Click on the individual payment to see full information.
Conclusion
Congratulations on processing your first payment with Django and Stripe! There is a lot of new material to internalize, but the general format is the same for all requests.
What are the next steps involved with building out a more robust e-commerce application?
- Switch from a hardcoded view and Stripe Price ID to using variables in our templates and retrieving items from the database
- Add an action to our view so a user receives access or another event is triggered after a payment
- Switch to using webhooks to ensure that transactions went through
- Add a
User
model and update permissions based on a payment - If you need shopping cart functionality, that involves working with sessions, handling anonymous users, and more.
- Subscriptions? This is popular for SaaS products and while there are some similarities versus one-time payments, there's a lot of nuance involved in getting it right!
It can take a while to wrap your head around e-commerce patterns in Django, but once you do you'll find they are remarkably similar. I am working on a future e-commerce course that will demonstrate all of these concepts progressively through multiple projects. If you are interested, make sure to sign up for my newsletter to be notified when it is available!