Check these in order: (1) Your webhook URL is correctly entered in the Paystack dashboard. (2) The URL uses HTTPS, not HTTP. (3) Your domain DNS resolves correctly. (4) Your firewall allows inbound HTTPS traffic. (5) Your server is running and the route exists. (6) Your endpoint returns 200 within 10 seconds. (7) You are using the right mode (test vs live). (8) Paystack has not exhausted retries for your endpoint. Start at step 1 and work down. Most issues are caught in the first three steps.
Log in to your Paystack dashboard. Go to Settings, then API Keys & Webhooks (or just Webhooks, depending on your dashboard version). Look at the webhook URL field.
Verify these things:
https://yourapp.com/webhooks/paystack is different from https://yourapp.com/webhook/paystack (missing 's').http://localhost:3000/webhooks/paystack is only reachable from your machine.How to verify:
# From your local machine or any machine with internet access
curl -X POST https://yourapp.com/webhooks/paystack -H "Content-Type: application/json" -d '{"test": true}'
# You should get a response (even if it's a 401 or 400).
# If you get "Could not resolve host" or "Connection refused",
# the URL is wrong or your server is down.
Paystack only sends webhooks to HTTPS URLs. If your URL starts with http://, Paystack will not send anything.
Check that:
https://How to verify:
# Check SSL certificate
openssl s_client -connect yourapp.com:443 -servername yourapp.com /dev/null | openssl x509 -noout -dates
# You should see:
# notBefore=...
# notAfter=... (this date must be in the future)
If you are developing locally, you need a tunnel service to get a public HTTPS URL:
# Using ngrok
ngrok http 3000
# Gives you https://abc123.ngrok-free.app
# Using Cloudflare Tunnel
cloudflared tunnel --url http://localhost:3000
# Gives you https://something.trycloudflare.com
Set the tunnel URL as your webhook URL in the Paystack dashboard during development.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Your domain must resolve to a valid IP address that Paystack's servers can reach.
How to verify:
# Check DNS resolution
dig yourapp.com +short
# Should return an IP address like 104.21.55.123
# Or use nslookup
nslookup yourapp.com
# Should show an Address line with your server's IP
Common DNS problems:
yourapp.com but your webhook URL uses api.yourapp.com. The subdomain needs its own DNS record.Your server must accept inbound HTTPS traffic (port 443) from Paystack's IP addresses.
How to verify:
# Test connectivity from outside your network
# Use a different machine or a service like https://reqbin.com
# Send a POST request to your webhook URL
# On AWS, check your Security Group inbound rules:
# Port 443 should be open to 0.0.0.0/0 (or at minimum, Paystack's IPs)
# On DigitalOcean, check your Firewall settings
# On Google Cloud, check your VPC firewall rules
Things that can silently block webhooks:
If you use Cloudflare in front of your server, make sure the webhook path is not behind a challenge page or bot protection rule. Paystack's servers will not solve CAPTCHAs.
This sounds obvious, but verify your application process is running and listening on the correct port.
How to verify:
# Check if your process is running
ps aux | grep node # for Node.js
ps aux | grep python # for Django
ps aux | grep php # for Laravel
# Check if something is listening on the expected port
ss -tlnp | grep :3000 # replace 3000 with your port
# Check your app logs for crash errors
tail -100 /var/log/yourapp/error.log
Common issues:
Your webhook endpoint must handle POST requests. A GET-only route will return 405 Method Not Allowed, and Paystack will treat that as a failure.
How to verify:
# Send a test POST request to your endpoint
curl -X POST https://yourapp.com/webhooks/paystack -H "Content-Type: application/json" -d '{"event":"charge.success","data":{}}' -v
# Look at the response:
# 200 = your endpoint is reachable and responding correctly
# 404 = the route does not exist (wrong path)
# 405 = the route exists but does not accept POST
# 500 = your handler is crashing
# 301/302 = redirect (Paystack may not follow redirects)
Watch out for trailing slash issues. Some frameworks treat /webhooks/paystack and /webhooks/paystack/ as different routes. If your framework redirects from one to the other with a 301, Paystack may not follow the redirect. Set your webhook URL to match the exact path your server expects.
Paystack expects your endpoint to return HTTP 200 within approximately 10 seconds. If your endpoint returns anything other than 200, or if it takes too long, Paystack marks the delivery as failed and schedules a retry.
Common mistakes:
The correct pattern:
app.post('/webhooks/paystack', async (req, res) => {
// 1. Verify signature (fast operation)
// 2. Return 200 IMMEDIATELY
res.sendStatus(200);
// 3. Process the event after responding
// Use a queue for reliable processing
try {
await processWebhookEvent(req.body);
} catch (err) {
console.error('Webhook processing failed:', err);
// The 200 was already sent, so Paystack will not retry.
// You need your own retry mechanism here.
}
});
Better yet, push the event to a message queue (BullMQ, Celery, Laravel Queues) and let a worker process it. That way your webhook handler always responds in milliseconds.
Paystack test mode and live mode have separate webhook configurations. This catches many developers after their first deployment.
Symptoms:
The cause: you set the webhook URL in test mode but never set it in live mode. Or you set a different URL in live mode that is wrong.
How to verify:
Also verify that you are using the matching API key. If you initialize transactions with your live secret key, the webhooks will come from live mode. Your server must be set up to receive live webhooks at the URL configured in live mode.
Paystack retries failed webhook deliveries with exponential backoff. After repeated failures, it may stop retrying for your endpoint.
How to check:
The dashboard shows you the exact response your server returned for each webhook attempt. If you see a pattern of 500s or timeouts, that tells you exactly what to fix.
If Paystack has stopped retrying because of too many failures, you can manually retry from the dashboard once you have fixed the underlying issue. You can also trigger a new event (like a test transaction) to confirm your endpoint is now working.
For a deeper look at retry behavior, see Paystack Webhook Retries: What to Expect and How to Cope.
After fixing the issue, run this end-to-end test to confirm everything works:
// Add this temporary logging to your webhook handler
app.post('/webhooks/paystack', (req, res) => {
console.log('[WEBHOOK] Received at:', new Date().toISOString());
console.log('[WEBHOOK] Headers:', JSON.stringify({
'content-type': req.headers['content-type'],
'x-paystack-signature': req.headers['x-paystack-signature'] ? 'present' : 'missing',
}));
console.log('[WEBHOOK] Body length:', JSON.stringify(req.body).length);
// Your signature verification here...
res.sendStatus(200);
console.log('[WEBHOOK] 200 sent');
});
Once you confirm the test webhook arrives and is processed correctly, remove the verbose logging and keep only essential operational logs.
For the complete error reference, see Paystack Errors and Troubleshooting: The Complete Reference. If webhooks arrive but signature verification fails, see Paystack Signature Verification Failed: The Definitive Fix.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course