python and django

Django Stripe Tutorial

Django Stripe Tutorial




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

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 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.8 -m venv env
$ source env/bin/activate
(env)$ pip install django==3.0.7
(env)$ django-admin.py startproject djangostripe .

Feel free to swap out virtualenv and Pip for Poetry or Pipenv.

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/v5.3.1/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==2.48.0

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

Stripe Developers

Then from the dropdown list 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" in the left sidebar 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. Send an AJAX request from the client to the server requesting the publishable key
  2. Respond with the key
  3. Use the key to create a new instance of Stripe.js
  4. Send another AJAX request to the server requesting a new Checkout Session ID
  5. Generate a new Checkout Session and send back the ID
  6. Redirect to Checkout
  7. Redirect user back to the Django app after payment

The user will then enter their payment info and finish the purchase. Once done, they will be redirected back to a success page.

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/'
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/v5.3.1/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 AJAX 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
]

AJAX Request

Next, use the Fetch API to make an AJAX 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 -- e.g., 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/v5.3.1/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. Send an AJAX request from the client to the server requesting the publishable key
  2. Respond with the key
  3. Use the key to create a new instance of Stripe.js
  4. Send another AJAX request to the server requesting a new Checkout Session ID
  5. Generate a new Checkout Session and send back the ID
  6. Redirect to Checkout
  7. Redirect user back to the Django app after payment

Create Checkout Session

Moving on, we need to attach an event handler to the button's click event which will send another AJAX 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] - lets capture the payment later
            # [customer_email] - lets you 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 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 redirect 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
]

AJAX Request

Add the event handler and subsequent AJAX 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 several test card numbers 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. Send an AJAX request from the client to the server requesting the publishable key
  2. Respond with the key
  3. Use the key to create a new instance of Stripe.js
  4. Send another AJAX request to the server requesting a new Checkout Session ID
  5. Generate a new Checkout Session and send back the ID
  6. Redirect to Checkout
  7. Redirect user back to the Django app after payment

Redirect Views

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/v5.3.1/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/v5.3.1/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. Send an AJAX request from the client to the server requesting the publishable key
  2. Respond with the key
  3. Use the key to create a new instance of Stripe.js
  4. Send another AJAX request to the server requesting a new Checkout Session ID
  5. Generate a new Checkout Session and send back the ID
  6. Redirect to Checkout
  7. Redirect user back to the Django app after payment

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 here for simplicity, it's a better idea to use environment variables instead to store them. 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.


Django



Join our mailing list to be notified about course updates and new tutorials.

 

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

Get the full course. Learn how to build, test, and deploy a microservice powered by Django, Django REST Framework, and Docker.

View the Course

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

Get the full course. Learn how to build, test, and deploy a microservice powered by Django, Django REST Framework, and Docker.



Table of Contents