Bonaventure OgetoBy Bonaventure Ogeto|

Accept Payments with Paystack in Django

To accept Paystack payments in Django, create a view that initializes a transaction with the Paystack API using the requests library and your secret key. Return the authorization URL to your template, redirect the user to Paystack checkout, then verify the payment in a callback view before granting value.

Project Setup

Install the requests library if you have not already:

pip install requests python-decouple

Add your Paystack keys to a .env file:

PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx
PAYSTACK_PUBLIC_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxx

Add the keys to your Django settings:

# settings.py
from decouple import config

PAYSTACK_SECRET_KEY = config('PAYSTACK_SECRET_KEY')
PAYSTACK_PUBLIC_KEY = config('PAYSTACK_PUBLIC_KEY')

Create an Order Model

# payments/models.py
from django.db import models
import uuid

class Order(models.Model):
    reference = models.CharField(max_length=100, unique=True)
    email = models.EmailField()
    amount = models.IntegerField(help_text='Amount in kobo')
    currency = models.CharField(max_length=3, default='NGN')
    paid = models.BooleanField(default=False)
    paid_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def save(self, *args, **kwargs):
        if not self.reference:
            self.reference = 'order_' + str(uuid.uuid4().hex[:12])
        super().save(*args, **kwargs)

    def __str__(self):
        return self.reference

Run migrations after creating the model:

python manage.py makemigrations payments
python manage.py migrate

Initialize a Transaction

# payments/views.py
import requests
from django.conf import settings
from django.shortcuts import redirect, render
from django.http import JsonResponse
from .models import Order

def initialize_payment(request):
    if request.method == 'POST':
        email = request.POST.get('email')
        amount = int(request.POST.get('amount', 5000))

        order = Order.objects.create(
            email=email,
            amount=amount * 100,  # Convert to kobo
        )

        headers = {
            'Authorization': 'Bearer ' + settings.PAYSTACK_SECRET_KEY,
            'Content-Type': 'application/json',
        }

        data = {
            'email': email,
            'amount': order.amount,
            'reference': order.reference,
            'callback_url': request.build_absolute_uri('/payments/callback/'),
        }

        response = requests.post(
            'https://api.paystack.co/transaction/initialize',
            json=data,
            headers=headers,
        )

        result = response.json()

        if result.get('status'):
            return redirect(result['data']['authorization_url'])

        return render(request, 'payments/checkout.html', {
            'error': result.get('message', 'Initialization failed'),
        })

    return render(request, 'payments/checkout.html')

Handle the Redirect Callback

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

def payment_callback(request):
    reference = request.GET.get('reference') or request.GET.get('trxref')

    if not reference:
        return render(request, 'payments/failed.html', {'error': 'Missing reference'})

    headers = {
        'Authorization': 'Bearer ' + settings.PAYSTACK_SECRET_KEY,
    }

    response = requests.get(
        'https://api.paystack.co/transaction/verify/' + reference,
        headers=headers,
    )

    result = response.json()

    if result.get('status') and result['data']['status'] == 'success':
        try:
            order = Order.objects.get(reference=reference)
        except Order.DoesNotExist:
            return render(request, 'payments/failed.html', {'error': 'Order not found'})

        # Check amount
        if result['data']['amount'] != order.amount:
            return render(request, 'payments/failed.html', {'error': 'Amount mismatch'})

        # Idempotent fulfillment
        if not order.paid:
            order.paid = True
            order.paid_at = timezone.now()
            order.save()

        return render(request, 'payments/success.html', {
            'reference': reference,
            'amount': order.amount / 100,
        })

    return render(request, 'payments/failed.html', {
        'error': 'Payment was not successful',
    })

URL Configuration

# payments/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('checkout/', views.initialize_payment, name='checkout'),
    path('callback/', views.payment_callback, name='payment_callback'),
]
# project/urls.py
from django.urls import path, include

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

Checkout Template

<!-- templates/payments/checkout.html -->
<form method="post" action="{% url 'checkout' %}">
  {% csrf_token %}
  <input type="email" name="email" placeholder="Email" required />
  <input type="hidden" name="amount" value="5000" />
  <p>Amount: NGN 5,000</p>
  <button type="submit">Pay Now</button>
  {% if error %}
    <p style="color: red;">{{ error }}</p>
  {% endif %}
</form>

The form posts to your Django view, which initializes the transaction and redirects to Paystack. The CSRF token is needed because this is a Django form submission, not a Paystack request.

Security and Production Checklist

  1. Validate amounts server-side. Look up the correct price from your database or product catalog. Never trust form data.
  2. Exempt webhooks from CSRF. Use @csrf_exempt on your webhook view. Paystack requests will not have Django CSRF tokens.
  3. Set up webhooks. The redirect can fail. See Handle Paystack Webhooks in Django.
  4. Use HTTPS. Required for callback and webhook URLs in production.
  5. Switch keys. Use sk_live_ in production. Keep test keys for development only.

Ship Payments Faster

Key Takeaways

  • Django views handle the Paystack API calls server-side. Use the requests library to call the Paystack REST API.
  • Store your Paystack secret key in environment variables or Django settings. Never hardcode it.
  • Amounts are in kobo (NGN), pesewas (GHS), or cents (ZAR/KES/USD). Multiply by 100 before sending to Paystack.
  • Exempt your callback and webhook views from CSRF protection since the requests come from Paystack, not your frontend.
  • Always verify the transaction server-side after the redirect. The redirect alone is not proof of payment.
  • Use Django models to store orders and their payment status. Check the status before fulfilling to prevent double-granting.

Frequently Asked Questions

Do I need a Paystack Python package?
No. The Paystack API is a standard REST API. The requests library handles all the HTTP calls. Community packages exist but add unnecessary abstraction for most projects.
Can I use class-based views instead of function-based views?
Yes. The Paystack API calls are the same. Use View or TemplateView and override get() and post() methods. The logic is identical.
How do I handle CSRF for Paystack callbacks?
The callback is a GET request (a redirect from Paystack), so CSRF does not apply. CSRF only affects POST requests. For webhooks (which are POST), use the @csrf_exempt decorator.
Can I use Django async views with Paystack?
Yes. Django 4.1+ supports async views. Use httpx instead of requests for async HTTP calls to the Paystack API. The API endpoints and logic are the same.

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