Django and Stripe Integration

May 13, 2020, 5:41 p.m.

Django and Stripe Integration

As one of the largest online payment processors, Stripe allows developers to quickly set up payment gateways for their applications.  If you ever wanted to create your own e-commerce store or subscription-based service, integrating Stripe with your web app is an easy way to get started.

 

Django setup

Integrating Stripe with Django is simple.  First, create a checkout page for your web app.  The page should display the products from a user's cart.  The "cart" can be a model field under the profile model if you are using a one-to-one link to extend the user. 

env > mysite > main > models.py

from django.db import models
from django.contrib.auth.models import User
from django.dispatch import receiver 
from django.db.models.signals import post_save

class Product(models.Model):
	product_name = models.CharField(max_length=150)
	product_type = models.CharField(max_length=25)
	product_description = models.TextField()
	product_price = models.IntegerField()




class Profile(models.Model):  
	user = models.OneToOneField(User, on_delete=models.CASCADE)
	cart = models.ManyToManyField(Product)

	@receiver(post_save, sender=User) 
	def create_user_profile(sender, instance, created, **kwargs):
		if created:
			Profile.objects.create(user=instance)

	@receiver(post_save, sender=User) 
	def save_user_profile(sender, instance, **kwargs):
		instance.profile.save()

 

env > mysite > main > views.py

from django.shortcuts import render, redirect
from .models import Product, Profile

...

def checkout(request)
	if request.user.is_authenticated:
		cart = Profile.objects.get(user= request.user).cart
		total = cart.aggregate(Sum('product_price'))['product_price__sum']
		return render(request,"main/checkout.html", {"cart":cart, "total":total})
	else:
		redirect("main:homepage")

 

env > mysite > main > templates > main > checkout.html

{% extends "main/header.html" %}

    {% block content %}

    <div>
      <br><br>
      <h2> Checkout </h2>
      <br><br>
      <table style="width:100%">
        <tr style=" font-size: 20px">
          <th>Name</th>
          <th>Price</th>
        </tr>
          
        {% for c in cart.all %}
        <tr>
          <td>{{ c.product_name }}</td>
          <td>${{ c.product_price }}</td>
        </tr>
        {% endfor %}

        <tr>
          <td style="padding-top:30px">Total</td>
          <td>${{ total }}</td>
        </tr>
      </table>
    </div>

    {% endblock %}

If you are interested in using the Django template tags extend and block content, learn how to extend the header.html.

 

checkout page

 

Create a Stripe account

Now that we have a basic checkout page that lists the products in a user's cart, let's integrate with Stripe.  First, create a Stripe account.  

create stripe account

 

 

Stripe installation

Next, install the Stripe package to have access to the official libraries that give access to Stripe's API.

CLI

pip install --upgrade stripe

 

Django integration

Import stripe, @csrf_exempt, json, and JsonResponse at the top of the views.py page and then create a new function that creates a payment intent.  Basically, a payment intent is setting up the data we need to create a purchase.  This function will be called from the checkout template using a JavaScript function provided by Stripe.

The first value we need is the total payment amount.  While we could get this from our checkout page, it's unsafe to do so given a user could change the amount on the page.  In this case, we will calculate the total the same way as before.

Next plugin in your test secret key from Stripe.  You'll notice we check for a POST request method.  We will create this in the checkout.html page soon.  Notice that for the amount we use total.  Finally, enter your test publishable key for the JSON Response.

env > mysite > main > views.py

from django.shortcuts import render, redirect
from .models import Product, Profile
from django.views.decorators.csrf import csrf_exempt
import stripe
import json
from django.http import JsonResponse

...

def checkout(request)
...

@csrf_exempt
def createpayment(request):
	if request.user.is_authenticated:
		cart  = Profile.objects.get(user=request.user).products
		total = cart.aggregate(Sum('product_price'))['product_price__sum']
		total = total * 100
		stripe.api_key = 'your test secret key'
		if request.method=="POST":
			
			data = json.loads(request.body)
			# Create a PaymentIntent with the order amount and currency
			intent = stripe.PaymentIntent.create(
				amount=total,
				currency=data['currency'],
				metadata={'integration_check': 'accept_a_payment'},
				)
			try:
				return JsonResponse({'publishableKey':	
					'your test publishable key', 'clientSecret': intent.client_secret})
			except Exception as e:
				return JsonResponse({'error':str(e)},status= 403)

 

Alright, now that we have a payment intent, we need to configure checkout.html to retrieve and then process the payment intent.  First, let's add the credit card form and the hidden form that will be submitted when the payment is complete.  Add just below the table.  Feel free to add the CSS as well.

env > mysite > main > templates > main > checkout.html

{% extends "main/header.html" %}

    {% block content %}

    <div>
      <br><br>
      <h2> Checkout </h2>
      <br><br>
      <table style="width:100%">
        ...
      </table>

      <h4 class="font-weight-bold my-3">Billing</h4>
    	<div class="card mx-5">
    		<div class="card-body">
    			<div class="sr-root">
    				<div class="sr-main">
    					<form id="payment-form" class="sr-payment-form">
    				    {% csrf_token %}
                	<div class="sr-combo-inputs-row">
                  	<div class="sr-input sr-card-element" id="card-element"></div>
                	</div>
                	<div class="sr-field-error" id="card-errors" role="alert"></div>
                	<button id="submit" class="btn">
                    <div class="spinner-border  spinner-border-sm text-light hidden" id="spinner" role="status">
                     	<span class="sr-only">Loading...</span>
                    </div>
                    <span id="button-text">Pay</span><span id="order-amount"></span>
                	</button>
              </form>
    	        <div class="sr-result hidden">
    	          <p>Payment completed<br></p>
    	          <pre>
    	            <code></code>
    	          </pre>
    	        </div>
    		    </div>
    			</div>
        </div>
    	</div>
			    
		  <form id="payload" class="hidden" action="/payment-complete" method="post">
		    {% csrf_token %}
		    <input id ="data-payload" type="hidden" name="payload"/>
		  </form>

    </div>

    <style>
      /**
     * The CSS shown here will not be introduced in the Quickstart guide, but shows
     * how you can use CSS to style your Element's container.
     */
    .StripeElement {
      box-sizing: border-box;

      height: 40px;

      padding: 10px 12px;

      border: 1px solid transparent;
      border-radius: 4px;
      background-color: white;

      box-shadow: 0 1px 3px 0 #e6ebf1;
      -webkit-transition: box-shadow 150ms ease;
      transition: box-shadow 150ms ease;
    }

    .StripeElement--focus {
      box-shadow: 0 1px 3px 0 #cfd7df;
    }

    .StripeElement--invalid {
      border-color: #fa755a;
    }

    .StripeElement--webkit-autofill {
      background-color: #fefde5 !important;
    }
    .hidden {
        display: none;
    }


    #submit:hover {
      filter: contrast(120%);
    }

    #submit {
      font-feature-settings: "pnum";
      --body-color: #f7fafc;
      --button-color: #556cd6;
      --accent-color: #556cd6;
      --gray-border: #e3e8ee;
      --link-color: #fff;
      --font-color: #697386;
      --body-font-family: -apple-system,BlinkMacSystemFont,sans-serif;
      --radius: 4px;
      --form-width: 400px;
      -webkit-box-direction: normal;
      word-wrap: break-word;
      box-sizing: border-box;
      font: inherit;
      overflow: visible;
      -webkit-appearance: button;
      -webkit-font-smoothing: antialiased;
      margin: 0;
      font-family: inherit;
      -webkit-tap-highlight-color: transparent;
      font-size: 16px;
      padding: 0 12px;
      line-height: 32px;
      outline: none;
      text-decoration: none;
      text-transform: none;
      margin-right: 8px;
      height: 36px;
      border-radius: var(--radius);
      color: #fff;
      border: 0;
      margin-top: 16px;
      font-weight: 600;
      cursor: pointer;
      transition: all .2s ease;
      display: block;
      box-shadow: 0 4px 5.5px 0 rgba(0,0,0,.07);
      width: 100%;
      background: var(--button-color);
    }
    </style>

    {% endblock %}

payment setup

 

Now let's add the following scripts to checkout.html to get then process the payment intent.  The first belongs at the top of the page and the second can be placed right before the endblock tag.

env > mysite > main > templates > main > checkout.html

{% extends "main/header.html" %}

    {% block content %}
    <script src="https://js.stripe.com/v3/"></script>

    ...

    {% block content %}

 

env > mysite > main > templates > main > checkout.html

{% extends "main/header.html" %}

    {% block content %}

    ...



    <script type="text/javascript">
    // A reference to Stripe.js


    var orderData = {
      items: [{ id: "products" }],
      currency: "usd",
    };

    // Disable the button until we have Stripe set up on the page
    document.getElementById("submit").disabled = true;

    fetch("/create-payment-intent", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(orderData)
    })
      .then(function(result) {
        return result.json();
      })
      .then(function(data) {
        return setupElements(data);
      })
      .then(function({ stripe, card, clientSecret }) {
        document.getElementById("submit").disabled = false;

        // Handle form submission.
        var form = document.getElementById("payment-form");
        form.addEventListener("submit", function(event) {
          event.preventDefault();
          // Initiate payment when the submit button is clicked
          pay(stripe, card, clientSecret);
        });
      });

    // Set up Stripe.js and Elements to use in checkout form
    var setupElements = function(data) {
      stripe = Stripe(data.publishableKey);
      var elements = stripe.elements();
      var style = {
        base: {
          color: "#32325d",
          fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
          fontSmoothing: "antialiased",
          fontSize: "16px",
          "::placeholder": {
            color: "#aab7c4"
          }
        },
        invalid: {
          color: "#fa755a",
          iconColor: "#fa755a"
        }
      };

      var card = elements.create("card", { style: style });
      card.mount("#card-element");

      return {
        stripe: stripe,
        card: card,
        clientSecret: data.clientSecret
      };
    };

    /*
     * Calls stripe.confirmCardPayment which creates a pop-up modal to
     * prompt the user to enter extra authentication details without leaving your page
     */
    var pay = function(stripe, card, clientSecret) {
      changeLoadingState(true);

      // Initiate the payment.
      // If authentication is required, confirmCardPayment will automatically display a modal
      stripe
        .confirmCardPayment(clientSecret, {
          payment_method: {
            card: card
          }
        })
        .then(function(result) {
          if (result.error) {
            // Show error to your customer
            showError(result.error.message);
          } else {
            // The payment has been processed!
            orderComplete(clientSecret);
          }
        });
    };

    /* ------- Post-payment helpers ------- */

    /* Shows a success / error message when the payment is complete */
    var orderComplete = function(clientSecret) {
      // Just for the purpose of the sample, show the PaymentIntent response object
      stripe.retrievePaymentIntent(clientSecret).then(function(result) {
        var paymentIntent = result.paymentIntent;
        var paymentIntentJson = JSON.stringify(paymentIntent, null, 2);
        
        // post data and show new page
        var form2 =document.getElementById("payload");
        var input = document.getElementById("data-payload")
        input.value = paymentIntentJson;
        form2.submit();
        changeLoadingState(false);
      });
    };

    var showError = function(errorMsgText) {
      changeLoadingState(false);
      var errorMsg = document.querySelector(".sr-field-error");
      errorMsg.textContent = errorMsgText;
      setTimeout(function() {
        errorMsg.textContent = "";
      }, 4000);
    };

    // Show a spinner on payment submission
    var changeLoadingState = function(isLoading) {
      if (isLoading) {
        document.getElementById("submit").disabled = true;
        document.querySelector("#spinner").classList.remove("hidden");
        document.querySelector("#button-text").classList.add("hidden");
      } else {
        document.getElementById("submit").disabled = false;
        document.querySelector("#spinner").classList.add("hidden");
        document.querySelector("#button-text").classList.remove("hidden");
      }
    };
    </script>

   {% endblock %}

Although a little complicated, this script fetches the payment intent from the function we just created, enables the Stripe payment form, and handles the payment submission.  

 

Add "create-payment-intent" to urls.py so we can fetch the payment intent from the view.

env > mysite > main > urls.py

from django.urls import path
from . import views

app_name = "main"   


urlpatterns = [
   ...
   path("create-payment-intent", views.createpayment, name="create-payment-intent"),

]

 

Looks good.

setup after

 

Also, notice the second form's action is "/payment-complete".  Make sure to add this to your urls.py and add the final view that sends the user to a page informing them of their purchase.  If you want to send them an email of their purchase, you could do so in this view or configure a webhook to handle it.  

env > mysite > main > urls.py

from django.urls import path
from . import views

app_name = "main"   


urlpatterns = [
   ...
   path("create-payment-intent", views.createpayment, name="create-payment-intent"),
   path("payment-complete", views.paymentcomplete, name="payment-complete"),

]

 

env > mysite > main > views.py

def paymentcomplete(request):
	if request.method=="POST":
		data = json.loads(request.POST.get("payload"))
		if data["status"] == "succeeded":
			# save purchase here/ setup email confirmation
		return render(request, "main/payment-complete.html")

You could also add a Purchase model that contains information about a purchase.  In any case, you can get the payment amount from "data".

 

Testing

To test the purchase form, use 4242 4242 4242 4242 as the test credit card.  Then go to your Stripe dashboard to check if the payment succeeded.  

stripe payment dashboard

 

Production

In production, add your live secret key and publishable key.  Make sure to secure this sensitive information with environment variables.  For more information on securing sensitive data, check out Securing Sensitive Django Variables using Python Decouple.


1
Subscribe now

Subscribe to stay current on our latest articles and promos





Post a Comment
Join the community

4 Comments


Mldjango Oct. 14, 2020, 8:50 a.m.

Hi! I love this guide, it has gotten my further than anything else, I can find. How would you do get_or_create_customer in the create payment view? So that the transaction is saved to an existing customer or a new one is created?

James replying to Mldjango Oct. 15, 2020, 2:04 p.m.

In the createpayment view, before instantiating a payment intent, you will need to create a customer. Then you can pass the customer id as an attribute when instantiating the payment intent.

James replying to James Oct. 15, 2020, 3:13 p.m.

Check out our article on Django and Stripe monthly subscriptions to learn more about creating, storing, and syncing Stripe customers.

Mldjango replying to James Oct. 16, 2020, 2:05 a.m.

Thank you! I'll try that :)