Handle Paystack Webhooks in Flask
To handle Paystack webhooks in Flask, create a POST route that reads request.data (raw bytes), computes an HMAC SHA-512 hash using your secret key, and compares it to the X-Paystack-Signature header. Parse the JSON and process the event. Return an empty response with status 200.
Webhook Route
# app.py
import hmac
import hashlib
import json
import os
from flask import Flask, request
app = Flask(__name__)
PAYSTACK_SECRET = os.environ.get('PAYSTACK_SECRET_KEY')
@app.route('/webhooks/paystack', methods=['POST'])
def paystack_webhook():
signature = request.headers.get('X-Paystack-Signature', '')
if not signature:
return 'Missing signature', 400
computed_hash = hmac.new(
PAYSTACK_SECRET.encode('utf-8'),
msg=request.data,
digestmod=hashlib.sha512,
).hexdigest()
if not hmac.compare_digest(computed_hash, signature):
return 'Invalid signature', 400
event = json.loads(request.data)
process_event(event)
return '', 200
request.data in Flask gives you the raw bytes of the request body. This is exactly what you need for HMAC computation. No special middleware or configuration is required.
Process Events
def process_event(event):
event_type = event.get('event')
data = event.get('data', {})
if event_type == 'charge.success':
handle_successful_payment(data)
elif event_type == 'charge.failed':
print('Payment failed: ' + str(data.get('reference')))
elif event_type == 'subscription.create':
handle_new_subscription(data)
elif event_type == 'invoice.payment_failed':
handle_failed_renewal(data)
def handle_successful_payment(data):
from helpers import verify_and_fulfill
reference = data.get('reference')
if reference:
verify_and_fulfill(reference, db)
Testing Webhooks
# test_webhook.py
import hmac
import hashlib
import json
def test_valid_webhook(client):
payload = json.dumps({
'event': 'charge.success',
'data': {'reference': 'test_123', 'amount': 500000},
})
signature = hmac.new(
b'sk_test_xxx',
msg=payload.encode(),
digestmod=hashlib.sha512,
).hexdigest()
response = client.post(
'/webhooks/paystack',
data=payload,
content_type='application/json',
headers={'X-Paystack-Signature': signature},
)
assert response.status_code == 200
Local Development with ngrok
npx ngrok http 5000
Set the ngrok HTTPS URL as your webhook URL in the Paystack dashboard: https://abc123.ngrok.io/webhooks/paystack
Production Notes
- HTTPS required. Use gunicorn behind nginx with SSL.
- Respond quickly. Return 200 within seconds. Use Celery for heavy processing.
- Idempotent handlers. Check if the event was already processed.
- Log everything. Store webhook events in a database for debugging.
Key Takeaways
- ✓Flask provides request.data as raw bytes, which is exactly what you need for HMAC verification.
- ✓Use hmac.compare_digest for constant-time signature comparison.
- ✓Return a 200 status immediately. Process events after responding if they are heavy.
- ✓Make handlers idempotent. Paystack retries failed deliveries.
- ✓The key events are charge.success for one-time payments and subscription.create for subscriptions.
- ✓Flask does not have CSRF protection by default, so no special exemption is needed for webhooks.
Frequently Asked Questions
- Does Flask need CSRF exemption for webhooks?
- Flask does not have CSRF protection by default. If you are using Flask-WTF with CSRFProtect, you need to exempt the webhook route using csrf.exempt or the @csrf.exempt decorator.
- Can I use Flask-CORS with the webhook route?
- Webhooks are server-to-server calls. CORS is a browser security feature and does not apply to Paystack webhook requests. You do not need Flask-CORS for the webhook route.
- How do I process webhooks asynchronously in Flask?
- Use Celery with Redis or RabbitMQ. Add the event to a Celery task in your route, return 200, and let the worker process it in the background.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon