Getting Started
How to Install
1. Download and install the package with pip
Get Django Paddle Subscriptions from Gumroad.
Create a directory private_wheels/
in your project's repository and add the wheel file of the app there.
Link to this file in your requirements.txt
:
Django==4.2
file:./private_wheels/django_paddle_subscriptions-2.3.3-py2.py3-none-any.whl
Install the pip requirements from the requirements.txt
file into your project's virtual environment:
(venv)$ pip install -r requirements.txt
Alternatively to start quickly, install the wheel file into your Django project's virtual environment right from the shell:
(venv)$ pip install /path/to/django_paddle_subscriptions-2.3.3-py2.py3-none-any.whl
2. Add the app to INSTALLED_APPS
of your project settings
INSTALLED_APPS = [
# ...
"paddle_subscriptions",
]
3. Add SubscriptionMiddleware
to the MIDDLEWARE
setting
MIDDLEWARE = [
# ...
"paddle_subscriptions.middleware.SubscriptionMiddleware",
]
It will allow to access the current subscriber's data at request.subscriber
.
4. Add PADDLE_SUBSCRIPTIONS
settings
PADDLE_SUBSCRIPTIONS = {
"API_KEY": "...",
"PUBLIC_KEY": "...",
"CLIENT_SIDE_TOKEN": "...",
"ENVIRONMENT": "sandbox",
"WEBSITE_URL": "https://example.com",
# ...
}
Important note! Don't commit the keys and tokens to the version control for security reasons!
Set the setting PADDLE_SUBSCRIPTIONS['RESTRICTED_TO_IPS'] = ['...']
to show pricing or other subscription-related widgets only to visitors from those IPs.
5. Add settings for PayPal
If you use PayPal as one of the alowed payment gateways, add these settings:
MIDDLEWARE = [
# ...
"django.middleware.security.SecurityMiddleware",
]
SECURE_CROSS_ORIGIN_OPENER_POLICY = "unsafe-none"
6. Add paths to urlpatterns
from django.urls import path, include
urlpatterns = [
# ...
# Common views, such as webhook
path("paddle/", include("paddle_subscriptions.urls")),
# Subscription-related views
path("subscriptions/", include("paddle_subscriptions.urls.subscriptions")),
# Non-recurring-purchase-related views
path("purchases/", include("paddle_subscriptions.urls.purchases")),
]
7. Create Paddle data
In your Django project, create subscription plans with benefits for pricing widgets. If you have a free plan, set the call-to-action URL name at PADDLE_SUBSCRIPTIONS['FREE_PLAN_CTA_URL']
, for example, the sign up or waitlist form.
For subscriptions, create Products at Paddle for each paid subscription plan, and monthly and yearly prices for each product. You can skip monthly or yearly pricing if you want, but then you will need to modify the overwritten templates to exclude them there too.
For other digital purchases, create Products at Paddle Billing for each digital good, and a price for each product. Later you will be able to group your products under product categories at your Django website.
Deploy the data to a publicly accessible staging or production website. Paddle will send events to that website.
8. Link Paddle with your website
Run the management command to fetch Paddle Billing data and install the webhook:
(venv)$ python manage.py set_up_paddle_subscriptions
The webhook at https://example.com/subscriptions/webhook/
will be registered at Paddle and all Paddle events available in the API will be sent to it.
Then, link your subscription plans with sandbox monthly and yearly prices.
If you have digital products, you can create nullable foreign-key relations from your models to Product model for each type and group the Product models into product categories. For example, Books can have ISBN and downloadable files, desktop apps can have downloadable files and license numbers, user credit can have balance and credit transaction items.
It's recommended to make them nullable, because while going live, you'll need to re-link those models with the new Product instances.
Also set the default payment link at Paddle (Paddle ➔ Checkout ➔ Checkout settings) to https://example.com/paddle/payments/
. It will redirect to the correct location based on your SaaS project if you have more than one with the same Paddle account.
9. Create a pricing page and/or purchasable product list
Add the following template tag for subscription-plan pricing either on the start page or on a separate pricing page view:
{% load paddle_subscriptions_tags %}
{% paddle_subscriptions_pricing %}
Add the following template tag to list out products under a specific category identified by a slug:
{% load paddle_subscriptions_tags %}
{% paddle_subscriptions_category_products "licenses" %}
10. Copy the templates to your project
Copy the templates from paddle_subscriptions/templates
in site-packages to your project and adjust them as necessary.
The copy of the templates serves two purposes: you can collect translatable strings into your project, and you can make the TailwindCSS classes discoverable by your installation.
11. Update your signup and account deletion views
Your Signup view must link to the subscription that has been done before signup or create a new subscriber with a free plan if they haven't subscribed yet:
from paddle_subscriptions.services import (
get_validated_unlinked_transaction,
connect_new_user_to_subscription,
create_subscriber_for_free_plan,
get_initial_data_for_new_user,
)
@transaction.atomic
@never_cache
def register(request, *arguments, **keywords):
"""
Displays the registration form and handles the registration action
"""
m = hashlib.md5()
m.update(force_bytes(request.META["REMOTE_ADDR"]))
request.session.session_id = m.hexdigest()[:20]
transaction = None
if transaction_id := request.COOKIES.get("paddle_transaction_id"):
transaction = get_validated_unlinked_transaction(transaction_id)
if request.method == "POST":
form = RegistrationForm(request, request.POST)
if form.is_valid():
user = form.save()
if transaction:
connect_new_user_to_subscription(user, transaction, subscriber_name=form.cleaned_data["company_name"])
else:
create_subscriber_for_free_plan(user, subscriber_name=form.cleaned_data["company_name"])
response = redirect("accounts:register_pending")
if transaction:
response.delete_cookie("paddle_transaction_id")
return response
else:
initial = None
if transaction:
initial = get_initial_data_for_new_user(transaction)
initial["company_name"] = transaction.business.name if transaction.business else ""
form = RegistrationForm(request, initial=initial)
if transaction:
form.fields["email"].widget.attrs["readonly"] = True
request.session.set_test_cookie()
response = render(
request,
"accounts/signup.html",
{"form": form},
)
return response
Then your account deletion view must cancel the existing subscription:
from paddle_subscriptions.services import cancel_subscription
@login_required
def delete_account(request):
context = {}
if (
request.user.is_authenticated
and not request.user.is_staff
and request.method == "POST"
):
form = DeleteAccountForm(user=request.user, data=request.POST)
if form.is_valid():
user = request.user
if subscription := request.subscriber.current_subscription:
cancel_subscription(subscription, effective_from="immediately")
if request.subscriber.membership_set.count() == 1:
request.subscriber.delete()
auth_logout(request)
form.delete()
response = redirect("accounts:delete_account_complete")
return response
else:
form = DeleteAccountForm(user=request.user)
context["form"] = form
return render(request, "accounts/delete_account.html", context)
12. Handle the purchases with the webhook_triggered
signal
When selling digital goods, make sure to update user's access to the purchased products at the
webhook_triggered
signal handler. Some examples:
- Link a purchased ebook to a user.
- Link a purchased app or license number to a user.
- Update user credit balanse based on the bought credits.
Here is an example with credits:
myproject/apps/credits/apps.py
from django.apps import AppConfig
class CreditsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "myproject.apps.credits"
def ready(self):
from paddle_subscriptions.signals import webhook_triggered
from .signal_handlers import update_credits
webhook_triggered.connect(update_credits)
myproject/apps/credits/signal_handlers.py
def update_credits(sender, **kwargs):
"""
Add credits that the user bought to their credit balance.
"""
from django.conf import settings
from django.db import transaction as db_transaction
from myproject.apps.accounts.models import User
from myproject.apps.credits.models import Credit, CreditTransaction
from paddle_subscriptions.models import Transaction
api_event = kwargs.get("api_event")
if api_event.event_type != "checkout.completed":
return
transaction_id = api_event.data["transaction_id"]
if not (transaction := Transaction.objects.filter(pk=transaction_id).first()):
return
transaction_item = transaction.transactionitem_set.first()
if transaction_item.price.billing_cycle_interval or not (
user := User.objects.filter(pk=transaction.custom_data.get("user_id")).first()
):
return
if not (
credits_to_add := settings.PADDLE_PRODUCT_PRICE_CREDITS.get(
transaction_item.price.name
)
):
return
credits_to_add = credits_to_add * transaction_item.quantity
credit, _created = Credit.objects.get_or_create(user=user)
with db_transaction.atomic():
credit_transaction = CreditTransaction.objects.create(
user=user,
transaction_type=CreditTransaction.TransactionType.BOUGHT,
credits=credits_to_add,
)
credit.balance += credit_transaction.credits
credit.save()
How to Use
1. The subscription details page
The page at https://example.com/subscriptions/
allows you to subscribe to a paid plan or view the details of your current subscription.
Use the {% url "paddle_subscriptions:subscription_details" %}
in the templates to link to that page.
Test the pausing, resuming, and cancelling subscriptions there. Also test the billing history and invoices.
2. Check subscriber status in views or templates
These are some common values that might be interesting in your views and templates about the current subscriber:
# The slug of the current subscriber's subscription plan
request.subscriber.plan.slug
# Does the current subscriber have free access to their subscription plan?
# (the plan itself is free or the subscriber has an exclusive manually set free access)
request.subscriber.has_free_access
# Is the current subscription active?
request.subscriber.is_subscription_active
# Will the current subscription be paused at the end of the billing cycle?
request.subscriber.is_subscription_to_be_paused
# Is the current subscription paused at the moment?
request.subscriber.is_subscription_paused
# Will the current subscription be cancelled at the end of the billing cycle?
request.subscriber.is_subscription_to_be_cancelled
# Is the current subscription cancelled at the moment?
# (only possible for pricing without a free plan)
request.subscriber.is_subscription_cancelled
3. Translate the strings in your templates
If your website has more than one language, prepare the translations:
- Use management command
makemessages
to collect translatable strings intodjango.po
files. - Translate the strings to the languages you need.
- Then, use the management command
compilemessages
to compile them todjango.mo
files.
When going live
Paddle has an extensive list of steps about going live. Read it thoroughly. What relates to Django Paddle Subscriptions follows:
1. Set Paddle environment to live
Set the environment to "live":
PADDLE_SUBSCRIPTIONS["ENVIRONMENT"] = "live"
Make sure that PADDLE_SUBSCRIPTIONS["WEBSITE_URL"]
points to the URL of the production website, which has to be approved by Paddle.
Remove the PADDLE_SUBSCRIPTIONS["RESTRICTED_TO_IPS"]
setting.
2. Flush staging data and set up Paddle subscription anew
(venv)$ python manage.py flush_paddle_billing_models
(venv)$ python manage.py set_up_paddle_subscriptions
Then, link your subscription plans with live monthly and yearly prices. Also link your specific product models with the Product model and attach the categories for them.
3. Test live payments and subscriptions
Create a 100% discount code on the Paddle live environment and test your subscriptions with that code.