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']
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
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'})
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.
Learn Paystack Integration
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