Bonaventure OgetoBy Bonaventure Ogeto|

Testing Paystack Webhooks Locally with ngrok

Install ngrok, run ngrok http 3000 to create a tunnel from a public URL to your local port 3000, copy the generated https URL, and paste it as your webhook URL in the Paystack dashboard (with your webhook path appended). When Paystack sends a webhook, it hits the ngrok URL, which forwards the request to your local server. Use the ngrok web inspector at localhost:4040 to see every request and response in real time.

Why You Need ngrok for Webhook Development

When you are developing locally, your server runs on localhost:3000 (or whatever port you choose). Paystack cannot reach localhost. It is not a real internet address. When Paystack tries to deliver a webhook to localhost:3000, the request has nowhere to go.

ngrok solves this by creating a tunnel: a public URL on the internet that forwards traffic to your local machine. When Paystack sends a webhook to https://abc123.ngrok-free.app/webhook/paystack, ngrok receives the request on its servers, forwards it through the tunnel to your machine, and delivers it to localhost:3000/webhook/paystack. Your local server sees the request as if it came from the internet.

This is the standard workflow for developing any webhook integration, not just Paystack. Stripe, GitHub, Twilio, and every other service that sends webhooks all require the same solution during local development.

Alternatives to ngrok exist (Cloudflare Tunnel, localtunnel, Tailscale Funnel), and we cover Cloudflare Tunnel in a separate guide. ngrok is the most popular option because it is the simplest to set up and has the best developer experience.

Installing ngrok

ngrok is a single binary with no dependencies. Download it from ngrok.com/download or install it via package managers:

macOS (Homebrew):

brew install ngrok

Linux (snap):

snap install ngrok

Linux (manual):

wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz
tar xvzf ngrok-v3-stable-linux-amd64.tgz
sudo mv ngrok /usr/local/bin/

Windows (Chocolatey):

choco install ngrok

Verify the installation:

ngrok version

You need an ngrok account (free) to use the service. Sign up at ngrok.com, then authenticate your local installation with your auth token:

ngrok config add-authtoken YOUR_AUTH_TOKEN

Find your auth token on the ngrok dashboard under "Your Authtoken." This is a one-time setup. ngrok remembers the token in its configuration file.

Starting the Tunnel

Make sure your local development server is running first. If your Express or Django or Laravel app runs on port 3000:

ngrok http 3000

ngrok starts the tunnel and displays output like this:

Session Status                online
Account                       your@email.com
Version                       3.x.x
Region                        United States (us)
Forwarding                    https://a1b2c3d4.ngrok-free.app -> http://localhost:3000

The HTTPS URL (something like https://a1b2c3d4.ngrok-free.app) is your public tunnel address. This is what you will give to Paystack.

If your server runs on a different port, change the number:

ngrok http 8000   # Django default
ngrok http 8080   # Common alternative
ngrok http 5000   # Flask default

Keep the ngrok process running in a separate terminal. If you close it, the tunnel shuts down and the URL stops working. On the free plan, every time you restart ngrok, you get a new random URL, which means you need to update your Paystack dashboard each time.

Configuring the Webhook URL in Paystack

With the tunnel running, configure Paystack to send webhooks to your ngrok URL:

  1. Log in to your Paystack dashboard.
  2. Make sure you are in Test Mode (toggle at the top of the dashboard). You should be developing against test keys, not live keys.
  3. Go to Settings > API Keys & Webhooks.
  4. In the Webhook URL field, enter your ngrok URL plus your webhook path. For example: https://a1b2c3d4.ngrok-free.app/webhook/paystack
  5. Save the settings.

Now trigger a test transaction. The simplest way is to use the Paystack Popup on a test page with your test public key. When the transaction completes, Paystack sends a charge.success webhook to your ngrok URL, which forwards it to your local server.

A common mistake: forgetting to append your webhook path. If your Express route is app.post('/webhook/paystack', ...), the full URL must be https://a1b2c3d4.ngrok-free.app/webhook/paystack, not just https://a1b2c3d4.ngrok-free.app. Without the path, ngrok delivers the request to your root route, which probably returns a 404 or your homepage.

Another common mistake: using your live Paystack keys during development. Always use test mode. Test transactions do not move real money, and test webhooks behave identically to live webhooks except for the data being simulated.

Inspecting Requests with the ngrok Web Inspector

One of ngrok's best features is the web inspector. While your tunnel is running, open http://localhost:4040 in your browser. This dashboard shows every HTTP request that passes through the tunnel in real time.

For each request, you can see:

  • Request headers. Including the x-paystack-signature header, Content-Type, and User-Agent.
  • Request body. The full JSON payload that Paystack sent. This is the webhook event data with the event type, transaction details, customer information, and all metadata.
  • Response status. What your server returned (200, 401, 500, etc.).
  • Response headers and body. What your server sent back to Paystack.
  • Timing. How long your server took to respond. If this number is high, you know your handler is too slow and will trigger retries in production.

The inspector is your primary debugging tool during webhook development. When something is not working, open the inspector and check:

  • Did the request arrive at all? If not, your Paystack webhook URL is wrong.
  • Did your server return 200? If it returned 401, your signature verification is failing. If it returned 500, your handler threw an error.
  • What does the request body look like? This tells you the exact structure of the event data, which is often different from what the documentation examples show for your specific use case.

The inspector also has a "Replay" button that resends any captured request to your local server. This is invaluable for debugging: trigger a webhook once, then replay it as many times as you need while fixing your handler code. No need to create a new test transaction each time.

Free Plan vs Paid Plan

ngrok offers a free tier and several paid plans. Here is what matters for Paystack webhook development:

Free plan:

  • Random subdomain that changes every time you restart ngrok (like a1b2c3d4.ngrok-free.app)
  • One tunnel at a time
  • Rate-limited connections
  • A browser interstitial page warning visitors that the site is served via ngrok (this does not affect API requests like webhooks)

Paid plan (starting at $8/month):

  • Stable custom subdomain (like myapp.ngrok.io) that persists across restarts
  • Multiple simultaneous tunnels
  • Higher connection limits
  • No interstitial page
  • IP restrictions and other security features

For Paystack webhook development, the free plan works fine. The main inconvenience is updating your Paystack dashboard webhook URL every time you restart ngrok. If you develop webhooks frequently and this becomes annoying, the paid plan pays for itself in saved time.

On the free plan, ngrok adds a warning page for browser requests. This does not affect Paystack webhooks because Paystack sends HTTP POST requests programmatically, not through a browser. The interstitial page is only shown to browser-initiated requests.

If you want a free alternative with stable URLs, consider Cloudflare Tunnel, which offers free tunnels with custom subdomains under your own domain.

Troubleshooting Common Issues

These are the problems developers hit most often when testing Paystack webhooks with ngrok:

1. Signature verification fails. Your handler rejects the webhook with a 401 error. This usually means your handler is using the live secret key but your Paystack dashboard is in test mode (or vice versa). The test mode uses a different secret key than live mode. Make sure your application's PAYSTACK_SECRET_KEY environment variable matches the mode you are using in the dashboard.

2. Webhooks are not arriving. You triggered a test transaction but nothing shows up in ngrok's inspector. Check: Is the ngrok tunnel still running? Is the webhook URL in the Paystack dashboard correct (including the path)? Are you in test mode in the dashboard? Did the transaction actually complete (check the Paystack dashboard for the transaction status)?

3. Your server returns 404. The request arrives at ngrok (you can see it in the inspector) but your server returns 404. This means the webhook path in your Paystack dashboard does not match the route in your application. Compare the path in the ngrok URL against your route definition.

4. Connection refused. ngrok says "connection refused" in the terminal. Your local server is not running, or it is running on a different port than what you told ngrok. Start your server first, then start ngrok with the correct port.

5. ngrok URL changed and webhooks stopped. You restarted ngrok (free plan) and got a new URL, but the Paystack dashboard still has the old URL. Update the webhook URL in the dashboard every time you restart ngrok. This is the biggest nuisance of the free plan.

6. Raw body parsing issues. Your handler receives the request but the JSON body is empty or malformed. This usually means your Express/framework middleware is parsing the body before your handler can access the raw bytes. For signature verification, you need the raw request body. See The Raw Body Problem for the fix.

Using an ngrok Configuration File

Instead of typing command-line flags every time, you can create an ngrok configuration file that stores your tunnel settings:

# ~/.config/ngrok/ngrok.yml (Linux/macOS)
version: "3"
authtoken: YOUR_AUTH_TOKEN

tunnels:
  paystack-dev:
    proto: http
    addr: 3000
    inspect: true

Start the named tunnel:

ngrok start paystack-dev

If you are on a paid plan with a custom domain, add it to the config:

tunnels:
  paystack-dev:
    proto: http
    addr: 3000
    hostname: paystack-dev.ngrok.io
    inspect: true

The configuration file is especially useful if you work on multiple projects with different port numbers. Define a tunnel for each project and start the one you need.

Security Considerations

ngrok exposes your local machine to the internet. While the tunnel is running, anyone who knows the URL can send requests to your local server. Keep these security points in mind:

  • Do not leave tunnels running unattended. Stop ngrok when you are done developing. A running tunnel is an open door to your local machine.
  • Use signature verification in development too. It is tempting to skip signature verification during development "because it is just testing." Do not. Verify signatures in every environment. This catches bugs in your verification logic before they reach production.
  • Do not use ngrok in production. ngrok is a development tool. For production webhook endpoints, deploy your application to a proper hosting platform with its own domain and SSL certificate.
  • Be careful with test data. Even in test mode, webhook payloads may contain email addresses or other data you have entered. If you are sharing ngrok URLs with teammates, be aware that they can see each other's test data in the inspector.

On paid plans, ngrok offers IP restrictions that let you limit which IP addresses can reach your tunnel. You can restrict it to Paystack's webhook IPs if you know them, adding an extra layer of security. See Webhook IP Allowlisting for details on Paystack's IP ranges.

The Complete Development Workflow

Here is the end-to-end workflow for testing Paystack webhooks locally with ngrok:

  1. Start your local server. npm run dev (or whatever your start command is). Verify it is running by visiting localhost:3000 in your browser.
  2. Start ngrok. ngrok http 3000. Copy the HTTPS URL from the output.
  3. Update Paystack dashboard. Paste the ngrok URL plus your webhook path into the Webhook URL field in Settings > API Keys & Webhooks. Make sure you are in test mode.
  4. Trigger a test transaction. Use the Paystack Popup, the API, or the dashboard to create a test transaction.
  5. Watch the ngrok inspector. Open localhost:4040 in your browser. You should see the incoming webhook request within a few seconds of the transaction completing.
  6. Debug your handler. Check the response code in the inspector. If it is 200, your handler is working. If not, read the response body for error details and fix your code.
  7. Iterate. Use the inspector's Replay button to re-send the same request while you fix bugs. No need to create new test transactions.
  8. Stop ngrok when done. Ctrl+C to stop the tunnel. Remember to update your Paystack webhook URL back to your production URL before going live.

This workflow should become second nature. Every developer building a Paystack integration goes through this cycle dozens of times during development. The faster you can iterate through it, the faster you ship a reliable webhook handler.

Key Takeaways

  • ngrok creates a secure tunnel from a public HTTPS URL to your local development server. No server deployment needed to test webhooks.
  • The free ngrok plan gives you a random URL that changes every time you restart ngrok. The paid plan gives you a stable subdomain that persists across restarts.
  • Always use the HTTPS URL from ngrok, not the HTTP one. Paystack requires HTTPS for webhook URLs.
  • The ngrok web inspector at localhost:4040 shows every request and response in real time, including headers, body, and timing. This is your primary debugging tool during development.
  • Update your Paystack dashboard webhook URL to point to the ngrok URL plus your webhook path (for example, https://abc123.ngrok-free.app/webhook/paystack).
  • Remember to switch back to your production webhook URL before going live. A common mistake is leaving the ngrok URL in the Paystack dashboard and wondering why production webhooks are not arriving.

Frequently Asked Questions

Is ngrok free to use for Paystack webhook testing?
Yes. The free plan is sufficient for webhook development. You get one tunnel at a time with a random subdomain that changes on restart. The only limitation that affects webhook testing is the URL change on restart, which means updating the Paystack dashboard each time. Paid plans ($8/month and up) give you stable custom subdomains.
Does ngrok work with Paystack live mode or only test mode?
ngrok works with both modes technically, but you should only use it with test mode during development. Using ngrok with live mode means real customer payment webhooks would be routed through a development tunnel to your local machine, which is unreliable and insecure. In production, deploy your application to a real server with a proper domain.
Why is my ngrok URL returning a warning page instead of my webhook response?
The free ngrok plan shows an interstitial warning page for browser-initiated requests. This does not affect Paystack webhooks, which are HTTP POST requests sent programmatically. Paystack does not load the interstitial page. If you are seeing 200 responses in the ngrok inspector but your handler is not processing, the issue is in your application code, not ngrok.
Can I use ngrok to test webhooks from multiple Paystack subaccounts?
Yes. ngrok forwards all HTTP requests to your local server regardless of who sends them. If you have multiple Paystack subaccounts sending webhooks to the same ngrok URL, your handler will receive all of them. Your handler code needs to route events correctly based on the payload content.
What happens to webhook requests when ngrok is not running?
When ngrok is not running, the public URL does not exist. Paystack tries to connect, gets a DNS or connection error, and treats the delivery as failed. Paystack will retry the webhook according to its retry schedule. When you restart ngrok (on the free plan, with a new URL), the old URL still does not work. You need to update the dashboard with the new URL.

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