Bonaventure OgetoBy Bonaventure Ogeto|

Testing Paystack Webhooks Locally with Cloudflare Tunnel

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.

Why You Need a Tunnel for Local Webhook Testing

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:

  1. Paystack sends a POST to https://random-words.trycloudflare.com/webhooks/paystack
  2. Cloudflare receives the request and forwards it through the tunnel to your machine
  3. Your local server at http://localhost:3000/webhooks/paystack receives the request
  4. Your server responds with 200
  5. The response travels back through the tunnel to Paystack

Cloudflare 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.

Installing cloudflared

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

Running a Quick Tunnel

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:

  1. Navigate to Settings > API Keys & Webhooks
  2. Paste the URL with your webhook path appended: https://bright-meadow-abc123.trycloudflare.com/webhooks/paystack
  3. Save

Now 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:

  • The URL is random and changes every time you restart cloudflared. You will need to update the Paystack Dashboard each time.
  • Quick tunnels can be rate-limited under heavy use. For regular development, this is fine. For load testing, use a named tunnel.
  • There is no authentication on quick tunnels. Anyone who guesses the URL can send requests to your local server. This is acceptable for development but never for production.

Named Tunnel for a Stable URL

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.

Verifying Signatures During Local 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:

  1. Is the secret key correct? Test mode uses sk_test_, live mode uses sk_live_. The key must match the mode of the transaction that triggered the webhook.
  2. Is the body being parsed before verification? Express with express.json() middleware will parse the body before your handler sees it. Use express.raw() on the webhook route. See the raw body problem.
  3. Is cloudflared modifying the body? It should not, but if you have other proxies in the chain (Nginx, a local reverse proxy), they might. Remove intermediaries and test with cloudflared pointing directly to your app server.

The Full Testing Workflow

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.

Troubleshooting Common Issues

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.

Cloudflare Tunnel vs ngrok

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.

Security Considerations

A tunnel exposes your local machine to the internet. During development this is acceptable, but keep these points in mind:

  • Stop the tunnel when you are done. Press Ctrl+C in the terminal running cloudflared. The public URL becomes unreachable immediately.
  • Never use live Paystack keys during local testing. Use test keys (sk_test_) only. If your tunnel is compromised, attackers cannot affect real payments.
  • Always verify the signature. Even during development, keep signature verification enabled. It ensures your verification code actually works before you deploy to production.
  • Do not leave the tunnel URL in the Paystack Dashboard when you are done testing. If you leave a dead URL configured, Paystack will keep trying to deliver webhooks to it and failing. Switch back to your production or staging URL.
  • Quick tunnel URLs are unguessable but not secret. The random words in the URL provide obscurity, not security. The HMAC signature verification is your real protection against forged events.

For production webhook security, see verifying the x-paystack-signature header and webhook IP allowlisting.

Key Takeaways

  • Cloudflare Tunnel (cloudflared) exposes your local server to the internet with a single command. No account is required for quick tunnels.
  • Run cloudflared tunnel --url http://localhost:3000 to get a public https URL that forwards requests to your local machine.
  • Set the tunnel URL plus your webhook path in the Paystack Dashboard to receive real webhook events during development.
  • Quick tunnels generate a random URL each time you restart. Named tunnels give you a stable subdomain.
  • Always use your Paystack test secret key for signature verification during local testing. Never paste live keys into development environments.
  • Cloudflare Tunnel is free and does not require a credit card, making it a practical alternative to ngrok for teams in Africa.

Frequently Asked Questions

Is Cloudflare Tunnel free for Paystack webhook testing?
Yes. Quick tunnels are completely free and do not require a Cloudflare account. Named tunnels require a free Cloudflare account and a domain managed by Cloudflare. There are no bandwidth limits or connection time restrictions on either type.
Does Cloudflare Tunnel modify the webhook request body or headers?
No. Cloudflare Tunnel forwards the request body and headers exactly as received. The x-paystack-signature header and the JSON body arrive at your local server unchanged, so your signature verification code works without modifications.
Why does my tunnel URL change every time I restart cloudflared?
Quick tunnels generate a random URL on each run. To get a stable URL, set up a named tunnel with a Cloudflare account and a domain. Named tunnels let you assign a fixed subdomain that persists across restarts.
Can two developers share one Cloudflare Tunnel for Paystack testing?
Paystack only supports one webhook URL per account. Two developers cannot both receive webhooks from the same Paystack account simultaneously through different tunnels. Use separate Paystack test accounts, or have one developer use simulated webhook events with curl instead of live tunnel testing.
Should I use Cloudflare Tunnel or ngrok for Paystack webhook testing?
Both work well. Cloudflare Tunnel has a more generous free tier with no bandwidth limits and no account required for quick tunnels. ngrok has a built-in request inspector that lets you view and replay webhook deliveries, which is useful for debugging. Choose based on which features matter more to your workflow.

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