Implementing Paddle Payments for my Django SaaS

Recent Posts Popular Posts


A couple of months ago, I published my SaaS side-project:
It is a Django web application to organize, learn, and practice keyboard shortcuts! You can read more about the concept in some of my other blog posts. This post describes how I implemented payment processing for KeyCombiner with Paddle.

Everybody loves Stripe. Their docs and tooling are excellent. However, they do not deal with the whole billing stack. Most of all, they do not handle taxes. Trying to figure out on your own which VAT rules apply for each of your customers and how to pay the respective sum to the correct authority is not feasible for a side-project.

Paddle tackles this problem head-on by acting as a Merchant of Record (MOR). If you buy a subscription for KeyCombiner, you will not buy it from me, but from Paddle.

The paddle documentation is quite good, but there is not that much third-party content going into more detail and providing step-by-step tutorials. This post describes, in great detail, one possible approach for accepting payments with Paddle in a Django app.


  • A Paddle account. It can be created in a matter of minutes.
  • A Django app that you want to monetize. This will probably take you a little longer.

Setting up your Plan in Paddle

KeyCombiner has a basic pricing model. There is only one Pro subscription plan. Before integrating such a plan into your Django web app, you need to create it. In your Paddle account, go to Catalog→Subscription Plans→New Plan, configure it according to your needs, and hit Save. We will later need the ID of the plan we just created. For this guide, I will use 487302. If you have an existing Paddle plan already, you don’t need to create a new one, just note its ID.

Installing and Configuring dj-paddle

As Django developers, we naturally shy away from implementing anything ourselves. There has to be an existing package for this, right?

Fortunately, there is. It is called dj-paddle, and I am grateful that Florian Pruchess put in the effort to create it. It is a relatively new project, though, and does not provide everything out of the box, so we will get to do a little bit of coding ourselves, too.

We will use dj-paddle for a couple of things:

  1. To provide the database models
  2. To provide service endpoints for Paddle’s webhooks
  3. To provide some convenience templates


Start by installing the package:

pip install dj-paddle

Then, add it to your INSTALLED_APPS :


If you have your settings split into production and development like a pro, I would recommend adding djpaddle to your base settings file, as we can do some local testing with it.

Then, add the following URL configuration to your

path("paddle/", include("djpaddle.urls", namespace="djpaddle"))

This is for the service endpoints that Paddle’s webhooks will contact whenever a subscription is created or modified. For this to work, we need to tell Paddle about our service URLs. In your Paddle account, head to Developer Tools→Alerts/Webhooks and look for the Receiving alerts section. There, you can enter your service URL that we just configured above. If you put the above line into your root []( it will be <base-url/paddle/webhook/>. It’s also a good idea to specify an email address for receiving alerts. You don’t want to miss those sweet notifications about users purchasing subscriptions.

Now it is time to add dj-paddle’s models to our database, run the migrations that come with the package:

python migrate

The final thing we have to do before we can implement the actual checkout process is synching our Paddle Plans with our database. Fortunately, dj-paddle provides a management command that does just that by contacting Paddle’s web services. However, before we have to set up credentials so that our app can communicate with Paddle:

# can be found at
DJPADDLE_VENDOR_ID = '<your-vendor-id>'

# create one at
DJPADDLE_API_KEY = '<your-api-key>'

# can be found at
DJPADDLE_PUBLIC_KEY = '<your-public-key>'

# ID of the plan we created before

Then, we are ready to run the management command:

python djpaddle_sync_plans_from_paddle

Implementing the Checkout

Now, we can get to work. To implement our checkout process, we first need a checkout page. If you want to see mine, you are very welcome to create an account on and check it out here. Spoiler: It is loosely based on this basic Bootstrap pricing page template.

We don’t need much, though. Start by including PaddleJS, which is conveniently provided by dj-paddle:

{% include "djpaddle_paddlejs.html" %}

And, of course, a button to trigger the checkout process:

<a href="#!" class="btn btn-primary paddle_button" data-theme="none"
    data-product="{{ }}"
    data-email="{{ }}">Purchase Subscription</a>

The corresponding class-based Django view looks like this:

class Checkout(TemplateView):
    template_name = 'checkout.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['paddle_plan'] = Plan.objects.get(pk=settings.PADDLE_PLAN_ID)
        # If you have not added 'djpaddle.context_processors.vendor_id' as a template context processors
        context['DJPADDLE_VENDOR_ID'] = settings.DJPADDLE_VENDOR_ID
        return context

The crazy thing is, that is already everything you need for a basic checkout process. If you pair this checkout process with a method in your User object, such as the following, you are ready to accept payments and restrict features for paying customers:

def has_subscription(self):
    return self.subscriptions.filter(status = 'active').exists()

Then, in your templates, do

{% if user.has_subscription %}
{% endif %}

to restrict features for users with active subscriptions.

Believe it or not, your customers can purchase subscriptions now, and Paddle will send you the money in recurring intervals.

Unfortunately, as is often the case in software engineering, the final 10% of usability is 90% of the work. To make this convenient to use for your customers, read the Polishing the Checkout section covering some frequent pain points with payment processing.

Testing the Checkout

The Paddle docs suggest a couple of different methods for testing your checkout process. In my experience, the easiest method is to create a 100% off coupon. To do this, go to Catalog->Coupons->+ New Coupon in the Paddle web interface.

Then, you can use the created coupon code to purchase a subscription without having to spend any money.

Testing the KeyCombiner checkout.

Testing the KeyCombiner checkout.

This way, you can step through the whole checkout process and, most importantly, see what happens after you successfully purchased a subscription. With your current setup, you will probably need to wait for a moment and refresh the page until user.has_subscription returns True.

Polishing the Checkout:

This section covers everything I did to make the KeyCombiner checkout experience more convenient. If you have more complex problems, have a look at dj-paddle’s documentation, the Paddle Client project, or, if things get really serious, Paddle’s API reference documentation.

What to do directly after checkout?

With our current setup, we will only be aware of a new subscription once the webhook is triggered and the subscription object is added to our database. This can take a few moments. Letting users wait for this time and hope that they refresh the page is not a good look. A user that just purchased a subscription has every right to feel like a Pro right away.

Fortunately, the creators of dj-paddle came up with a solution. There is a template that will add data to the Checkout model upon a successful completion of the checkout process. We only need to include it:

{% include "djpaddle_post_checkout.html" %}

We can also redirect the user to a specific page after checkout:

context['djpaddle_checkout_success_redirect'] = reverse('users:checkout')

To make use of the Checkout object that will now be in the database immediately after a successful purchase, we need to adapt our has_subscription method. In addition to an active subscription, a recently completed checkout is enough to be considered a Pro user on KeyCombiner:

def has_subscription(self):
    return self.subscriptions.filter(status='active').exists() or Checkout.objects.filter(completed=True,,

Let a User Manage their Subscription

Unfortunately, some users don’t know what is good for them. So we need to allow them to cancel their subscription. Also, they should be able to update their payment details so they can ensure that the next payment is handled properly.

I implemented this in the simplest form possible. By displaying Paddle’s update payment and cancellation URLs. Conveniently, these are present in dj-paddle’s subscription model. To pass the user’s subscription to the template, modify the class-based Checkout view:

def get_context_data(self, **kwargs):
    if self.request.user.has_subscription:
        active_subscription = self.request.user.subscriptions.get(status='active')
        context['subscription'] = active_subscription

If you implement a grace period as described below, you will need to update the code to retrieve the subscription from the database accordingly. Also make sure to show appropriate information to the user when they have no subscription but a recently completed checkout, as described above.

Then, you can access the subscription’s update_url and cancel_url in your template:

<a href="{{ subscription.update_url }}" class="btn btn-lg btn-primary">
    Update Payment Method
<a href="{{ subscription.cancel_url }}" class="btn btn-lg btn-danger">
    Cancel Subscription

These URLs link to simple views provided and hosted by Paddle that offer the respective functionality. We can only rely on this simple approach because we listen to the subscription-related webhooks that keep our database models updated with the correct URLs.

Handle Grace Period

After a user cancels their subscription, they should still have access to the service until the end of their subscription period. I would have thought that Paddle has some API for this, but it appears that there is none. So, I copied the approach from Laravel Cashier, which is a Paddle integration project for the Laravel PHP framework. We simply use the next billing date field that remains the same even after a subscription is canceled:

def has_subscription(self):
    return self.subscriptions.filter(
        Q(status='active') | Q(status='deleted', next_bill_date__gte=now)).exists()
            or Checkout.objects.filter(completed=True,,


Stripe is often seen as the de-facto standard for accepting SaaS payments. Their documentation is universally praised, and integration is as simple as it gets. However, this post shows that integrating Paddle into your Django app isn’t rocket science either.

The Paddle web interface lets you create coupon codes. I hope you feel inspired by this classic comic. (Source:

The Paddle web interface lets you create coupon codes. I hope you feel inspired by this classic comic. (Source:

If you want to see the described approach in practice, you are very welcome to create an account on KeyCombiner and purchase a Pro subscription ;)

comments powered by Disqus

  Blog Post Tags