Payment gateway integrations almost always need a real, publicly reachable URL. It can’t use localhost:8000 because the gateway itself calls your server to confirm a payment happened. While building payment handling for my project using Midtrans’s sandbox environment, I hit exactly that wall, and ngrok was the fix.
The Problem
Midtrans (like most payment gateways: Stripe, Xendit, etc) all work the same way by using a server-to-server notification model for payment status. After a customer completes or fails a payment, Midtrans’s servers send an HTTP POST to a notification URL you configure in your Midtrans dashboard. That’s how your app finds out a payment succeeded; it’s not something the browser tells you directly.
The problem happens when Midtrans’s servers are out on the internet. They cannot POST to http://localhost:8000/midtrans/notification, because localhost only means “this machine.” From Midtrans’s servers, that address means nothing. Setting the notification/webhook URL to a localhost address in the sandbox dashboard simply results in failed delivery: no notification ever arrives, and Midtrans logs a connection error on their end. This is a real blocker during local development. You want to test the full payment flow before deploying anywhere, but the webhook literally cannot reach your machine.
The Fix: ngrok
ngrok creates a temporary public URL that tunnels straight to a port on your local machine. Traffic hits the public ngrok URL, and ngrok forwards it to localhost:8000 (or whatever port your app is running on), so from Midtrans’s point of view, they’re calling a completely normal HTTPS domain.
You can see the documentation here: https://ngrok.com
Installing and Setting Up ngrok
Download it, or install via package manager:
# macOS
brew install ngrok
# or download directly from ngrok.com/download and unzip
ngrok requires a free account to run tunnels past the first few minutes of a session. After signing up, connect your auth token once:
ngrok config add-authtoken <your-authtoken-here>
That token is tied to your ngrok account and only needs to be set once per machine.
Running the Tunnel
For exampe with the Laravel or any app you run locally:
php artisan serve --port=8000
Open a separate terminal and start the tunnel:
ngrok http 8000
ngrok would prints something like:
Forwarding https://[random-subdomain].ngrok-free.app -> http://localhost:8000
That https://[random-subdomain].ngrok-free.app URL is now live on the internet, for as long as this ngrok process keeps running. Closing the terminal, or losing your connection, kills the tunnel and the URL with it.
Inspecting Requests
ngrok runs a local web interface at http://127.0.0.1:4040 while the tunnel is active. This is genuinely the most useful part of the tool for debugging: it shows every request that came through the tunnel, the full headers, and the full body, both what was sent and what your app responded with.
For a webhook integration like the Midtrans notification, this is how you check whether a bug is on your side (bad handler logic) or on the payload side (unexpected field, different structure than expected), without needing to add print statements or log lines just to see what arrived.
Pointing the Notification URL at the Tunnel
In the Midtrans sandbox dashboard, the Payment Notification URL gets set to the ngrok address plus your route:
https://[random-subdomain].ngrok-free.app/midtrans/notification
Triggering a sandbox payment now actually reaches the local route, visible both in the ngrok inspector and in the app’s own logs.
What to Watch Out For
- The free ngrok URL changes every time you restart the tunnel. That means re-pasting the URL into whatever dashboard needs it each session. Annoying, but fine for development. A paid plan gives a fixed subdomain if this gets tedious.
- This is a development workaround, not a deployment strategy. In production, the callback URL should point at the actual deployed domain. ngrok is only for local testing before a real server exists to point at.
- The tunnel is only as available as your own machine. If your laptop sleeps or loses network, the webhook stops arriving, silently, with no error on your side until you notice nothing came through.
Takeaway
Any integration that calls back into your server from the outside (webhooks, OAuth redirects, most third-party callbacks) needs a real reachable URL, and localhost fundamentally can’t satisfy that from an external server’s perspective. ngrok closes that gap for local development without needing a deployed server just to test a callback.