Skip to main content

Payment and Subscription


The payment feature is built on Stripe, which is the most popular solution for now.

Project Structure

If you check django_app directory.

├── demo_stripe
├── payment


  1. demo_stripe contains demo code, which show you how to integrate dj-stripe
  2. payment is the Django app which used exclusively by SaaS Hammer live site, you can ignore and remove it from your project.


# stripe
  1. The above ENV are used by dj-stripe,
  2. In new version of the dj-stripe, please store the STRIPE_TEST_SECRET_KEY and STRIPE_LIVE_SECRET_KEY in the Django admin (Stripe API Key), or you will get warning You don't have any API Keys in the database. Did you forget to add them? and some features might not work as expected.

dj-stripe Intro

dj-stripe is a Django package which can let you work with Django ORM instead of calling Stripe API directly.

Example 1

Create Stripe Customer from Django User.

You can create Customer using stripe API.

response = stripe.Customer.create(,

Or you can do it via dj-stripe

from djstripe.models import Customer



  1. The dj-stripe will help send request to Stripe API to create Customer, and also create Customer instance in the database.

Example 2

from djstripe.settings import djstripe_settings

class PaymentsContextMixin:

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context


  1. dj-stripe can help us expose stripe public key according to the STRIPE_LIVE_MODE
  2. So we can switch between stripe test mode and stripe live mode by just changing STRIPE_LIVE_MODE env variable.


This section will show you how to config Stripe and make it run.

To avoid confusion, please delete stripe testing data first, you can do it on stripe developers page in test mode.

# install stripe CLI

$ stripe login
$ stripe listen --forward-to

> Ready! You are using Stripe API Version [2023-08-16]. Your webhook signing secret is XXXXXX

The above command will redirect stripe webhook events to local dev server, I will talk about it with more details in a bit.

And then, add Stripe relevant env variable in the env file, you can get DJSTRIPE_WEBHOOK_SECRET from the stripe listen --forward-to command terminal output.

STRIPE_TEST_SECRET_KEY and STRIPE_LIVE_SECRET_KEY should be added in the Django admin (Stripe API Key).

# stripe
# create dj-stripe tables
$ python migrate

# pull data to from to local database
$ python djstripe_sync_models

Since we already deleted Stripe testing data, the djstripe_sync_models will not create any new records.

Let's run command to create Product, Price in Stripe test mode.

$ python bootstrap_demo_products

If you check Products page on Stripe dashboard, then you would be able to see them.

Basic plan

Starter plan

Pro plan
# sync data from to local database
$ python djstripe_sync_models
$ python runserver
# check on

If you can see the product data, then it means the demo_stripe is setup successfully.

Manual Test

Before the test, please make sure the stripe listen --forward-to command is running, or some features might not work as expected.

Please test Stripe Checkout Session on

Please test Subscription on

Stripe Subscription

Webhook Notification

A webhook enables Stripe to push real-time notifications to your app. Stripe uses HTTPS to send these notifications to your app as a JSON payload. You can then use these notifications to execute actions in your backend systems.

There are mainly two ways to handle Stripe webhook notification.

Solution 1: dj-stripe

It is recommended to write webhook handler with dj-stripe's @webhooks.handler

import logging
from djstripe import webhooks

LOGGER = logging.getLogger(__name__)

def checkout_complete_webhook_handler(event):
We can confirm payment in the webhook handler
session =["object"]
customer_details = session["customer_details"]

You can check django_app/demo_stripe/ for more details.

Solution 2

Or you can write webhook handler with Stripe API directly.

def webhook_view(request):
endpoint_secret = settings.DJSTRIPE_WEBHOOK_SECRET
payload = request.body
sig_header = request.META["HTTP_STRIPE_SIGNATURE"]

event = stripe.Webhook.construct_event(
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":

return HttpResponse(status=200)



To save developers time, below are some stimulus controllers which can be used directly.

  1. For Stripe Checkout Session, please check frontend/src/lazy_controllers/checkout_controller.js
  2. For Stripe Payment element, please check frontend/src/lazy_controllers/payment_controller.js
  3. For Price Toggle, please check frontend/src/lazy_controllers/pricing_controller.js


To make Stripe JS SDK works well with hotwired/turbo lib (avoid Failed to execute 'postMessage' on 'DOMWindow' error), there are two solutions

  1. Force reload page on certain pages with <meta name="turbo-visit-control" content="reload">
  2. Persist initial stripe iFrame DOM Object across turbo AJAX page requests, please check

In SaaS Hammer, we use the latter solution and it seems working well.


We can let dj-stripe to process below Stripe events:

  1. customer.subscription.created
  2. customer.subscription.updated
  3. customer.subscription.deleted

Once dj-stripe receive the event, it will automatically sync relevant data to database.

class SubscriptionMixin(PaymentsContextMixin):
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
context["customer"], _created = Customer.get_or_create(
context["subscription"] = context["customer"].subscription
return context

The benefit of this approach is that we can access subscription object without touching Stripe API.

And we can still use the Django ORM syntax to get the data we want.

Customer Portal

To let user manage Subscription and payment method, we can integrate the customer portal

In the template, we can display a Manage Billing button to let user generate portal_session and redirect to Stripe portal.

portal_session = stripe.billing_portal.Session.create(



We can use ValidSubscriptionRequiredMixin

class SubscriptionPermissionCheckView(
LoginRequiredMixin, ValidSubscriptionRequiredMixin, TemplateView
limit_to_products = [PRO_PRODUCT, STARTER_PRODUCT]
redirect_url = reverse_lazy("demo-stripe:subscription_payment_intent_lander")

template_name = "demo_stripe/subscription_permission_check.html"

subscription_permission_check_view = SubscriptionPermissionCheckView.as_view()


Another solution is to use the classic Django Group.

For example, if user subscribe to Pro plan, we can add the user to Pro group.

And then check permission using user.has_perm()