Local Webhook Testing

Testing webhooks locally is essential during development. paystack-django provides two management commands and a WebhookTester utility to make this straightforward.

Overview

Tool

Purpose

paystack_listen

Start a Cloudflare Tunnel so Paystack can reach your local server

paystack_webhook_event

Send a simulated webhook event to your local server

WebhookTester class

Programmatic webhook simulation for automated tests

Step-by-Step Workflow

  1. Start your Django server:

    python manage.py runserver
    
  2. In a second terminal, start the tunnel:

    python manage.py paystack_listen
    

    Copy the printed Webhook URL and paste it in Paystack Dashboard → Settings → API Keys & Webhooks.

  3. In a third terminal, send a test event:

    python manage.py paystack_webhook_event charge.success
    

    Or trigger a real event from Paystack (e.g. complete a test payment).

paystack_webhook_event

This command sends a properly signed webhook POST request to your local server. It uses PAYSTACK['SECRET_KEY'] to generate the HMAC signature, exactly as Paystack does in production.

Basic Usage

# Charge events
python manage.py paystack_webhook_event charge.success

# Transfer events
python manage.py paystack_webhook_event transfer.success --amount 100000

# Subscription events
python manage.py paystack_webhook_event subscription.create --email user@example.com

# Custom data
python manage.py paystack_webhook_event charge.success \
    --data '{"reference": "my_ref_123", "amount": 75000}'

Options

event_type              Event type (e.g. charge.success)
--url URL               Target webhook URL (default: http://localhost:8000/webhooks/paystack/)
--data JSON             Custom JSON payload (overrides sample data)
--reference REF         Transaction reference (auto-generated if omitted)
--amount KOBO           Amount in kobo (default: 50000)
--email EMAIL           Customer email (default: test@example.com)
--list                  List all supported event types

Listing Available Events

python manage.py paystack_webhook_event --list

Output:

Paystack Webhook Event Types

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Charge:
    ● charge.success
  Transfer:
    ● transfer.success
    ● transfer.failed
    ● transfer.reversed
  Subscription:
    ● subscription.create
    ● subscription.disable
  Refund:
    ● refund.processed
    ● refund.failed
  Dispute:
    ● charge.dispute.create
    ● charge.dispute.resolve
  ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ● = sample data included   ○ = use --data for custom payload

WebhookTester (Programmatic)

For automated tests you can use the WebhookTester class directly:

from djpaystack.dev.webhook_tester import WebhookTester

tester = WebhookTester(
    webhook_url='http://localhost:8000/webhooks/paystack/',
    secret_key='sk_test_xxxxx',
)

# Send any event
response = tester.send_event('charge.success', {
    'reference': 'test_001',
    'amount': 50000,
    'status': 'success',
    'customer': {'email': 'test@example.com'},
})
assert response.status_code == 200

Convenience methods are available for common events:

tester.send_charge_success(reference='ref_001', amount=50000, email='a@b.com')
tester.send_subscription_create(email='a@b.com')
tester.send_transfer_success(amount=100000)

Django TestCase Example

import json, hashlib, hmac
from django.test import TestCase, RequestFactory
from unittest.mock import patch
from djpaystack.webhooks.handlers import webhook_handler
from djpaystack.webhooks.views import PaystackWebhookView

class WebhookIntegrationTest(TestCase):
    def test_charge_success_webhook(self):
        secret = 'sk_test_xxxxx'
        payload = json.dumps({
            'event': 'charge.success',
            'data': {
                'reference': 'test_ref',
                'amount': 50000,
                'status': 'success',
                'customer': {'email': 'test@example.com'},
            },
        }).encode()

        signature = hmac.new(
            secret.encode(), payload, hashlib.sha512
        ).hexdigest()

        factory = RequestFactory()
        request = factory.post(
            '/webhook/',
            data=payload,
            content_type='application/json',
            HTTP_X_PAYSTACK_SIGNATURE=signature,
        )

        with patch('djpaystack.webhooks.handlers.paystack_settings') as mock:
            mock.SECRET_KEY = secret
            mock.ENABLE_MODELS = False
            mock.ENABLE_SIGNALS = False
            with patch.object(webhook_handler, 'verify_ip', return_value=True):
                response = PaystackWebhookView.as_view()(request)
                self.assertEqual(response.status_code, 200)

Troubleshooting

“SECRET_KEY not configured”

Make sure PAYSTACK['SECRET_KEY'] is set in your Django settings.

Webhook returns 403 — Unauthorized IP

During local testing, the request comes from 127.0.0.1, which isn’t in Paystack’s IP whitelist. Either:

  • Clear ALLOWED_WEBHOOK_IPS in settings (allows all IPs), or

  • Add 127.0.0.1 to the whitelist during development.

Webhook returns 400 — Invalid signature

The SECRET_KEY used to send the test event must match the one configured in your Django settings.

Connection refused

Make sure python manage.py runserver is running on the expected port.