Bonaventure OgetoBy Bonaventure Ogeto|

Build Paystack Subscriptions in Django REST Framework

To build Paystack subscriptions in DRF, create API endpoints for subscribing customers (passing a plan_code to the initialize endpoint), handle subscription webhooks (subscription.create, charge.success, invoice.payment_failed), track status in a Subscription model, and provide endpoints for viewing and cancelling subscriptions.

Subscribe Endpoint

# payments/views.py
class SubscribeView(APIView):
    def post(self, request):
        plan_code = request.data.get('plan_code')
        email = request.user.email

        response = requests.post(
            'https://api.paystack.co/transaction/initialize',
            json={
                'email': email,
                'plan': plan_code,
                'callback_url': settings.FRONTEND_URL + '/subscription/callback',
            },
            headers={
                'Authorization': 'Bearer ' + settings.PAYSTACK_SECRET_KEY,
                'Content-Type': 'application/json',
            },
        )

        result = response.json()

        if not result.get('status'):
            return Response({'error': result.get('message')}, status=400)

        return Response({
            'authorization_url': result['data']['authorization_url'],
            'reference': result['data']['reference'],
        })

Subscription Model and Serializer

# payments/models.py
class Subscription(models.Model):
    user = models.OneToOneField('auth.User', on_delete=models.CASCADE)
    subscription_code = models.CharField(max_length=100, unique=True)
    plan_code = models.CharField(max_length=100)
    email_token = models.CharField(max_length=100)
    status = models.CharField(max_length=20, default='active')
    next_payment_date = models.DateTimeField(null=True)
    created_at = models.DateTimeField(auto_now_add=True)

# payments/serializers.py
class SubscriptionSerializer(serializers.ModelSerializer):
    class Meta:
        model = Subscription
        fields = ['subscription_code', 'plan_code', 'status', 'next_payment_date', 'created_at']

Subscription Webhook Handlers

# payments/services.py
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'],
            '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')

Status and Cancellation Endpoints

# payments/views.py
class SubscriptionStatusView(APIView):
    def get(self, request):
        try:
            sub = request.user.subscription
        except Subscription.DoesNotExist:
            return Response({'has_subscription': False})

        serializer = SubscriptionSerializer(sub)
        return Response({'has_subscription': True, 'subscription': serializer.data})

class CancelSubscriptionView(APIView):
    def post(self, request):
        try:
            sub = request.user.subscription
        except Subscription.DoesNotExist:
            return Response({'error': 'No subscription found'}, status=404)

        response = requests.post(
            'https://api.paystack.co/subscription/disable',
            json={'code': sub.subscription_code, 'token': sub.email_token},
            headers={
                'Authorization': 'Bearer ' + settings.PAYSTACK_SECRET_KEY,
                'Content-Type': 'application/json',
            },
        )

        result = response.json()
        if result.get('status'):
            sub.status = 'cancelled'
            sub.save()
            return Response({'message': 'Subscription cancelled'})

        return Response({'error': result.get('message')}, status=400)

Subscription Permission Class

# payments/permissions.py
from rest_framework.permissions import BasePermission

class HasActiveSubscription(BasePermission):
    message = 'An active subscription is required.'

    def has_permission(self, request, view):
        if not request.user or not request.user.is_authenticated:
            return False
        try:
            return request.user.subscription.status == 'active'
        except Exception:
            return False

Use it on any APIView:

class PremiumContentView(APIView):
    permission_classes = [IsAuthenticated, HasActiveSubscription]

    def get(self, request):
        return Response({'content': 'Premium data here'})

Ship Payments Faster

Key Takeaways

  • DRF serializers validate subscription requests. Use them to ensure valid plan codes and email addresses.
  • The first payment creates the subscription. Pass the plan_code when initializing the transaction.
  • Track subscription status in a Django model. Update it via webhook events, not by polling Paystack.
  • Use DRF permissions to gate API endpoints behind active subscription checks.
  • Store the email_token from subscription.create webhooks. You need it for cancellation.
  • Webhook events are the source of truth for subscription state changes.

Frequently Asked Questions

Can I offer multiple subscription tiers?
Yes. Create separate plans in Paystack with different amounts and intervals. Store the plan_code in your app config and let the frontend pass the desired plan_code when subscribing.
How do I handle plan upgrades in DRF?
Cancel the current subscription via the disable endpoint, then create a new subscription on the new plan. Handle prorating in your application logic.
Should I check subscription status on every request?
Use the HasActiveSubscription permission class on protected endpoints. It checks your database, which is updated by webhooks. This is fast and does not require calling the Paystack API on every request.

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