The tutorial assumes that you have a good understanding of a bit of Javascript( specially AJAX fetch and handling promise), good grasp of python and django, and little understanding of HTML and CSS.
git clone https://github.com/sunilale0/Django-Stripe-Checkout.git
cd Django-Stripe-Checkout
Start a virtual environment
pip install -r requirements.txt
I have used a separate settings file called user_settings.py
in djangostripe/
to hard code these secret keys and client ids. I have import this file somewhere at the bottom in djangostripe/settings.py
.
The variables that are necessary in user_settings.py
are:
STRIPE_PUBLISHABLE_KEY = "sxxdfsdfdffffffffffffffffffffffffffffffffffffffffff"
STRIPE_SECRET_KEY = "asdfjalsdfnaslfnsdknfsldwerwer"
STRIPE_ENDPOINT_SECRET = "adfasdfasdfxcvd343adfaf"
Signup for an account in Stripe. Obtain the keys above. Create a test product. Follow the section add-stripe for more details.
Start the server
python manage.py runserver
visit: http://localhost:8000/
py -m venv venv
venv\Scripts\activate
pip install django==3.1
django-admin startproject djangostripe .
python manage.py startapp payments
add 'payments.apps.PaymentsConfig',
to the INSTALLED_APPS
configuration in settings.py
Update django/urls.py
with the following:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('payments.urls')),
]
Add and fill up the following in payments/urls.py
:
from django.urls import path
from . import views
urlpatterns =[
path('', views.HomePageView.as_view(), name="home"),
]
Add the following codes in payments/views.py
from django.views.generic.base import TemplateView
class HomePageView(TemplateView):
template_name = "home.html"
Create a templates
folder and create home.html
file inside it
mkdir templates
touch templates/home.html
fill templates/home.html
with:
<!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/bulma@0.8.2/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>
update settings.py
to let Django know where to look fo templates
folder.
TEMPLATES = [
{
#...,
'DIRS':["templates"],
#...,
}
]
Finally, migrate to sync the database and runserer to start Django's local web server:
python manage.py migrate
python manage.py runserver
Install stripe and register for an account.
pip install stripe
Fetch two keys publishable key
and secret key
from https://dashboard.stripe.com/test/apikeys
and save them at the end of the file settings.py
STRIPE_PUBLISHABLE_KEY = "your test publishable key"
STRIPE_SECRET_KEY = "your test secret key"
I am going to put those keys in a separate file in the same folder as settings.py
and import that file by doing
from that_file import *
so that I can exclude this file from being uploaded to github through .gitignore
, so that the secret key will be protected.
Finally, account name withing the account settings for stripe.
Now, we need to create a product in stripe with random values. we only need Name, price and select "one time" payment.
After the user clicks the purchase button we need to do the following:
- Get a Publishable Key
- Send an AJAX request from the client to the server requesting the publishable key
- Respond with a key
- Use the Key to create a new instance of Stripe.js
- Create Checkout Session
- Send another AJAX 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
- Redirect the User Appropriately
- Redirect to a success page after a successful payment
- Redirect to a cancellation page after a cancelled payment
- Confirm Payment with Stripe Webhooks
- Set up webhook endpoint
- Test the endpoint using the Stripe CLI
- Register the endpoint with Stripe
Create a static file to hold all our javascript
mkdir static
touch static/main.js
Add a quick check log to the main.js
file at static/main.js
:
console.log("Sanity check!");
Update the djangostripe/settings.py
so Django knows where to find static files:
STATIC_URL = '/static/'
STATICFILES_DIRS = [Path(BASE_DIR).joinpath('static')]
Add the static template tag in the new script tag inside the HTML template:
{% load static %}
<!-- ... -->
<!-- <link rel="stylesheet" ... > -->
<script src="{% static 'main.js' %}"></script>
<!-- <script defer src="https.//use.fotawesome ... > -->
<!-- ... -->
Now reload 127.0.0.1:8000
and check chrome dev tools, usually (right-click
on the page and select inspect
). And check console. If the text Sanity check!
is displayed, then the setup has worked.
Now, add a new view to payments/views.py
to handle AJAX request:
from django.conf import settings
from django.http.response import JsonResponse
from django.views.decorators.csrf import csrf_exempt
# from ...
@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 for it at payments/urls.py
:
# from ...
urlpatterns = [
# ...,
path('config/', views.stripe_config),
]
Next, use Fetch API to make an AJAX request to the new /config/
endpoint in static/main.js
:
// ...
fetch("/config/")
.then((result) => {
return result.json();
})
.then((data) => {
// Initialize Stripe.js
const stripe = Stripe(data.publicKey);
});
Include Stripe.js in templates/home.html like so:
<!-- -->
<script src="https://js.stripe.com/v3/"></script>
<!-- -->
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
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.
add a new view in payments/views.py
# from ..
import stripe
# ...
@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 +"canceled/",
payment_method_types=["card"],
mode="payment",
line_items=[
{
"name": "Sunglass 101",
"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 iwll be sent automatically when we make a request to creat 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.
Add the URL in payments/urls.py
:
# from ...
urlpatterns = [
# ...,
path("create-checkout-session/", views.create_checkout_session),
]
Add event handler and subsequent AJAX request to static/main.js
// ...
// 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 });
})
.catch((res) => {
console.log(res);
});
});
Now, if we click on Purchase!
, we will be redirected to an instance of Stripe Checkout, a Stripe-hosted page to securely collect payment information.
To test the form, use
- one card number that works is
4242 4242 4242 4242
or choose any one of the test card number that stripe provides here - a future expiration date
- any 3 numbers for the CVC
- any 5 numbers for postal code
- any email address and name
With the correct information, the payment should be successful and we will be redirected to localhost:8000/success?session_id=cs_test_ASDFAREWS...
, which is a page that does not exist yet. But no worries, that is the least of the problems now.
Finally, let's put together templates, views, and URLs for handling the success and cancellation redirects.
Success template in 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/bulma@0.8.2/css/bulma.min.css"
/>
<script
defer
src="https://use.fontawesome.come/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>
Canceled templates in templates/canceled.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/bulma@0.8.2/css/bulma.min.css"
/>
<script
defer
src="https://use.fontawesome.come/release/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>
Add two views for success and cancellation in payments/views.py
class SuccessView(TemplateView):
template_name="success.html"
class CanceledView(TemplateView):
template_name="canceled.html"
URLs:
# from ...
urlpatterns = [
# ...,
path("success/", views.SuccessView.as_view()),
path("canceled/", views.CanceledView.as_view()),
]
To review, we used the secret key to create a unique Checkout Session ID on the server. When we click the Purchase!
button, we get redirected to a Checkout instance created using Checkout Session ID on the stripe server. We get redirected to a success
page or canceled
page depending the choice of the user and/or whether valid information was provided.
The app works well at this point, but we still haven't programmatically confirmed if the payment was successful.
There are two types of events in Stripe:
- Synchronous events - which have an immediate effect and results (e.g., creating a customer)
- Asynchronous events - which don't have an immediate result like trying to confirm payments.
Because of the asynchronous nature of payment confirmation, the user might get redirected to the success before the payment was confirmed.
Hence, we need to verify payement confirmation, and we will use Stripe webhook for that. Webhooks refers to a combination of elements that collectively create a notification and reaction system within a larger integration. We will need to create a simple endpoint in our application, which Stripe will call whenever an event occurs (i.e. when a user buys the product only for now ). In order to use webhooks, we need to:
- Set up the webhook endpoint
- Test the endpoint using Stripe Cli
- Register the endpoint with Stripe
Create a new view stripe_webhook
in payments/view.py
:
# from ...
from django.http.response import JsonResponse, HttpResponse
# ...
@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)
Register the view at payments/urls.py
# from ...
urlpatterns = [
# ...,
path("webhook/", views.stripe_webhook),
]
Install Stripe CLI from here. Once installed, log in.
stripe login
Next, start listening to Stripe events and forward them to our endpoint using the following command:
stripe listen --forward-to localhost:8000/webhook/
This will give us an output that looks like
> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxx (^C to quit)
To initialize the endpoint, add the secret to the djanogostripe/settings.py
file. In my case I have created a separate djangostripe/user_settings.py
file that I will importing in djangostripe/settings.py
:
STRIPE_ENDPOINT_SECRET = "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Stripe will now forward events to our endpoint. Go to localhost:8000
and test purchase again. If the webhook is working, we should see similar messages in the terminal:
2020-12-26 09:53:21 --> payment_intent.created [evt_zdfadfasdfasdf232]
2020-12-26 09:53:21 <-- [200] POST http://localhost:8000/webhook/ [evt_zdfadfasdfasdf232]
2020-12-26 09:54:02 --> charge.succeeded [evt_adfasdfasdfsadfsfdsdf]
2020-12-26 09:54:02 <-- [200] POST http://localhost:8000/webhook/ [evt_adfasdfasdfsadfsfdsdf]
2020-12-26 09:54:03 --> payment_method.attached [evt_qweqwvuwernfsdfa]
2020-12-26 09:54:03 <-- [200] POST http://localhost:8000/webhook/ [evt_qweqwvuwernfsdfa]
2020-12-26 09:54:04 --> customer.created [evt_erwqerqefwsdfadf]
2020-12-26 09:54:04 <-- [200] POST http://localhost:8000/webhook/ [evt_adsfaewrwerwer]
2020-12-26 09:54:04 --> payment_intent.succeeded [evt_qwe3rqewrnlkncv]
2020-12-26 09:54:04 <-- [200] POST http://localhost:8000/webhook/ [evt_qwe3rqewrnlkncv]
2020-12-26 09:54:05 --> checkout.session.completed [evt_cvnzvklzxcilviweresdf]
2020-12-26 09:54:05 <-- [200] POST http://localhost:8000/webhook/ [evt_wsxczsdsadfsdsdf]
There are several other sessions objects like client_reference_id
that can be accessed from a Stripe Session. Read more about it here.
We can add endpoint only when we deploy our app that is publicly accessible and using https
. We would add some more codes we want to run after at the part in create_checkout_sessions
that has the comment # TODO: run some custom code here
.
The API keys have been hardcoded for this work. But in production we would want to store those variables in environment. Event domain url would be stored in an environment variable like domain_url
.
Another thing is that I have used a separate settings file called user_settings.py
in djangostripe/
to hard code these secret keys and client ids. I have import this file somewhere at the bottom in djangostripe/settings.py
.
Thanks to TestDrive.io for a great tutorial. Discuss ideas at here. Report issues at here.