Bonaventure OgetoBy Bonaventure Ogeto|

Accept Payments with Paystack in Django REST Framework

To accept Paystack payments in DRF, create an APIView that receives email and amount via a serializer, initializes a transaction with the Paystack API, and returns the authorization URL as JSON. Your SPA or mobile frontend redirects to Paystack or opens the inline popup. A separate APIView verifies the payment.

Setup

Install DRF and requests if not already installed:

pip install djangorestframework requests python-decouple

Add 'rest_framework' to INSTALLED_APPS in settings.py. Configure your Paystack keys as shown in the Django guide.

Payment Serializer

# payments/serializers.py
from rest_framework import serializers

class InitializePaymentSerializer(serializers.Serializer):
    email = serializers.EmailField()
    amount = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=1)

class VerifyPaymentSerializer(serializers.Serializer):
    reference = serializers.CharField(max_length=100)

Initialize Transaction APIView

# payments/views.py
import uuid
import requests as http_requests
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import InitializePaymentSerializer
from .models import Order

class InitializePaymentView(APIView):
    def post(self, request):
        serializer = InitializePaymentSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        email = serializer.validated_data['email']
        amount = serializer.validated_data['amount']
        reference = 'order_' + uuid.uuid4().hex[:12]

        # Save order to database
        order = Order.objects.create(
            email=email,
            amount=int(amount * 100),
            reference=reference,
        )

        # Call Paystack
        response = http_requests.post(
            'https://api.paystack.co/transaction/initialize',
            json={
                'email': email,
                'amount': order.amount,
                'reference': reference,
                'callback_url': request.build_absolute_uri('/api/payments/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', 'Initialization failed')},
                status=status.HTTP_400_BAD_REQUEST,
            )

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

Verify Payment APIView

# payments/views.py (continued)
from django.utils import timezone

class VerifyPaymentView(APIView):
    def get(self, request):
        reference = request.query_params.get('reference')

        if not reference:
            return Response(
                {'error': 'Missing reference'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # Call Paystack verify
        response = http_requests.get(
            'https://api.paystack.co/transaction/verify/' + reference,
            headers={
                'Authorization': 'Bearer ' + settings.PAYSTACK_SECRET_KEY,
            },
        )

        result = response.json()

        if not result.get('status') or result['data']['status'] != 'success':
            return Response(
                {'error': 'Payment not successful'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # Look up and validate order
        try:
            order = Order.objects.get(reference=reference)
        except Order.DoesNotExist:
            return Response(
                {'error': 'Order not found'},
                status=status.HTTP_404_NOT_FOUND,
            )

        if result['data']['amount'] != order.amount:
            return Response(
                {'error': 'Amount mismatch'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # Idempotent fulfillment
        updated = Order.objects.filter(
            reference=reference, paid=False
        ).update(paid=True, paid_at=timezone.now())

        return Response({
            'success': True,
            'already_fulfilled': updated == 0,
            'amount': order.amount / 100,
            'currency': order.currency,
        })

URL Configuration

# payments/urls.py
from django.urls import path
from .views import InitializePaymentView, VerifyPaymentView

urlpatterns = [
    path('initialize/', InitializePaymentView.as_view(), name='initialize_payment'),
    path('verify/', VerifyPaymentView.as_view(), name='verify_payment'),
]
# project/urls.py
from django.urls import path, include

urlpatterns = [
    path('api/payments/', include('payments.urls')),
]

Frontend Integration

Your React, Vue, or mobile frontend calls the API:

// Frontend (React/Vue/etc.)
async function initializePayment(email, amount) {
  var res = await fetch('/api/payments/initialize/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Token ' + authToken,
    },
    body: JSON.stringify({ email: email, amount: amount }),
  });

  var data = await res.json();

  if (data.authorization_url) {
    // Redirect checkout
    window.location.href = data.authorization_url;
  }
}

For inline popup, use the access_code with Paystack Inline.js. For redirect checkout, use the authorization_url.

Security Notes

  1. Authentication. Add authentication to your initialize endpoint so only logged-in users can start payments.
  2. Amount validation. Look up the correct price from your product catalog. Never trust the client-submitted amount.
  3. Webhooks. Set up a webhook endpoint for reliable payment notification. See Handle Paystack Webhooks in DRF.
  4. CORS. Configure django-cors-headers to allow requests from your frontend domain.

Ship Payments Faster

Key Takeaways

  • DRF APIViews return JSON responses, making them ideal backends for React, Vue, Angular, or mobile apps using Paystack.
  • Use serializers to validate payment input (email, amount) before calling the Paystack API.
  • The Paystack secret key stays in your Django settings. The API returns only the authorization URL and access code to the client.
  • Set authentication_classes and permission_classes appropriately. Initialize endpoints usually require authentication. Webhook endpoints require none.
  • Amounts are in kobo (NGN), pesewas (GHS), or cents (ZAR/KES/USD). Validate and convert on the server.
  • For inline popup checkout, return the access_code. For redirect checkout, return the authorization_url.

Frequently Asked Questions

Should I use function-based views or class-based APIViews?
APIViews are the DRF convention and give you built-in serializer validation, content negotiation, and authentication handling. Use them for consistency with the rest of your DRF API.
How do I add authentication to the payment endpoint?
Set authentication_classes = [TokenAuthentication] and permission_classes = [IsAuthenticated] on your APIView. DRF handles the rest. You can then use request.user to get the customer email.
Can I use ViewSets and Routers for Paystack endpoints?
ViewSets work best for CRUD operations on models. Payment initialization and verification are action-oriented, not model CRUD, so APIViews are a better fit.
How do I handle CORS with DRF and Paystack?
Install django-cors-headers and add your frontend domain to CORS_ALLOWED_ORIGINS. This has nothing to do with Paystack directly. It allows your SPA to call your DRF backend.

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