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'), ]
Configure your secret key:
Paystack uses your API secret key to sign webhooks. In the Paystack Dashboard, add your webhook URL. No separate webhook secret is needed:
PAYSTACK = { 'SECRET_KEY': 'sk_...', 'PUBLIC_KEY': 'pk_...', }
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 |
|
New dispute opened |
|
Dispute reminder sent |
|
Dispute resolved |
|
Customer identity verification succeeded |
|
Customer identity verification failed |
|
Dedicated Virtual Account assigned |
|
Dedicated Virtual Account assignment failed |
|
New invoice created |
|
Invoice updated |
|
Invoice payment failed |
|
Payment request pending |
|
Payment request paid |
|
Refund initiated |
|
Refund being processed |
|
Refund completed |
|
Refund failed |
|
Subscription created |
|
Subscription disabled / cancelled |
|
Subscription will not renew |
|
Cards on subscriptions are about to expire |
|
Transfer completed |
|
Transfer failed |
|
Transfer reversed |
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 webhook_handler
from djpaystack.webhooks.events import WebhookEvent
def my_charge_success(data):
reference = data['reference']
# Your custom logic here
# Override the default handler
webhook_handler.register(WebhookEvent.CHARGE_SUCCESS, my_charge_success)
Django Signals
from django.dispatch import receiver
from djpaystack.signals import (
paystack_payment_successful,
paystack_transfer_successful,
paystack_refund_processed,
paystack_dispute_created,
paystack_invoice_created,
paystack_customeridentification_success,
)
@receiver(paystack_payment_successful)
def on_payment(sender, data, **kwargs):
print(f"Paid: {data['reference']}")
Testing Webhooks Locally
Use the built-in paystack_listen command to expose your local server
via a Cloudflare Tunnel:
python manage.py paystack_listen
# Displays https://xxxx.trycloudflare.com/webhooks/paystack/
Or send simulated events directly:
python manage.py paystack_webhook_event charge.success
See Cloudflare Tunnel for Local Development and Local Webhook Testing for full documentation.
Best Practices
Keep your SECRET_KEY safe — Webhook signatures are verified using your API secret key.
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 and Webhook Security.