How to Integrate Stripe Payment Gateway With Django
Django Website How To

How to Integrate Stripe Payment Gateway With Django

Mishel Shaji
Mishel Shaji

Integrating Stripe payment gateway in your Django project is quite simple. You can start accepting payments from your Django website using Stripe, and it takes only a few minutes. In this post, I'll explain How to Integrate Stripe Payment Gateway with Django using an example.

Downloading Source Code

The source code of this project is available on GitHub. You can download this source code from this link.

Before running the project, replace the stripe Publishable Key and Secret Key in settings.py. Then install the project dependencies using pip install -r requirements.txt.

mishelshaji/Django-StripePayments
Stripe Payment Gateway integration in Django. Contribute to mishelshaji/Django-StripePayments development by creating an account on GitHub.

How to integrate Stripe in your Django project?

Following are the steps that we should follow to integrate Stripe into our Django project.

  1. Create a stripe account.
  2. Verify your business (to use it in production).
  3. Create a new Django Project and add required pages to accept payments or sell products.
  4. Install stripe package using pip install stripe.
  5. Configure stripe checkout.
  6. Accept payments.

Creating a Django Project

Let us start by creating a new Django project. In this project, I'd be using Python 3.9 and Django version 3.2.

First, create a new folder named django-payment-app and navigate to the folder.

mkdir django-payment-app
cd django-payment-app

Now, create a virtual environment for your application. Although you can proceed without creating a virtual environment, I recommend you always create a virtual environment for your projects because, without virtual environments, managing the project dependencies will be a herculean task.

python -m venv env

Activate the virtual environment using:

env/scripts/activate

Then install Django and create a new project using these commands.

pip install django
django-admin startproject paymentsapp

After creating the project, create a new app named payments.

cd paymentsapp
python manage.py startapp payments

Add payments app to settings.py in the list of INSTALLED_APPS.

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

I'll be adding all the code related to stripe payments in this app.

Create Stripe Account

To accept payments using stripe, we should have a Stripe Publishable API key and a Secret Key. You can obtain these items from the Stripe dashboard.

You should not reveal your secret keys to anyone else.

Stripe API Key

To obtain the API Keys, log in to the Stripe dashboard. We have to obtain a pair of productions and test keys. As the name indicates, we'll be using test keys in the development environment, and the production keys will be used when we deploy our app in production.

To obtain the production keys, you should verify your account first. The procedures and documents required to verify your account depend on the country in which you are from. If you do not have a verified account, obtain the test key and secret only.

To obtain the test publishable key and key secret, log in to your Stripe dashboard and turn on Test Mode. From the right side of the page, expand the Get your test API keys section to obtain the publishable key and key secret. Keep these keys safe and never share the keys with anyone else.

If you have a verified account, obtain the live keys also after turning off the test mode.

Configuring stripe

Install stripe using pip install stripe. This the the official Stripe package for Python.

Configuring stripe

After obtaining the Publishable key and secret, we can start integrating stripe in our project. For that, first, we need to as the stripe API key and the secret to settings.py.

Adding the details directly to settings.py is not a security best practice. For example, you may accidentally reveal the API secret to others if you are hosting your code publicly on services like GitHub. But, to be concise, we'll be saving these secrets in settings.py itself.

if DEBUG:
    STRIPE_PUBLISHABLE_KEY = 'test_publishable_key'
    STRIPE_SECRET_KEY = 'test_secret_key'
# Uncomment these lines if you have a live keys
# else:
#     STRIPE_PUBLISHABLE_KEY = 'production_publishable_key'
#     STRIPE_SECRET_KEY = 'production_secret_key'

Create Product Model

Add this code to models.py of the product app. This will create a table named Products in the database with id, name, description, and price columns.

from django.db import models
from django.core import validators

# Create your models here.
class Product(models.Model):
    id = models.BigAutoField(
        primary_key=True
    )

    name = models.CharField(
        max_length=70,
        verbose_name='Product Name'
    )

    description = models.TextField(
        max_length=800,
        verbose_name='Description'
    )

    price = models.FloatField(
        verbose_name='Price',
        validators=[
            validators.MinValueValidator(50),
            validators.MaxValueValidator(100000)
        ]
    )

Commit the changes to the database using:

python manage.py makemigrations
python manage.py migrate

Create Views

The next step is to create all the required views. Following are the views that we'll be creating.

  1. ProductCreateView - To create a new Product.
  2. ProductListView - This is the home page of our application. We'll display a list of products in the database here.
  3. ProductDetailView - As the name indicates, this view will be used to display the details of a product. We'll integrate Stripe Payment Gateway to this page.
  4. create_checkout_session - This view serves as an API to initialize the payment gateway.
  5. PaymentSuccessView - This is that page that users will be redirected to after successful payment.
  6. PaymentFailedView - This is that page that users will be redirected to if the payment failed.
  7. OrderHistoryView - All previous orders and status will be displayed on this page.

Open payments/views.py and add these views. We'll add the logic of these views later.

from django.http.response import HttpResponseNotFound, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse, reverse_lazy
from .models import *
from django.views.generic import ListView, CreateView, DetailView, TemplateView
import stripe
from django.conf import settings
from django.views.decorators.csrf import csrf_exempt
import json

# Create your views here.

class ProductListView(ListView):
    pass


class ProductCreateView(CreateView):
    pass


class ProductDetailView(DetailView):
    pass

@csrf_exempt
def create_checkout_session(request, id):
	pass


class PaymentSuccessView(TemplateView):
    pass

class PaymentFailedView(TemplateView):
    pass

class OrderHistoryListView(ListView):
    pass

Before writhe the code for these views, we should create a common layout page, a navigation bar and other required templates.

Create a new folder named payments in the templates folder of payments and add base.html, navbar.html, product_list.html, product_create.html, product_detail.html, payment_success.html, payment_failed.html, and order_history.html files to the folder.

Here's the folder structure of payments app.

payments
│   admin.py
│   apps.py
│   models.py
│   tests.py
│   urls.py
│   views.py
│   __init__.py
│
├───migrations
├───templates
│   └───payments
│           base.html
│           navbar.html
│           order_history.html
│           payment_failed.html
│           payment_success.html
│           product_create.html
│           product_detail.html
│           product_list.html

Replace the content of project's urls.py as shown below.

paymentsapp/urls.py

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

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

And in the payments/urls.py (app):

from django.urls import path
from .views import *

urlpatterns = [
    path('', ProductListView.as_view(), name='home'),
    path('create/', ProductCreateView.as_view(), name='create'),
    path('detail/<id>/', ProductDetailView.as_view(), name='detail'),
    path('success/', PaymentSuccessView.as_view(), name='success'),
    path('failed/', PaymentFailedView.as_view(), name='failed'),
    path('history/', OrderHistoryListView.as_view(), name='history'),

    path('api/checkout-session/<id>/', create_checkout_session, name='api_checkout_session'),
]

Then, make changes to the template files as shown below.

payments/templates/payments/base.html

<!doctype html>
<html lang="en">

<head>
	<title>Django Payments App</title>
	<!-- Required meta tags -->
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

	<!-- Bootstrap CSS -->
	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
		integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>

<body>
	{% include 'payments/navbar.html' %}

	{% block content %}

	{% endblock content %}

	<!-- Optional JavaScript -->
	<!-- jQuery first, then Popper.js, then Bootstrap JS -->
	<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
	<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</body>

</html>

payments/templates/payments/navbar.html

<nav class="navbar navbar-expand-sm navbar-light bg-light">
    <a class="navbar-brand" href="#">Navbar</a>
    <button class="navbar-toggler d-lg-none" type="button" data-toggle="collapse" data-target="#collapsibleNavId" aria-controls="collapsibleNavId"
        aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="collapsibleNavId">
        <ul class="navbar-nav mr-auto mt-2 mt-lg-0">
            <li class="nav-item active">
                <a class="nav-link" href="{% url 'home' %}">Home</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="{% url 'create' %}">New Product</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="{% url 'history' %}">Order History</a>
            </li>
        </ul>
        <form class="form-inline my-2 my-lg-0">
            <input class="form-control mr-sm-2" type="text" placeholder="Search">
            <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
        </form>
    </div>
</nav>

ProductCreate View

Let us write the code to create products and save them to the database. Modify the code of ProductCreateView as shown below.

class ProductCreateView(CreateView):
    model = Product
    fields = '__all__'
    template_name = "payments/product_create.html"
    success_url = reverse_lazy("home")

If you are new to class-based views in Django, it might be a little bit confusing. But with class-based views and generic views, Django will handle the basic actions like creating and listing data from the database.

Here, the ProductCreateView inherits from CreateView. When we use a CreateView, Django will create a form for the specified model (or you can specify a custom form using the form_class attribute), handle the post method, validate the data and save the data to the database. It abstracts all the code that you would otherwise write in a function-based view.

Important Properties

  1. model - The target model.
  2. fields - Fields that will be displayed in the form. The value __all__ indicates that the form should contain fields for all properties of the model. Primary keys and auto fields will be excluded.
  3. template_name - Name of the template that should be rendered. If this property is not set, Django will try to use a default template.
  4. success_url - URL of the the page that the user should be redirected to, after saving the form.

Open product_create.html and add this code.

{% extends 'payments/base.html' %}

{% block content %}
<h1 class="text-center">Create Product</h1>
<div class="container-fluid">
    <div class="row">
        <div class="col-sm-6 offset-sm-3">
            <form method="post">
                {{ form.as_p }}
                {% csrf_token %}
                <div class="form-group">
                    <button type="submit" class="btn btn-primary">Save</button>
                </div>
            </form>
        </div>
    </div>
</div>
<script>
    document.querySelectorAll('input, textarea').forEach(e => {
        e.classList.add('form-control');
    })
</script>
{% endblock content %}

ProductList View

In this page, we'll display the products from the database.

views.py

class ProductListView(ListView):
    model = Product
    template_name = "payments/product_list.html"
    context_object_name = 'product_list'

Important Properties

  1. context_object_name - Name of the context object that will hold the list of products. Django will automatically fetch the list of products, so that we do not have to write the query to fetch products from the database.

product_list.html

{% extends 'payments/base.html' %}

{% block content %}
<h1 class="text-center">Product List</h1>
<div class="container">
    {% if product_list %}
    <div class="row">


        {% for p in product_list %}
        <div class="col-sm-4">
            <div class="card">
                <img class="card-img-top" src="https://dummyimage.com/200x150.jpg?text={{ p.name }}" alt="">
                <div class="card-body">
                    <h4 class="card-title">{{ p.name }}</h4>
                    <p class="card-text">{{ p.description }}</p>
                </div>
                <div class="card-footer d-flex">
                    <a href="{% url 'detail' id=p.id %}" class="btn btn-success ml-auto">Buy Now</a>
                </div>
            </div>
        </div>
        {% endfor %}

    </div>
    {% else %}
    <div class="alert alert-info text-center mt-5">
        The product list is empty. Please add some products first.
    </div>
    {% endif %}
</div>
{% endblock content %}

Run the project using python manage.py runserver and some products to the database.

Integrating Stripe Payment to Product Detail Page

This is the most important page of our project. In the page, we'll display the details of a selected product along with a checkout button to purchase the product. We will also integrate the Stripe payment gateway on this page.

Modify the code of ProductDetailPage as shown below.

class ProductDetailView(DetailView):
    model = Product
    template_name = "payments/product_detail.html"
    pk_url_kwarg = 'id'

    def get_context_data(self, **kwargs):
        context = super(ProductDetailView, self).get_context_data(**kwargs)
        context['stripe_publishable_key'] = settings.STRIPE_PUBLISHABLE_KEY
        return context  

Here in this code, we are overriding the get_context_data() method to add publishable key as a data to the template context.

We set pk_url_kwarg = 'id' to instruct Django to fetch details of the product with the id passed as a URL parameter.

We should pass the Stripe Publishable key to the client-side as a template content object. The JavaScript SDK of Stripe that we will add in the template of this view requires this publishable key to authenticate our request and redirect the user to the payment page.

product_detail.html

{% extends 'payments/base.html' %}

{% block content %}
<h1 class="text-center">Product Detail</h1>
<div class="container">

    <div class="card">
        <div class="card-header">
            <h2>Product Detail</h2>
        </div>
        <div class="card-body">
            <div class="container row">
                <div class="col-md-2">
                    <img src="https://dummyimage.com/150x150.gif?text={{ object.name }}" alt="">
                </div>
                <div class="col-md-10">
                    <h1>Name: {{ object.name }}</h1>
                    <p>Description: {{ object.description }}</p>
                    <p>Price: {{ object.price }}</p>

                    <div class="form-group">
                        <label for="email">Email: </label>
                        <input type="email" name="email" id="email" class="form-control" placeholder="Email">
                        <small>Please enter your email address</small>
                    </div>
                </div>
            </div>
        </div>
        <div class="card-footer d-flex">
            <button class="btn btn-success ml-auto" id="checkout-button">Checkout</button>
        </div>
    </div>
</div>
<!-- Add JavaScript Here-->
{% endblock content %}

This is the basic code of product_datail.html. It contains a bootstrap card to display the details of the product that the user wants to purchase.

I have also added an email field to the template. We'll use this email to pre-fill the payment gateway page and it will be used as an identifier of a user.

We can remove this email field from the view if we are allowing only authenticated users to purchase items from our website.

The OrderDetails Model

The user should be redirected to a payment page hosted by stripe after clicking the checkout button. If we want to redirect a user to the payment page, we should pass a payment session id along with the request. This session id can be created only from the server-side as it requires our secret key.

After creating the checkout session id, we will store it in the database along with the order details to the database. For that, add a new model to the database as shown in this code.

products/models.py

from django.db import models
from django.core import validators

# Create your models here.
class Product(models.Model):
    # Code removed for brevity

class OrderDetail(models.Model):

    id = models.BigAutoField(
        primary_key=True
    )

    # You can change as a Foreign Key to the user model
    customer_email = models.EmailField(
        verbose_name='Customer Email'
    )

    product = models.ForeignKey(
        to=Product,
        verbose_name='Product',
        on_delete=models.PROTECT
    )

    amount = models.IntegerField(
        verbose_name='Amount'
    )

    stripe_payment_intent = models.CharField(
        max_length=200
    )

    # This field can be changed as status
    has_paid = models.BooleanField(
        default=False,
        verbose_name='Payment Status'
    )

    created_on = models.DateTimeField(
        auto_now_add=True
    )

    updated_on = models.DateTimeField(
        auto_now_add=True
    )

Save the model and run:

python manage.py makemigrations
python manage.py migrate

To create the session id, we will use the create_checkout_session view. Unlike other views that we used in this project, this is a function-based view and it should accept only post methods.

Creating Stripe Checkout Session

Add the following code to create_checkout_session view.

@csrf_exempt
def create_checkout_session(request, id):

    request_data = json.loads(request.body)
    product = get_object_or_404(Product, pk=id)

    stripe.api_key = settings.STRIPE_SECRET_KEY
    checkout_session = stripe.checkout.Session.create(
        # Customer Email is optional,
        # It is not safe to accept email directly from the client side
        customer_email = request_data['email'],
        payment_method_types=['card'],
        line_items=[
            {
                'price_data': {
                    'currency': 'usd',
                    'product_data': {
                    'name': product.name,
                    },
                    'unit_amount': int(product.price * 100),
                },
                'quantity': 1,
            }
        ],
        mode='payment',
        success_url=request.build_absolute_uri(
            reverse('success')
        ) + "?session_id={CHECKOUT_SESSION_ID}",
        cancel_url=request.build_absolute_uri(reverse('failed')),
    )

    # OrderDetail.objects.create(
    #     customer_email=email,
    #     product=product, ......
    # )

    order = OrderDetail()
    order.customer_email = request_data['email']
    order.product = product
    order.stripe_payment_intent = checkout_session['payment_intent']
    order.amount = int(product.price * 100)
    order.save()

    # return JsonResponse({'data': checkout_session})
    return JsonResponse({'sessionId': checkout_session.id})

In this view, we are performing three main actions.

  1. Creating a Stripr checkout session using the Stripe Library.
  2. Saving the order details along with the payment intent obtained from stripe session. You can consider payment intent as a unique identifier for each payments.
  3. Return the session ID as JSON data.

To create a new checkout session, we should provide these details.

  1. customer_email - This is an optional field. If specified, this email will be used to identity a customer in Stripe. This email will also be displayed on the payment page.
  2. payment_method_types - Payment methods that the user can use to make payments. You can read more about payment types here.
  3. line_items - Details about the products that the customer is purchasing. If you want to learn more about customizing line_items, check the documentation.
  4. unit_amount - Price of the product multiplied by 100. It should be an integer value.
  5. quantity - An integer value that indicates the order count.
  6. success_url - Full URL of that page that the user should be redirected after a successful payment. You can use this page to display a success message and mark the order as completed. Note that I am appending ?session_id={CHECKOUT_SESSION_ID} to the end of the URL. This part tells Stripe to append the checkout session id as a parameter to the URL of the page, so that we can identity the payment that was successful and mark the corresponding order as completed.
  7. cancel_url - Full URL of that page that the user should be redirected if the payment failed for some reason.
  8. mode - Indicates the type of payment. It can be a single payment or a subscription.

Redirecting the user

In the client side, we should add the Stripe JavaScript SDK and redirect the user to a payment page.

Add this code to the end of product_detail.html just before the {% endblock content %}.


<script src="https://js.stripe.com/v3/"></script>
<script type="text/javascript">
    // Create an instance of the Stripe object with your publishable API key
    var stripe = Stripe('{{ stripe_publishable_key }}');
    var checkoutButton = document.getElementById('checkout-button');

    checkoutButton.addEventListener('click', function () {

        var email = document.getElementById('email').value;
        if (email.length == 0) {
            alert("Please enter your email address.");
            return;
        }

        // Create a new Checkout Session using the server-side endpoint you
        // created in step 3.
        fetch("{% url 'api_checkout_session' id=object.id %}", {
            method: 'POST',
            body: JSON.stringify(
                { email: email }
            )
        })
            .then(function (response) {
                return response.json();
            })
            .then(function (session) {
                return stripe.redirectToCheckout({ sessionId: session.sessionId });
            })
            .then(function (result) {
                // If `redirectToCheckout` fails due to a browser or network
                // error, you should display the localized error message to your
                // customer using `error.message`.
                if (result.error) {
                    alert(result.error.message);
                }
            })
            .catch(function (error) {
                console.error('Error:', error);
            });
    });
</script>

This code will make an AJAX request to create_checkout_session view and collect the checkout session id. After obtaining the id, the user will be redirected to a Stripe hosted payment page.

Handling Successful Payments

After a successful payment, the user will be sent back to the success page of our website.

payments/views.py

class PaymentSuccessView(TemplateView):
    template_name = "payments/payment_success.html"

    def get(self, request, *args, **kwargs):
        session_id = request.GET.get('session_id')
        if session_id is None:
            return HttpResponseNotFound()
        
        stripe.api_key = settings.STRIPE_SECRET_KEY
        session = stripe.checkout.Session.retrieve(session_id)

        order = get_object_or_404(OrderDetail, stripe_payment_intent=session.payment_intent)
        order.has_paid = True
        order.save()
        return render(request, self.template_name)

In the get handler of PaymentSuccessView, we are using the session_id obtained from the URL to mark the Order as completed.

payment_success.html

{% extends 'payments/base.html' %}

{% block content %}
<div class="container">
    <div class="jumbotron text-center mt-5">
        <h1 class="text-success">Payment Success</h1>
        <p>We received your order. You can now close this window or use the
            link below to visit 
            <a href="{% url 'home' %}">home page</a>.
        </p>
    </div>
</div>
{% endblock content %}

Payment Failure Page

Modify the view and template files as shown below.

payments/views.py

class PaymentFailedView(TemplateView):
    template_name = "payments/payment_failed.html"

payment_failed.html

{% extends 'payments/base.html' %}

{% block content %}
<div class="container">
    <div class="jumbotron text-center mt-5">
        <h1 class="text-danger">Payment Failed</h1>
        <p>We received your order. You can now close this window or use the
            link below to visit
            <a href="{% url 'home' %}">home page</a>.
        </p>
    </div>
</div>
{% endblock content %}

Order History Page

In this page, we can view the payment history.

payments/views.py

class OrderHistoryListView(ListView):
    model = OrderDetail
    template_name = "payments/order_history.html"

order_history.html

{% extends 'payments/base.html' %}

{% block content %}
    <h1 class="text-center">Order History</h1>
    <div class="container">
        
        {% if object_list %}
        <table class="table table-striped table-bordered table-hover">
            <thead>
                <tr>
                    <th>Product</th>
                    <th>Customer Email</th>
                    <th>Amount</th>
                    <th>Status</th>
                    <th>Date</th>
                </tr>
            </thead>
            <tbody>
                
                {% for i in object_list %}
                    <tr>
                        <td>{{ i.product.name }}</td>
                        <td>{{ i.customer_email }}</td>
                        <td>{{ i.amount }}</td>
                        <td>
                            {% if i.has_paid %}
                                <b class="text-success">Success</b>
                            {% else %}
                                <b class="text-danger">Failed</b>
                            {% endif %}
                        </td>
                        <td>{{ i.created_on }}</td>
                    </tr>
                {% endfor %}
                    
            </tbody>
        </table>
        {% else %}
        <div class="alert alert-info">
            Payment history is empty.
        </div>
        {% endif %}
            
    </div>
{% endblock content %}

This is how we can integrate Stripe Payment Gateway with our Django Application. If you face any issues, check the stripe documentation or share your issues in the comment section.