Bonaventure OgetoBy Bonaventure Ogeto|

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

  1. HTTPS required. Use gunicorn behind nginx with SSL.
  2. Respond quickly. Return 200 within seconds. Use Celery for heavy processing.
  3. Idempotent handlers. Check if the event was already processed.
  4. Log everything. Store webhook events in a database for debugging.

Ship Payments Faster

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