Cloudflare Tunnel (cloudflared) creates a secure tunnel from a public URL to your local development server. Run cloudflared tunnel --url http://localhost:3000, copy the generated URL, paste it into the Paystack Dashboard as your webhook URL, and your local server will receive real Paystack webhook events. No account is required for quick tunnels.
Paystack sends webhooks as HTTP POST requests to a URL you register in the dashboard. That URL must be reachable from the public internet. Your local development machine, running behind a router or NAT, is not.
When you visit http://localhost:3000 in your browser, that works because the browser and the server are on the same machine. Paystack's servers are in a data center somewhere else entirely. They cannot reach your localhost.
A tunnel solves this by creating a secure connection between your local server and a publicly accessible URL. Traffic flows like this:
https://random-words.trycloudflare.com/webhooks/paystackhttp://localhost:3000/webhooks/paystack receives the requestCloudflare Tunnel is one of several tunneling options. We cover ngrok in a separate guide. This guide focuses on Cloudflare Tunnel because it is free with no bandwidth limits, requires no account for quick tunnels, and works reliably across Linux, macOS, and Windows.
This article is part of the Paystack webhooks engineering guide.
The CLI tool is called cloudflared. Installation depends on your operating system.
macOS (Homebrew)
brew install cloudflared
Ubuntu / Debian
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update
sudo apt install cloudflared
Windows
# Download the installer from
# https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/
# Or use winget:
winget install --id Cloudflare.cloudflared
Direct binary (any Linux)
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
chmod +x cloudflared
sudo mv cloudflared /usr/local/bin/
Verify the installation:
cloudflared --version
# cloudflared version 2026.x.x
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
A quick tunnel is the fastest way to get started. No Cloudflare account, no configuration, no DNS setup. One command:
# Make sure your local server is running first
# (e.g., node server.js is listening on port 3000)
cloudflared tunnel --url http://localhost:3000
The output will look something like this:
2026-07-20T10:30:00Z INF Thank you for trying Cloudflare Tunnel!
2026-07-20T10:30:00Z INF Your quick Tunnel has been created!
2026-07-20T10:30:00Z INF +---------------------------------------------------+
2026-07-20T10:30:00Z INF | Your free tunnel URL is: |
2026-07-20T10:30:00Z INF | https://bright-meadow-abc123.trycloudflare.com |
2026-07-20T10:30:00Z INF +---------------------------------------------------+
Copy that URL. Now go to the Paystack Dashboard:
https://bright-meadow-abc123.trycloudflare.com/webhooks/paystackNow trigger a test transaction. Use Paystack's test card numbers in your checkout flow. Within seconds, you should see the webhook POST arrive at your local server.
Quick tunnel limitations:
If you are tired of updating the Paystack Dashboard every time you restart cloudflared, set up a named tunnel. This requires a free Cloudflare account and a domain managed by Cloudflare.
# 1. Authenticate with Cloudflare
cloudflared tunnel login
# This opens a browser window. Select the domain you want to use.
# 2. Create a named tunnel
cloudflared tunnel create paystack-dev
# Output: Created tunnel paystack-dev with id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# 3. Route traffic from a subdomain to the tunnel
cloudflared tunnel route dns paystack-dev webhooks-dev.yourdomain.com
# 4. Create a config file at ~/.cloudflared/config.yml
Create the config file:
# ~/.cloudflared/config.yml
tunnel: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
credentials-file: /home/youruser/.cloudflared/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json
ingress:
- hostname: webhooks-dev.yourdomain.com
service: http://localhost:3000
- service: http_status:404
Now run the tunnel:
cloudflared tunnel run paystack-dev
Your webhook URL is now https://webhooks-dev.yourdomain.com/webhooks/paystack. It stays the same across restarts. Set it once in the Paystack Dashboard and forget about it until you change domains.
Named tunnels also support access policies if you want to restrict who can reach the tunnel. For local development this is overkill, but it is useful if you share the tunnel URL with teammates for collaborative testing.
The tunnel does not modify the request body or headers. Paystack's x-paystack-signature header arrives at your local server exactly as Paystack sent it. Your signature verification code should work without changes.
One thing to double-check: you must use the correct secret key for the environment. If you are testing with Paystack's test mode, use your test secret key (sk_test_...). If you accidentally use a live secret key to verify a test mode webhook, the signature will not match because Paystack signed the payload with the test key.
// Make sure your .env file has the test key
// PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
const crypto = require('crypto');
function verifyPaystackSignature(rawBody, signatureHeader) {
const secret = process.env.PAYSTACK_SECRET_KEY;
const hash = crypto
.createHmac('sha512', secret)
.update(rawBody)
.digest('hex');
return hash === signatureHeader;
}
If verification fails during tunnel testing, check these things in order:
sk_test_, live mode uses sk_live_. The key must match the mode of the transaction that triggered the webhook.express.json() middleware will parse the body before your handler sees it. Use express.raw() on the webhook route. See the raw body problem.Here is the complete workflow from start to finish:
# Terminal 1: Start your application
cd /path/to/your/project
npm run dev
# Server running on http://localhost:3000
# Terminal 2: Start the tunnel
cloudflared tunnel --url http://localhost:3000
# Tunnel URL: https://bright-meadow-abc123.trycloudflare.com
Then in the Paystack Dashboard, set the webhook URL to https://bright-meadow-abc123.trycloudflare.com/webhooks/paystack.
Now trigger events. The easiest way is to make a test payment:
// Initialize a transaction using the Paystack API
const response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'test@example.com',
amount: 50000, // 500 NGN in kobo
reference: 'test-' + Date.now(),
}),
});
const data = await response.json();
console.log('Checkout URL:', data.data.authorization_url);
// Open this URL in a browser and complete payment with a test card
Use Paystack's test card numbers to complete the payment. After the charge succeeds, Paystack sends a charge.success webhook to your tunnel URL, which forwards it to your local server.
Watch your server logs. You should see the event arrive:
Received webhook: charge.success
Reference: test-1721472000000
Amount: 50000
Currency: NGN
If nothing arrives, check the Paystack Dashboard under API Keys & Webhooks. Paystack shows recent webhook delivery attempts and whether they succeeded or failed. A failed delivery with a connection error means the tunnel is not running or the URL is wrong.
Tunnel starts but no webhooks arrive
Check that the URL in the Paystack Dashboard matches the tunnel URL exactly, including the path. A common mistake is setting https://bright-meadow-abc123.trycloudflare.com without appending /webhooks/paystack (or whatever your endpoint path is).
Webhooks arrive but signature verification fails
This is almost always a key mismatch or a body parsing issue. Verify that PAYSTACK_SECRET_KEY in your .env file starts with sk_test_ and matches the key shown in the Paystack Dashboard for the same account. Then confirm your webhook route reads the raw body before any JSON parsing middleware runs.
Tunnel disconnects frequently
Quick tunnels can be interrupted by network changes (WiFi switching, VPN toggling). If this is a recurring problem, use a named tunnel. Named tunnels reconnect automatically after brief network interruptions.
Port conflict
If your app runs on a different port, pass that port to cloudflared:
cloudflared tunnel --url http://localhost:8080
HTTPS locally
If your local server uses HTTPS (self-signed cert), point cloudflared to the HTTPS URL:
cloudflared tunnel --url https://localhost:3443 --no-tls-verify
The --no-tls-verify flag tells cloudflared to accept your self-signed certificate. The connection between Cloudflare and the public internet is still secured by Cloudflare's own certificate.
Multiple developers on the same Paystack account
Paystack only allows one webhook URL per account. If two developers are testing simultaneously, only one will receive webhooks. Solutions: use separate Paystack test accounts, or have one developer use simulated webhook events instead of the tunnel.
Both tools solve the same problem. Here is how they compare for Paystack webhook testing:
| Feature | Cloudflare Tunnel | ngrok |
|---|---|---|
| Free tier | Unlimited bandwidth, no account required for quick tunnels | Free tier available, account required, bandwidth limits |
| Stable URL | Named tunnels (requires Cloudflare account + domain) | Static domains on paid plans |
| Request inspection | No built-in web inspector | Web inspector at localhost:4040 shows all requests |
| Replay requests | Not built in | Built-in replay from the web inspector |
| Setup complexity | One binary, one command | One binary, one command (plus account signup) |
For Paystack webhook testing specifically, ngrok's request inspector is genuinely useful. You can see the exact headers and body of each webhook delivery, and replay requests without triggering a new transaction. Cloudflare Tunnel does not have this feature built in.
On the other hand, Cloudflare Tunnel's free tier is more generous. No bandwidth caps, no connection time limits, no account required for quick tunnels. For developers in Africa where internet costs matter, this is a practical advantage.
Pick whichever you prefer. Both work correctly with Paystack webhooks. See the ngrok guide for the alternative setup.
A tunnel exposes your local machine to the internet. During development this is acceptable, but keep these points in mind:
sk_test_) only. If your tunnel is compromised, attackers cannot affect real payments.For production webhook security, see verifying the x-paystack-signature header and webhook IP allowlisting.
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