To build Paystack subscriptions in Django, create a plan via the Paystack API, initialize a transaction with the plan code, and handle subscription webhooks. Store subscription data in a Django model. Paystack handles all recurring billing automatically.
# payments/models.py
from django.db import models
from django.contrib.auth.models import User
class Subscription(models.Model):
STATUS_CHOICES = [
('active', 'Active'),
('past_due', 'Past Due'),
('cancelled', 'Cancelled'),
]
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='subscription')
subscription_code = models.CharField(max_length=100, unique=True)
customer_code = models.CharField(max_length=100)
plan_code = models.CharField(max_length=100)
email_token = models.CharField(max_length=100)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
next_payment_date = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
cancelled_at = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.subscription_code
# payments/paystack.py
import requests
from django.conf import settings
PAYSTACK_BASE = 'https://api.paystack.co'
def get_headers():
return {
'Authorization': 'Bearer ' + settings.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
}
def create_plan(name, amount, interval):
"""Create a Paystack plan. Interval: daily, weekly, monthly, quarterly, biannually, annually."""
response = requests.post(
PAYSTACK_BASE + '/plan',
json={
'name': name,
'amount': int(amount * 100),
'interval': interval,
'currency': 'NGN',
},
headers=get_headers(),
)
return response.json()
def disable_subscription(code, email_token):
"""Cancel a subscription."""
response = requests.post(
PAYSTACK_BASE + '/subscription/disable',
json={'code': code, 'token': email_token},
headers=get_headers(),
)
return response.json()
Create plans once through the dashboard or a management command. Store the plan codes in your settings or database.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
# payments/views.py
import requests as http_requests
from django.conf import settings
from django.shortcuts import redirect
from django.contrib.auth.decorators import login_required
@login_required
def subscribe(request):
if request.method == 'POST':
plan_code = request.POST.get('plan_code')
response = http_requests.post(
'https://api.paystack.co/transaction/initialize',
json={
'email': request.user.email,
'plan': plan_code,
'callback_url': request.build_absolute_uri('/payments/subscription/callback/'),
},
headers={
'Authorization': 'Bearer ' + settings.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
)
result = response.json()
if result.get('status'):
return redirect(result['data']['authorization_url'])
return render(request, 'payments/subscribe.html')
When you pass a plan parameter to the initialize endpoint, you do not need to pass amount. Paystack uses the plan's configured amount.
# payments/webhooks.py
from django.utils import timezone
from django.contrib.auth.models import User
from .models import Subscription
def handle_subscription_create(data):
email = data['customer']['email']
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return
Subscription.objects.update_or_create(
user=user,
defaults={
'subscription_code': data['subscription_code'],
'customer_code': data['customer']['customer_code'],
'plan_code': data['plan']['plan_code'],
'email_token': data.get('email_token', ''),
'status': 'active',
'next_payment_date': data.get('next_payment_date'),
},
)
def handle_invoice_failed(data):
sub_code = data.get('subscription', {}).get('subscription_code')
if sub_code:
Subscription.objects.filter(
subscription_code=sub_code,
).update(status='past_due')
def handle_subscription_disable(data):
sub_code = data.get('subscription_code')
if sub_code:
Subscription.objects.filter(
subscription_code=sub_code,
).update(status='cancelled', cancelled_at=timezone.now())
# payments/decorators.py
from functools import wraps
from django.shortcuts import redirect
def subscription_required(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect('/login/')
try:
sub = request.user.subscription
if sub.status == 'active':
return view_func(request, *args, **kwargs)
except Exception:
pass
return redirect('/payments/subscribe/')
return wrapper
Use it on any view:
@subscription_required
def premium_dashboard(request):
return render(request, 'premium/dashboard.html')
@login_required
def cancel_subscription(request):
if request.method == 'POST':
try:
sub = request.user.subscription
except Subscription.DoesNotExist:
return redirect('/dashboard/')
result = disable_subscription(sub.subscription_code, sub.email_token)
if result.get('status'):
sub.status = 'cancelled'
sub.cancelled_at = timezone.now()
sub.save()
return redirect('/dashboard/')
return render(request, 'payments/cancel.html')
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course