Webhook Security
Paystack uses HMAC SHA-512 signatures to prove that a webhook request
genuinely originated from their servers. paystack-django verifies these
signatures automatically — all you need is a valid PAYSTACK['SECRET_KEY'].
Why SECRET_KEY and not a separate webhook secret?
Paystack computes the webhook signature using your API secret key
(the same sk_live_... or sk_test_... key you use for API calls).
There is no separate “webhook secret” in Paystack’s architecture.
paystack-django therefore uses PAYSTACK['SECRET_KEY'] for both:
Authenticating outbound API requests (
Authorization: Bearer sk_...).Verifying inbound webhook signatures (
X-Paystack-Signatureheader).
This simplifies configuration — one key to manage instead of two.
# settings.py — this is all you need
PAYSTACK = {
'SECRET_KEY': 'sk_live_xxxxxxxxxxxxxxxx',
'PUBLIC_KEY': 'pk_live_xxxxxxxxxxxxxxxx',
}
Important
If you previously configured PAYSTACK['WEBHOOK_SECRET'], you can
safely remove it. As of v1.2.0 that setting is ignored.
Webhook verification now uses SECRET_KEY exclusively.
How HMAC SHA-512 Verification Works
When Paystack sends a webhook, it:
Takes the raw JSON body of the POST request.
Computes
HMAC-SHA512(body, your_secret_key).Sends the hex digest in the
X-Paystack-SignatureHTTP header.
paystack-django reproduces the same computation on your server:
import hmac, hashlib
computed = hmac.new(
secret_key.encode('utf-8'),
request.body, # raw bytes
hashlib.sha512,
).hexdigest()
is_valid = hmac.compare_digest(computed, request_signature)
If the two digests match, the payload is authentic and untampered.
hmac.compare_digest is used (instead of ==) to prevent timing attacks.
Request Flow
┌────────────┐ ┌──────────────────┐
│ Paystack │ POST │ Your Django App │
│ Servers │ ─────────► │ /webhooks/paystack/ │
└────────────┘ └──────────────────┘
│ │
1. JSON body 2. Read X-Paystack-Signature
2. HMAC-SHA512(body, sk_...) 3. HMAC-SHA512(body, sk_...)
3. Attach as header 4. compare_digest()
5. If valid → process event
If invalid → 400 Bad Request
Additional Layers
IP Whitelisting
Paystack sends webhooks from a known set of IPs. By default paystack-django allows these IPs:
52.31.139.7552.49.173.16952.214.14.220
You can override the whitelist:
PAYSTACK = {
'SECRET_KEY': 'sk_...',
'ALLOWED_WEBHOOK_IPS': ['52.31.139.75', '52.49.173.169', '52.214.14.220'],
}
Set to an empty list to use the default Paystack IPs.
Event Deduplication
Paystack may retry webhook delivery. paystack-django tracks processed event
IDs in an OrderedDict (in-memory, capped at 1 000 entries) and
optionally in the PaystackWebhookEvent database model, preventing
double-processing.
HTTPS in Production
Paystack requires HTTPS for webhook URLs in production. During local
development, the paystack_listen command provides an HTTPS tunnel
automatically via Cloudflare.
Best Practices
Never expose ``SECRET_KEY`` in client-side code or version control. Use environment variables:
import os PAYSTACK = { 'SECRET_KEY': os.environ['PAYSTACK_SECRET_KEY'], }
Always verify signatures — paystack-django does this by default; do not bypass it.
Return 200 quickly — Process webhook data asynchronously (e.g. with Celery) and return
200 OKwithin a few seconds, or Paystack will retry.Handle retries idempotently — Use the built-in deduplication or check transaction references before fulfilling orders.
Monitor failures — Check
PaystackWebhookEventrecords withprocessed=Falsefor events that could not be handled.