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.
# 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'],
})
# 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.
# 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')
# 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)
# 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'})
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