Using Ngrok Custom Subdomains for Stable Webhook Testing
Stop updating webhook URLs every time you restart your server. Learn how to use ngrok custom subdomains to create a persistent, TLS-enabled bridge to your local environment.
31 Aug 2026, 14:41 UTC

The Webhook Configuration Headache
When testing a service that relies on inbound webhooks - such as Stripe payment events or GitHub push notifications - you need a publicly reachable URL that your local machine can accept. Running a development server behind a NAT or firewall means the only practical way to expose it is through a tunneling service. Ngrok creates an outbound connection to its edge network and forwards traffic to a local port. However, each time you start the tunnel without a reserved name, ngrok assigns a random subdomain like a1b2-c3d4.ngrok-free.app. After a restart or a crash, you must copy the new URL into the third-party dashboard, breaking the flow and wasting time.
How Custom Subdomains Provide a Stable Endpoint
Ngrok allows you to reserve a subdomain (e.g., dev-api-project.ngrok-free.app) that maps to your account. When you launch a tunnel with the --domain flag, the ngrok client authenticates, establishes the outbound connection, and tells the edge to forward requests for that exact host to your local process. Because ngrok terminates TLS at the edge, the forwarded traffic is plain HTTP (or HTTPS if you configure TLS termination locally), satisfying the HTTPS requirement of most webhook providers without you managing certificates.
Implementation: Authenticating and Launching the Tunnel
First, obtain your authtoken from the ngrok dashboard and register it locally:
ngrok config add-authtoken YOUR_AUTHTOKEN
Replace YOUR_AUTHTOKEN with the token string. This step is required once per machine.
Assuming a local web server is listening on port 8000, start a tunnel bound to your reserved subdomain:
ngrok http 8000 --domain=dev-api-project.ngrok-free.app
The terminal will show a status screen similar to:
Forwarding https://dev-api-project.ngrok-free.app -> http://localhost:8000
You can now place that URL in the webhook configuration of your external service and leave it unchanged for the duration of the project.
Verification and Diagnostic Checks
Before relying on the tunnel for real payloads, verify connectivity from an external network (e.g., using your phone's cellular data or a colleague's machine):
curl -I https://dev-api-project.ngrok-free.app
A successful TLS handshake returns HTTP 200 if your local server responds, or HTTP 404 if the route exists but no matching handler is defined. Either outcome confirms the tunnel is active. If you see HTTP 502 Bad Gateway, the ngrok connection is up but the local process on port 8000 is not reachable—check that the server is running and not blocked by a local firewall.
Trade-offs and Practical Considerations
- Subscription requirement: Reserved subdomains are available on paid ngrok plans; free accounts typically receive only random URLs that change each session.
- Surface exposure: A static URL is a permanent entry point to your machine. If your local application lacks authentication, anyone who discovers the subdomain can send arbitrary requests. Mitigate this by adding ngrok's built-in basic auth (
--basic-auth=user:pass) or by implementing authentication in your application. - Dependency on ngrok's edge: The tunnel relies on ngrok's cloud infrastructure. An outage there will block inbound webhooks until service is restored.
Closing the Loop
By fixing the public endpoint with a custom subdomain, you eliminate the repetitive "webhook shuffle" and keep your integration tests predictable. Treat the reserved subdomain as part of your development environment—document it in your README, version-control the ngrok authtoken handling instructions, and consider automating the tunnel start in your IDE or docker-compose setup. This small change reduces configuration drift and lets you focus on the logic of your webhook handlers rather than the plumbing of network exposure.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.