To verify a Paystack payment in Django, use the requests library to GET https://api.paystack.co/transaction/verify/:reference with your secret key. Check that data.data.status is "success", the amount matches your Order model, and the currency is correct. Use update() with a filter on paid=False for idempotent fulfillment.
# payments/utils.py
import requests
from django.conf import settings
def verify_paystack_transaction(reference):
"""Call Paystack verify endpoint and return the result."""
headers = {
'Authorization': 'Bearer ' + settings.PAYSTACK_SECRET_KEY,
}
response = requests.get(
'https://api.paystack.co/transaction/verify/' + reference,
headers=headers,
)
return response.json()
This function returns the raw Paystack response. Your views and webhook handlers will use it and apply their own business logic on top.
# payments/utils.py (continued)
from django.utils import timezone
from .models import Order
def verify_and_fulfill(reference):
"""Verify payment and fulfill the order idempotently."""
# 1. Look up the order
try:
order = Order.objects.get(reference=reference)
except Order.DoesNotExist:
return {'error': 'Order not found'}
if order.paid:
return {'success': True, 'already_fulfilled': True}
# 2. Verify with Paystack
result = verify_paystack_transaction(reference)
if not result.get('status'):
return {'error': 'Verification request failed'}
data = result['data']
if data['status'] != 'success':
return {'error': 'Payment not successful', 'paystack_status': data['status']}
# 3. Check amount
if data['amount'] != order.amount:
return {'error': 'Amount mismatch: expected ' + str(order.amount) + ' got ' + str(data['amount'])}
# 4. Check currency
if data['currency'] != order.currency:
return {'error': 'Currency mismatch'}
# 5. Idempotent fulfillment using update() with filter
updated = Order.objects.filter(
reference=reference,
paid=False,
).update(paid=True, paid_at=timezone.now())
if updated == 0:
return {'success': True, 'already_fulfilled': True}
# 6. Deliver value
# send_confirmation_email(order.email, order)
return {'success': True, 'already_fulfilled': False}
The Order.objects.filter(reference=reference, paid=False).update() pattern is atomic in Django. If two requests arrive simultaneously (webhook and callback), only one of them will update the row. The other gets updated == 0 and skips fulfillment.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
# payments/views.py
from django.shortcuts import render
from .utils import verify_and_fulfill
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'})
result = verify_and_fulfill(reference)
if result.get('success'):
return render(request, 'payments/success.html', {'reference': reference})
return render(request, 'payments/failed.html', {'error': result.get('error')})
The same verify_and_fulfill function should be called from your webhook handler. This ensures consistent verification logic regardless of which notification path fires first.
# payments/utils.py
import time
def verify_with_retry(reference, max_retries=3):
"""Verify with exponential backoff on failure."""
for attempt in range(max_retries):
try:
result = verify_paystack_transaction(reference)
if result.get('status') is not None:
return result
except requests.exceptions.RequestException as e:
print('Verify attempt ' + str(attempt + 1) + ' failed: ' + str(e))
if attempt < max_retries - 1:
time.sleep(2 ** attempt) # 1s, 2s, 4s
return None
If all retries fail, log the reference for manual investigation. The webhook will independently deliver the event, so the order can still be fulfilled through that path.
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