Webhooks
paystack-django includes a production-ready webhook system with signature verification, IP whitelisting, event deduplication, Django model persistence, and Django signal dispatch.
Setup
Add the URL endpoint:
# urls.py from django.urls import path from djpaystack.webhooks.views import handle_webhook urlpatterns = [ path('webhooks/paystack/', handle_webhook, name='paystack-webhook'), ]
Set the Webhook Secret:
In the Paystack Dashboard, add your URL and copy the secret:
PAYSTACK = { 'SECRET_KEY': 'sk_...', 'PUBLIC_KEY': 'pk_...', 'WEBHOOK_SECRET': 'whsec_...', }
How It Works
When Paystack sends a webhook:
Signature verified — HMAC SHA-512 of the raw body is compared against
X-Paystack-Signature.IP checked — Request IP is compared against Paystack’s known IPs (or your custom whitelist).
Event deduplicated — Duplicate events (same reference + event type) are skipped.
Handler dispatched — The registered handler for the event type is called.
Model saved — If
ENABLE_MODELSisTrue, data is persisted toPaystackWebhookEvent.Signal sent — If
ENABLE_SIGNALSisTrue, the appropriate Django signal fires.
Supported Webhook Events
Event |
Description |
|---|---|
|
Payment completed successfully |
|
Payment attempt failed |
|
New dispute opened |
|
Dispute reminder |
|
Dispute resolved |
|
Transfer completed |
|
Transfer failed |
|
Transfer reversed |
|
Bank transfer rejected |
|
Subscription created |
|
Subscription disabled |
|
Subscription will not renew |
|
Refund initiated |
|
Refund being processed |
|
Refund completed |
|
Refund failed |
|
Refund requires manual intervention |
|
DVA assigned successfully |
|
Direct debit authorization created |
|
Direct debit authorization activated |
See djpaystack.webhooks.events.WebhookEvent for the full enum.
Custom Handlers
Register a custom handler that overrides or extends the default behaviour:
from djpaystack.webhooks.handlers import WebhookHandler
from djpaystack.webhooks.events import WebhookEvent
handler = WebhookHandler()
@handler.register(WebhookEvent.CHARGE_SUCCESS)
def my_charge_success(data):
reference = data['reference']
# Your custom logic here
Django Signals
from django.dispatch import receiver
from djpaystack.signals import (
paystack_payment_successful,
paystack_payment_failed,
paystack_transfer_successful,
paystack_refund_processed,
paystack_dispute_created,
)
@receiver(paystack_payment_successful)
def on_payment(sender, transaction_data, **kwargs):
print(f"Paid: {transaction_data['reference']}")
Testing Webhooks Locally
Use ngrok to expose your local server:
ngrok http 8000
# Use https://xxxx.ngrok.io/webhooks/paystack/ as your webhook URL
Best Practices
Always configure WEBHOOK_SECRET — Without it, all requests are rejected.
Respond quickly — Return
200within 5 seconds. Offload heavy work to Celery.Process idempotently — The same event may be delivered more than once.
Log events —
PaystackWebhookEventmodel stores all received events.Monitor failures — Check
PaystackWebhookEvent.objects.filter(processed=False).
For advanced patterns, see Advanced Webhook Handling.