Webhooks Guide
Understanding PayFast Webhooks
PayFast uses webhooks (also called ITN - Instant Transaction Notifications) to notify your application about payment events in real-time. This guide covers everything you need to know about implementing and handling webhooks effectively.
What are Webhooks?
Webhooks are HTTP callbacks that PayFast sends to your server when a payment event occurs. These events include:
Payment completion
Payment failure
Payment cancellation
Subscription updates
Refund notifications
Why Webhooks are Important
Webhooks are critical for your payment system because:
Real-time Updates: Get instant notification when payments complete
Reliability: Even if users close their browser, you still receive updates
Automation: Automatically grant access, send emails, update inventory
Reconciliation: Keep your database synchronized with PayFast
Security: Server-side validation ensures payment authenticity
How Webhooks Work
The webhook flow follows these steps:
1. User completes payment on PayFast
2. PayFast sends POST request to your webhook URL
3. Your server validates the request
4. Your server updates payment status
5. Your server responds with HTTP 200
6. PayFast marks notification as delivered
Important: You must respond with HTTP 200 within 10 seconds, or PayFast will retry.
Setting Up Webhooks
Step 1: Configure Webhook URL
Your webhook URL must be:
Publicly accessible (not localhost)
HTTPS (required for production)
POST-enabled (GET requests will fail)
Not behind authentication (PayFast can’t log in)
Example webhook URL:
https://yourdomain.com/payfast/notify/
For Local Development:
Use ngrok to expose your local server:
# Install ngrok
brew install ngrok # macOS
# or download from https://ngrok.com
# Start ngrok
ngrok http 8000
# Use the generated URL
# Example: https://abc123.ngrok.io/payfast/notify/
Step 2: Add URL to Django
The webhook URL is automatically configured when you include payfast URLs:
# urls.py
from django.urls import path, include
urlpatterns = [
path('payfast/', include('payfast.urls')),
]
This creates the endpoint: /payfast/notify/
Step 3: Configure in PayFast Form
Include the webhook URL when generating payment forms:
from django.urls import reverse
notify_url = request.build_absolute_uri(
reverse('payfast:notify')
)
form = PayFastPaymentForm(initial={
'amount': 99.99,
'item_name': 'Premium Plan',
'm_payment_id': payment_id,
'email_address': user.email,
'notify_url': notify_url, # Webhook URL
})
Webhook Security
dj-payfast implements multiple security layers to protect your webhooks.
1. IP Address Validation
Webhooks only come from PayFast servers:
Valid PayFast IPs:
www.payfast.co.zasandbox.payfast.co.zaw1w.payfast.co.zaw2w.payfast.co.za
The library validates these automatically:
from payfast.utils import validate_ip
ip_address = get_client_ip(request)
if not validate_ip(ip_address):
return HttpResponseForbidden('Invalid IP')
2. Signature Verification
Every webhook includes a cryptographic signature:
from payfast.utils import verify_signature
post_data = request.POST.dict()
if not verify_signature(post_data, PAYFAST_PASSPHRASE):
return HttpResponseBadRequest('Invalid signature')
How Signatures Work:
PayFast generates MD5 hash of payment data + passphrase
Signature is included in webhook POST data
Your server recalculates the signature
If signatures match, request is authentic
3. Server-Side Validation
The final security layer validates with PayFast servers:
import requests
from payfast.conf import PAYFAST_VALIDATE_URL
response = requests.post(
PAYFAST_VALIDATE_URL,
data=post_data,
timeout=10
)
if response.text != 'VALID':
return HttpResponseBadRequest('Validation failed')
Webhook Data Structure
PayFast sends the following data in webhook requests:
Standard Fields
{
# Payment Identifiers
'm_payment_id': 'your-unique-id', # Your payment ID
'pf_payment_id': '1234567', # PayFast's payment ID
# Payment Status
'payment_status': 'COMPLETE', # COMPLETE, FAILED, PENDING, CANCELLED
# Amount Details
'amount_gross': '100.00', # Gross amount
'amount_fee': '5.75', # PayFast fee
'amount_net': '94.25', # Net amount (what you receive)
# Item Information
'item_name': 'Premium Plan',
'item_description': '1 month subscription',
# Customer Details
'name_first': 'John',
'name_last': 'Doe',
'email_address': 'john@example.com',
# Security
'signature': 'abc123def456...', # MD5 signature
# Custom Fields (if provided)
'custom_str1': 'order_123',
'custom_int1': '5',
}
Payment Status Values
PayFast sends one of these statuses:
Status |
Description |
|---|---|
|
Payment successfully completed |
|
Payment failed (insufficient funds, etc.) |
|
Payment initiated but not yet complete |
|
User cancelled the payment |
Handling Webhooks
Built-in Webhook Handler
dj-payfast includes a complete webhook handler:
# payfast/views.py
from payfast.views import PayFastNotifyView
# Already included in payfast.urls
# Available at: /payfast/notify/
The built-in handler automatically:
✅ Validates IP address
✅ Verifies signature
✅ Validates with PayFast servers
✅ Updates payment record
✅ Logs notification
✅ Returns appropriate response
Custom Webhook Processing
To add custom logic, subclass the view:
# myapp/views.py
from payfast.views import PayFastNotifyView
from django.core.mail import send_mail
class CustomPayFastNotifyView(PayFastNotifyView):
def post(self, request, *args, **kwargs):
# Call parent processing first
response = super().post(request, *args, **kwargs)
# Only run custom logic if validation passed
if response.status_code == 200:
post_data = request.POST.dict()
payment_status = post_data.get('payment_status')
m_payment_id = post_data.get('m_payment_id')
# Your custom logic
if payment_status == 'COMPLETE':
self.handle_successful_payment(m_payment_id)
elif payment_status == 'FAILED':
self.handle_failed_payment(m_payment_id)
return response
def handle_successful_payment(self, payment_id):
"""Process successful payment"""
payment = PayFastPayment.objects.get(m_payment_id=payment_id)
# Grant access
if payment.user:
self.activate_subscription(payment.user)
# Send email
send_mail(
subject='Payment Confirmed!',
message=f'Your payment of R{payment.amount} was successful.',
from_email='noreply@example.com',
recipient_list=[payment.email_address],
)
# Update inventory
self.update_stock(payment)
def handle_failed_payment(self, payment_id):
"""Handle failed payment"""
payment = PayFastPayment.objects.get(m_payment_id=payment_id)
# Send notification
send_mail(
subject='Payment Failed',
message='Your payment was unsuccessful. Please try again.',
from_email='noreply@example.com',
recipient_list=[payment.email_address],
)
Register your custom view:
# myapp/urls.py
from django.urls import path
from .views import CustomPayFastNotifyView
urlpatterns = [
path('payfast/notify/', CustomPayFastNotifyView.as_view()),
]
Using Django Signals
Recommended approach for handling payment events:
# myapp/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from payfast.models import PayFastPayment
from django.core.mail import send_mail
@receiver(post_save, sender=PayFastPayment)
def handle_payment_update(sender, instance, created, **kwargs):
"""Handle payment status changes"""
# Only process completed payments
if instance.status == 'complete' and instance.payment_status == 'COMPLETE':
handle_successful_payment(instance)
def handle_successful_payment(payment):
"""Process completed payment"""
# 1. Grant access to user
if payment.user:
payment.user.profile.is_premium = True
payment.user.profile.save()
# 2. Send confirmation email
send_mail(
subject='Payment Confirmed',
message=f'''
Thank you for your payment of R{payment.amount}.
Transaction ID: {payment.m_payment_id}
Your premium access has been activated!
''',
from_email='noreply@example.com',
recipient_list=[payment.email_address],
)
# 3. Process custom fields
if payment.custom_str1:
process_order(payment.custom_str1)
# 4. Log the event
logger.info(f'Payment completed: {payment.m_payment_id}')
Register signals in apps.py:
# myapp/apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = 'myapp'
def ready(self):
import myapp.signals # Register signals
Webhook Debugging
Logging Webhooks
All webhook attempts are logged in PayFastNotification model:
from payfast.models import PayFastNotification
# Get all notifications
notifications = PayFastNotification.objects.all()
# Get failed notifications
failed = PayFastNotification.objects.filter(is_valid=False)
# Check errors
for notification in failed:
print(f"Error: {notification.validation_errors}")
print(f"Data: {notification.raw_data}")
print(f"IP: {notification.ip_address}")
View in Django Admin
Navigate to: /admin/payfast/payfastnotification/
You’ll see:
All webhook attempts
Validation status
Error messages
Raw POST data
IP addresses
Timestamps
Testing Webhooks Locally
Method 1: ngrok (Recommended)
# Start Django
python manage.py runserver
# In another terminal, start ngrok
ngrok http 8000
# Use the ngrok URL in your payment form
notify_url = 'https://abc123.ngrok.io/payfast/notify/'
Method 2: Manual Testing
Send a test POST request:
import requests
test_data = {
'm_payment_id': 'test-123',
'pf_payment_id': '1234567',
'payment_status': 'COMPLETE',
'amount_gross': '100.00',
'amount_fee': '5.75',
'amount_net': '94.25',
'item_name': 'Test Item',
'email_address': 'test@example.com',
}
response = requests.post(
'http://localhost:8000/payfast/notify/',
data=test_data
)
print(response.status_code)
print(response.text)
Common Webhook Issues
Issue 1: Webhook Not Receiving Notifications
Symptoms: Payments complete on PayFast but webhook never fires.
Causes & Solutions:
URL Not Accessible
# Test if your webhook is accessible curl -X POST https://yourdomain.com/payfast/notify/ # Should return: "Method Not Allowed" (405) - This is correct! # Means the endpoint exists and can receive POST requests
Using localhost
PayFast can’t reach
http://localhost:8000Solution: Use ngrok for local development
Firewall Blocking
Check server firewall allows incoming connections
Solution: Whitelist PayFast IP addresses
Wrong URL in Form
# ❌ WRONG - Relative URL notify_url = '/payfast/notify/' # ✅ CORRECT - Absolute URL notify_url = request.build_absolute_uri( reverse('payfast:notify') )
Issue 2: Signature Verification Failing
Symptoms: Webhook returns “Invalid signature”
Causes & Solutions:
Passphrase Mismatch
# settings.py PAYFAST_PASSPHRASE = 'YourExactPassphrase' # Must match PayFast dashboard
Test vs Production Passphrase
# Use different passphrases for test and production if PAYFAST_TEST_MODE: PAYFAST_PASSPHRASE = 'SandboxPassphrase' else: PAYFAST_PASSPHRASE = 'ProductionPassphrase'
Debug Signature
from payfast.utils import generate_signature data = request.POST.dict() calculated = generate_signature(data, PAYFAST_PASSPHRASE) received = data.get('signature') print(f"Calculated: {calculated}") print(f"Received: {received}") print(f"Match: {calculated == received}")
Issue 3: Payment Status Not Updating
Symptoms: Webhook fires but payment stays “pending”
Solution: Check webhook logs for errors
from payfast.models import PayFastNotification
# Get recent failed notifications
failed = PayFastNotification.objects.filter(
is_valid=False,
created_at__gte=timezone.now() - timedelta(hours=1)
)
for notification in failed:
print(notification.validation_errors)
Issue 4: Duplicate Webhooks
Symptoms: Same webhook received multiple times
Causes:
PayFast retries if you don’t respond with HTTP 200
Network issues
Slow processing
Solution: Implement idempotency
from django.db import transaction
@transaction.atomic
def process_webhook(post_data):
m_payment_id = post_data.get('m_payment_id')
# Get payment with lock to prevent race conditions
payment = PayFastPayment.objects.select_for_update().get(
m_payment_id=m_payment_id
)
# Only process if still pending
if payment.status == 'pending':
payment.status = 'complete'
payment.save()
# Process additional logic
grant_access(payment)
# Always return success if payment exists
return True
Webhook Best Practices
1. Respond Quickly
PayFast expects response within 10 seconds:
def post(self, request):
# ✅ GOOD - Quick processing
payment_id = request.POST.get('m_payment_id')
# Update database (fast)
payment = PayFastPayment.objects.get(m_payment_id=payment_id)
payment.mark_complete()
# Queue slow tasks (email, API calls) for later
send_confirmation_email.delay(payment.id)
return HttpResponse('OK', status=200)
2. Use Background Tasks
For slow operations, use Celery:
# tasks.py
from celery import shared_task
@shared_task
def send_confirmation_email(payment_id):
payment = PayFastPayment.objects.get(id=payment_id)
# Send email (slow)
send_mail(...)
@shared_task
def update_external_system(payment_id):
# Call external API (slow)
requests.post(...)
3. Implement Idempotency
Handle duplicate webhooks gracefully:
def process_payment(payment_id):
payment = PayFastPayment.objects.get(m_payment_id=payment_id)
# Check if already processed
if payment.status == 'complete':
logger.info(f"Payment {payment_id} already processed")
return True
# Process payment
payment.mark_complete()
grant_access(payment.user)
return True
4. Log Everything
import logging
logger = logging.getLogger('payfast.webhooks')
def handle_webhook(request):
logger.info(f"Webhook received from {request.META.get('REMOTE_ADDR')}")
try:
# Process webhook
logger.info("Webhook processed successfully")
except Exception as e:
logger.error(f"Webhook processing failed: {e}")
raise
5. Monitor Webhook Health
from payfast.models import PayFastNotification
from django.utils import timezone
from datetime import timedelta
def check_webhook_health():
"""Check for webhook issues in last hour"""
one_hour_ago = timezone.now() - timedelta(hours=1)
total = PayFastNotification.objects.filter(
created_at__gte=one_hour_ago
).count()
failed = PayFastNotification.objects.filter(
created_at__gte=one_hour_ago,
is_valid=False
).count()
success_rate = ((total - failed) / total * 100) if total > 0 else 100
if success_rate < 90:
send_alert(f"Webhook success rate: {success_rate}%")
Production Checklist
Before going live:
☐ Webhook URL is HTTPS
☐ Webhook URL is publicly accessible
☐ Passphrase is configured correctly
☐ Signature verification is enabled
☐ IP validation is enabled
☐ Server-side validation is enabled
☐ Webhook logging is configured
☐ Error monitoring is set up
☐ Background task processing is configured
☐ Idempotency is implemented
☐ Tested with real PayFast account
☐ Monitoring alerts are configured
Next Steps
security - Security best practices
Testing Guide - Testing webhooks thoroughly
Troubleshooting Guide - Solutions to common issues
API Reference - API reference for webhook handler