Bonaventure OgetoBy Bonaventure Ogeto|

Build Paystack Subscriptions in Django

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.

Subscription Model

# 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

Create a Plan

# 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.

Subscribe a Customer

# 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.

Handle Subscription Webhooks

# 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())

Gate Content Behind Subscription

# 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')

Cancel a Subscription

@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')

Ship Payments Faster

Key Takeaways

  • Paystack handles recurring billing. You create plans, subscribe customers, and Paystack charges them on schedule.
  • Create a Subscription model to track subscription status, plan, and the Paystack subscription_code.
  • The first payment creates the subscription. No separate API call is needed.
  • Webhook events drive subscription state changes. Handle subscription.create, charge.success, invoice.payment_failed, and subscription.disable.
  • Store the email_token from the subscription.create webhook. You need it to cancel subscriptions via the API.
  • Use Django middleware or decorators to gate premium content behind an active subscription check.

Frequently Asked Questions

Can I offer monthly and yearly plans?
Yes. Create separate plans with different intervals (monthly vs annually) and amounts. Let the customer choose on your pricing page and pass the corresponding plan_code when initializing.
How do I handle plan upgrades?
Paystack does not support direct plan changes. Cancel the current subscription and create a new one on the new plan. Handle prorating in your Django application logic.
What happens when a renewal payment fails?
Paystack retries the charge. You receive invoice.payment_failed for each failed attempt. Set the subscription status to past_due and notify the customer to update their payment method.
How do I store the email_token?
The email_token is included in the subscription.create webhook event. Save it in your Subscription model when you receive that webhook. You need it later to cancel the subscription via the API.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon