Django Stripe Tutorial

Last updated July 30th, 2023

In this tutorial, I'll demonstrate how to configure a new Django website from scratch to accept one-time payments with Stripe.

Need to handle subscription payments? Check out Django Stripe Subscriptions.

Contents

Stripe Payment Options

There are currently three ways to accept one-time payments with Stripe:

  1. Charges API (legacy)
  2. Stripe Checkout (the focus of this tutorial)
  3. Payment Intents API (often coupled with Stripe Elements)

Which one should you use?

  1. Use Checkout if you want to get up and running fast. If you're familiar with the old modal version of Checkout, this is the way to go. It provides a ton of features out-of-the-box, supports multiple languages, and includes an easy path to implementing recurring payments. Most importantly, Checkout manages the entire payment process for you, so you can begin accepting payments without even having to add a single form!
  2. Use the Payment Intents API If you want a more custom experience for your end users.

Although you can still use the Charges API, if you're new to Stripe do not use it since it does not support the latest banking regulations (like SCA). You will see a high rate of declines. For more, review the Charges vs. Payment Intents APIs page from the official Stripe docs.

Still using the Charges API? If most of your customers are based in the US or Canada you don't need to migrate just yet. Review the Checkout migration guide guide for more info.

Project Setup

Create a new project directory along with a new Django project called djangostripe:

$ mkdir django-stripe-checkout && cd django-stripe-checkout
$ python3.11 -m venv env
$ source env/bin/activate

(env)$ pip install django==4.2.3
(env)$ django-admin startproject djangostripe .

Feel free to swap out virtualenv and Pip for Poetry or Pipenv. For more, review Modern Python Environments.

Next, create a new app called payments:

(env)$ python manage.py startapp payments

Now add the new app to the INSTALLED_APPS configuration in settings.py:

# djangostripe/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # Local
    'payments.apps.PaymentsConfig', # new
]

Update the project-level urls.py file with the payments app:

# djangostripe/urls.py

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

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

Create a urls.py file within the new app, too:

(env)$ touch payments/urls.py

Then populate it as follows:

# payments/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),
]

Now add a views.py file:

# payments/views.py

from django.views.generic.base import TemplateView

class HomePageView(TemplateView):
    template_name = 'home.html'

And create a dedicated "templates" folder and file for our homepage.

(env)$ mkdir templates
(env)$ touch templates/home.html

Then, add the following HTML:

<!-- templates/home.html -->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Checkout</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
    <script defer src="https://use.fontawesome.com/releases/v6.4.0/js/all.js"></script>
  </head>
  <body>
  <section class="section">
    <div class="container">
      <button class="button is-primary" id="submitBtn">Purchase!</button>
    </div>
  </section>
  </body>
</html>

Make sure to update the settings.py file so Django knows to look for a "templates" folder:

# djangostripe/settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'], # new
        ...

Finally run migrate to sync the database and runserver to start Django's local web server.

(env)$ python manage.py migrate
(env)$ python manage.py runserver

That's it! Check out http://localhost:8000/ and you'll see the homepage:

Django

Add Stripe

Time for Stripe. Start by installing it:

(env)$ pip install stripe==5.5.0

Next, register for a Stripe account (if you haven't already done so) and navigate to the dashboard. Click on "Developers":

Stripe Developers

Then click on "API keys":

Stripe Developers Key

Each Stripe account has four API keys: two for testing and two for production. Each pair has a "secret key" and a "publishable key". Do not reveal the secret key to anyone; the publishable key will be embedded in the JavaScript on the page that anyone can see.

Currently the toggle for "Viewing test data" in the upper right indicates that we're using the test keys now. That's what we want.

At the bottom of your settings.py file, add the following two lines including your own test secret and test publishable keys. Make sure to include the '' characters around the actual keys.

# djangostripe/settings.py

STRIPE_PUBLISHABLE_KEY = '<your test publishable key here>'
STRIPE_SECRET_KEY = '<your test secret key here>'

Finally, you'll need to specify an "Account name" within your "Account settings" at https://dashboard.stripe.com/settings/account:

Stripe Account Name

Create a Product

Next, we need to create a product to sell.

Click "Products" and then "Add product":

Stripe Add Product

Add a product name, enter a price, and select "One time":

Stripe Add Product

Click "Save product".

User Flow

After the user clicks the purchase button we need to do the following:

  1. Get Publishable Key

    • Send an XHR request from the client to the server requesting the publishable key
    • Respond with the key
    • Use the key to create a new instance of Stripe.js
  2. Create Checkout Session

    • Send another XHR request to the server requesting a new Checkout Session ID
    • Generate a new Checkout Session and send back the ID
    • Redirect to the checkout page for the user to finish their purchase
  3. Redirect the User Appropriately

    • Redirect to a success page after a successful payment
    • Redirect to a cancellation page after a cancelled payment
  4. Confirm Payment with Stripe Webhooks

    • Set up the webhook endpoint
    • Test the endpoint using the Stripe CLI
    • Register the endpoint with Stripe

Get Publishable Key

JavaScript Static File

Let's start by creating a new static file to hold all of our JavaScript:

(env)$ mkdir static
(env)$ touch static/main.js

Add a quick sanity check to the new main.js file:

// static/main.js

console.log("Sanity check!");

Then update the settings.py file so Django knows where to find static files:

# djangostripe/settings.py

STATIC_URL = '/static/'

# for django >= 3.1
STATICFILES_DIRS = [BASE_DIR / 'static']  # new

# for django < 3.1
# STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]  # new

Add the static template tag along with the new script tag inside the HTML template:

<!-- templates/home.html -->

{% load static %} <!-- new -->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Checkout</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
    <script src="{% static 'main.js' %}"></script>   <!-- new -->
    <script defer src="https://use.fontawesome.com/releases/v6.4.0/js/all.js"></script>
  </head>
  <body>
  <section class="section">
    <div class="container">
      <button class="button is-primary" id="submitBtn">Purchase!</button>
    </div>
  </section>
  </body>
</html>

Run the development server again. Navigate to http://localhost:8000/, and open up the JavaScript console. You should see the sanity check:

JavaScript Sanity Check

View

Next, add a new view to payments/views.py to handle the XHR request:

# payments/views.py

from django.conf import settings # new
from django.http.response import JsonResponse # new
from django.views.decorators.csrf import csrf_exempt # new
from django.views.generic.base import TemplateView


class HomePageView(TemplateView):
    template_name = 'home.html'


# new
@csrf_exempt
def stripe_config(request):
    if request.method == 'GET':
        stripe_config = {'publicKey': settings.STRIPE_PUBLISHABLE_KEY}
        return JsonResponse(stripe_config, safe=False)

Add a URL as well:

# payments/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),
    path('config/', views.stripe_config),  # new
]

XHR Request

Next, use the Fetch API to make an XHR (XMLHttpRequest) request to the new /config/ endpoint in static/main.js:

// static/main.js

console.log("Sanity check!");

// new
// Get Stripe publishable key
fetch("/config/")
.then((result) => { return result.json(); })
.then((data) => {
  // Initialize Stripe.js
  const stripe = Stripe(data.publicKey);
});

The response from a fetch request is a ReadableStream. result.json() returns a promise, which we resolved to a JavaScript object -- i.e., data. We then used dot-notation to access the publicKey in order to obtain the publishable key.

Include Stripe.js in templates/home.html like so:

<!-- templates/home.html -->

{% load static %}

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Checkout</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
    <script src="https://js.stripe.com/v3/"></script>  <!-- new -->
    <script src="{% static 'main.js' %}"></script>
    <script defer src="https://use.fontawesome.com/releases/v6.4.0/js/all.js"></script>
  </head>
  <body>
  <section class="section">
    <div class="container">
      <button class="button is-primary" id="submitBtn">Purchase!</button>
    </div>
  </section>
  </body>
</html>

Now, after the page load, a call will be made to /config/, which will respond with the Stripe publishable key. We'll then use this key to create a new instance of Stripe.js.

Flow:

  1. Get Publishable Key

    • Send an XHR request from the client to the server requesting the publishable key
    • Respond with the key
    • Use the key to create a new instance of Stripe.js
  2. Create Checkout Session

    • Send another XHR request to the server requesting a new Checkout Session ID
    • Generate a new Checkout Session and send back the ID
    • Redirect to the checkout page for the user to finish their purchase
  3. Redirect the User Appropriately

    • Redirect to a success page after a successful payment
    • Redirect to a cancellation page after a cancelled payment
  4. Confirm Payment with Stripe Webhooks

    • Set up the webhook endpoint
    • Test the endpoint using the Stripe CLI
    • Register the endpoint with Stripe

Create Checkout Session

Moving on, we need to attach an event handler to the button's click event which will send another XHR request to the server to generate a new Checkout Session ID.

View

First, add the new view:

# payments/views.py

@csrf_exempt
def create_checkout_session(request):
    if request.method == 'GET':
        domain_url = 'http://localhost:8000/'
        stripe.api_key = settings.STRIPE_SECRET_KEY
        try:
            # Create new Checkout Session for the order
            # Other optional params include:
            # [billing_address_collection] - to display billing address details on the page
            # [customer] - if you have an existing Stripe Customer ID
            # [payment_intent_data] - capture the payment later
            # [customer_email] - prefill the email input in the form
            # For full details see https://stripe.com/docs/api/checkout/sessions/create

            # ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
            checkout_session = stripe.checkout.Session.create(
                success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
                cancel_url=domain_url + 'cancelled/',
                payment_method_types=['card'],
                mode='payment',
                line_items=[
                    {
                        'name': 'T-shirt',
                        'quantity': 1,
                        'currency': 'usd',
                        'amount': '2000',
                    }
                ]
            )
            return JsonResponse({'sessionId': checkout_session['id']})
        except Exception as e:
            return JsonResponse({'error': str(e)})

Here, if the request method is GET, we defined a domain_url, assigned the Stripe secret key to stripe.api_key (so it will be sent automatically when we make a request to create a new Checkout Session), created the Checkout Session, and sent the ID back in the response. Take note of the success_url and cancel_url. The user will be redirected back to those URLs in the event of a successful payment or cancellation, respectively. We'll set those views up shortly.

Don't forget the import:

import stripe

Add the URL:

# payments/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),
    path('config/', views.stripe_config),
    path('create-checkout-session/', views.create_checkout_session), # new
]

XHR Request

Add the event handler and subsequent XHR request to static/main.js:

// static/main.js

console.log("Sanity check!");

// Get Stripe publishable key
fetch("/config/")
.then((result) => { return result.json(); })
.then((data) => {
  // Initialize Stripe.js
  const stripe = Stripe(data.publicKey);

  // new
  // Event handler
  document.querySelector("#submitBtn").addEventListener("click", () => {
    // Get Checkout Session ID
    fetch("/create-checkout-session/")
    .then((result) => { return result.json(); })
    .then((data) => {
      console.log(data);
      // Redirect to Stripe Checkout
      return stripe.redirectToCheckout({sessionId: data.sessionId})
    })
    .then((res) => {
      console.log(res);
    });
  });
});

Here, after resolving the result.json() promise, we called redirectToCheckout with the Checkout Session ID from the resolved promise.

Navigate to http://localhost:8000/. On button click you should be redirected to an instance of Stripe Checkout (a Stripe-hosted page to securely collect payment information) with the T-shirt product information:

Stripe Checkout

We can test the form by using one of the several test card numbers that Stripe provides. Let's use 4242 4242 4242 4242. Make sure the expiration date is in the future. Add any 3 numbers for the CVC and any 5 numbers for the postal code. Enter any email address and name. If all goes well, the payment should be processed, but the redirect will fail since we have not set up the /success/ URL yet.

Flow:

  1. Get Publishable Key

    • Send an XHR request from the client to the server requesting the publishable key
    • Respond with the key
    • Use the key to create a new instance of Stripe.js
  2. Create Checkout Session

    • Send another XHR request to the server requesting a new Checkout Session ID
    • Generate a new Checkout Session and send back the ID
    • Redirect to the checkout page for the user to finish their purchase
  3. Redirect the User Appropriately

    • Redirect to a success page after a successful payment
    • Redirect to a cancellation page after a cancelled payment
  4. Confirm Payment with Stripe Webhooks

    • Set up the webhook endpoint
    • Test the endpoint using the Stripe CLI
    • Register the endpoint with Stripe

Redirect the User Appropriately

Finally, let's wire up the templates, views, and URLs for handling the success and cancellation redirects.

Success template:

<!-- templates/success.html -->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Checkout</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
    <script defer src="https://use.fontawesome.com/releases/v6.4.0/js/all.js"></script>
  </head>
  <body>
  <section class="section">
    <div class="container">
      <p>Your payment succeeded.</p>
    </div>
  </section>
  </body>
</html>

Cancelled template:

<!-- templates/cancelled.html -->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Checkout</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
    <script defer src="https://use.fontawesome.com/releases/v6.4.0/js/all.js"></script>
  </head>
  <body>
  <section class="section">
    <div class="container">
      <p>Your payment was cancelled.</p>
    </div>
  </section>
  </body>
</html>

Views:

# payments/views.py

class SuccessView(TemplateView):
    template_name = 'success.html'


class CancelledView(TemplateView):
    template_name = 'cancelled.html'

URLs:

# payments/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),
    path('config/', views.stripe_config),
    path('create-checkout-session/', views.create_checkout_session),
    path('success/', views.SuccessView.as_view()), # new
    path('cancelled/', views.CancelledView.as_view()), # new
]

Ok, refresh the web page at http://localhost:8000/. Click on the payment button and use the credit card number 4242 4242 4242 4242 again along with the rest of the dummy info. Submit the payment. You should be redirected back to http://localhost:8000/success/.

To confirm a charge was actually made, go back to the Stripe dashboard under "Payments":

Stripe Payments

To review, we used the secret key to create a unique Checkout Session ID on the server. This ID was then used to create a Checkout instance, which the end user gets redirected to after clicking the payment button. After the charge occurred, they are then redirected back to the success page.

Flow:

  1. Get Publishable Key

    • Send an XHR request from the client to the server requesting the publishable key
    • Respond with the key
    • Use the key to create a new instance of Stripe.js
  2. Create Checkout Session

    • Send another XHR request to the server requesting a new Checkout Session ID
    • Generate a new Checkout Session and send back the ID
    • Redirect to the checkout page for the user to finish their purchase
  3. Redirect the User Appropriately

    • Redirect to a success page after a successful payment
    • Redirect to a cancellation page after a cancelled payment
  4. Confirm Payment with Stripe Webhooks

    • Set up the webhook endpoint
    • Test the endpoint using the Stripe CLI
    • Register the endpoint with Stripe

Confirm Payment with Stripe Webhooks

Our app works well at this point, but we still can't programmatically confirm payments and perhaps run some code if a payment was successful. We already redirect the user to the success page after they check out, but we can't rely on that page alone since payment confirmation happens asynchronously.

There are two types of events in Stripe and programming in general: Synchronous events, which have an immediate effect and results (e.g., creating a customer), and asynchronous events, which don't have an immediate result (e.g., confirming payments). Because payment confirmation is done asynchronously, the user might get redirected to the success page before their payment is confirmed and before we receive their funds.

One of the easiest ways to get notified when the payment goes through is to use a callback or so-called Stripe webhook. We'll need to create a simple endpoint in our application, which Stripe will call whenever an event occurs (e.g., when a user buys a T-shirt). By using webhooks, we can be absolutely sure the payment went through successfully.

In order to use webhooks, we need to:

  1. Set up the webhook endpoint
  2. Test the endpoint using the Stripe CLI
  3. Register the endpoint with Stripe

This section was written by Nik Tomazic.

Endpoint

Create a new view called stripe_webhook which prints a message every time a payment goes through successfully:

# payments/views.py

@csrf_exempt
def stripe_webhook(request):
    stripe.api_key = settings.STRIPE_SECRET_KEY
    endpoint_secret = settings.STRIPE_ENDPOINT_SECRET
    payload = request.body
    sig_header = request.META['HTTP_STRIPE_SIGNATURE']
    event = None

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, endpoint_secret
        )
    except ValueError as e:
        # Invalid payload
        return HttpResponse(status=400)
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        return HttpResponse(status=400)

    # Handle the checkout.session.completed event
    if event['type'] == 'checkout.session.completed':
        print("Payment was successful.")
        # TODO: run some custom code here

    return HttpResponse(status=200)

stripe_webhook now serves as our webhook endpoint. Here, we're only looking for checkout.session.completed events which are called whenever a checkout is successful, but you can use the same pattern for other Stripe events.

Make sure to add the HttpResponse import to the top:

from django.http.response import JsonResponse, HttpResponse

The only thing left to do to make the endpoint accessible is to register it in urls.py:

# payments/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),
    path('config/', views.stripe_config),
    path('create-checkout-session/', views.create_checkout_session),
    path('success/', views.SuccessView.as_view()),
    path('cancelled/', views.CancelledView.as_view()),
    path('webhook/', views.stripe_webhook), # new
]

Testing the webhook

We'll use the Stripe CLI to test the webhook.

Once downloaded and installed, run the following command in a new terminal window to log in to your Stripe account:

$ stripe login

This command should generate a pairing code:

Your pairing code is: peach-loves-classy-cozy
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser (^C to quit)

By pressing Enter, the CLI will open your default web browser and ask for permission to access your account information. Go ahead and allow access. Back in your terminal, you should see something similar to:

> Done! The Stripe CLI is configured for Django Test with account id acct_<ACCOUNT_ID>

Please note: this key will expire after 90 days, at which point you'll need to re-authenticate.

Next, we can start listening to Stripe events and forward them to our endpoint using the following command:

$ stripe listen --forward-to localhost:8000/webhook/

This will also generate a webhook signing secret:

> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (^C to quit)

In order to initialize the endpoint, add the secret to the settings.py file:

# djangostripe/settings.py

STRIPE_ENDPOINT_SECRET = '<your webhook signing secret here>'

Stripe will now forward events to our endpoint. To test, run another test payment through with 4242 4242 4242 4242. In your terminal, you should see the Payment was successful. message.

Once done, stop the stripe listen --forward-to localhost:8000/webhook/ process.

If you'd like to identify the user making the purchase, you can use the client_reference_id to attach some sort of user identifier to the Stripe session.

For example:

# payments/views.py

@csrf_exempt
def create_checkout_session(request):
    if request.method == 'GET':
        domain_url = 'http://localhost:8000/'
        stripe.api_key = settings.STRIPE_SECRET_KEY
        try:
            checkout_session = stripe.checkout.Session.create(
                # new
                client_reference_id=request.user.id if request.user.is_authenticated else None,
                success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
                cancel_url=domain_url + 'cancelled/',
                payment_method_types=['card'],
                mode='payment',
                line_items=[
                    {
                        'name': 'T-shirt',
                        'quantity': 1,
                        'currency': 'usd',
                        'amount': '2000',
                    }
                ]
            )
            return JsonResponse({'sessionId': checkout_session['id']})
        except Exception as e:
            return JsonResponse({'error': str(e)})

Register the endpoint

Finally, after deploying your app, you can register the endpoint in the Stripe dashboard, under Developers > Webhooks. This will generate a webhook signing secret for use in your production app.

For example:

Django webhook

Flow:

  1. Get Publishable Key

    • Send an XHR request from the client to the server requesting the publishable key
    • Respond with the key
    • Use the key to create a new instance of Stripe.js
  2. Create Checkout Session

    • Send another XHR request to the server requesting a new Checkout Session ID
    • Generate a new Checkout Session and send back the ID
    • Redirect to the checkout page for the user to finish their purchase
  3. Redirect the User Appropriately

    • Redirect to a success page after a successful payment
    • Redirect to a cancellation page after a cancelled payment
  4. Confirm Payment with Stripe Webhooks

    • Set up the webhook endpoint
    • Test the endpoint using the Stripe CLI
    • Register the endpoint with Stripe

What's Next

On a live website it's required to have HTTPS so your connection is secure. Also while we hardcoded our API keys and webhook signing secret for simplicity, these should really be stored in environment variables. You'll probably want to store the domain_url as an environment variable as well.

Grab the code from the django-stripe-checkout repo on GitHub.

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.