How to test webhooks locally with ngsrv
ngsrv helps developers expose local services, share preview links, test webhooks, and get feedback faster. Webhook testing is the use case where the difference is loudest — instead of deploying, redeploying, and watching logs, you keep everything on your laptop with full breakpoints.
This guide walks through testing webhooks from any provider (Stripe, GitHub, Shopify, Slack, Twilio, Discord) without leaving your editor.
Why local webhook testing is painful by default
Webhooks need a public HTTPS URL. Your local handler doesn't have one. The usual workarounds:
- Deploy to a staging server every time — minutes per round trip, no debugger.
- Use a request bin — fine for inspecting payloads, useless for actually running your code.
- Use a generic tunnel — works, but you get a new random URL every restart, so you re-paste it into the provider's dashboard ten times a day.
ngsrv gives you a stable public URL (reserved subdomain or custom domain) that forwards to your local handler. Set it once, debug forever.
Step 1 — Run your webhook handler locally
Whatever your stack:
# Node.js / Next.js
npm run dev # usually port 3000 or 4242
# Python / FastAPI
uvicorn main:app --port 4242
# Go
go run ./cmd/server # default 8080
For the rest of this guide we'll assume port 4242 because Stripe defaults its CLI listener there.
Step 2 — Start the tunnel with a reserved subdomain
ngsrv http 4242 --subdomain hooks
You'll get a stable URL:
https://hooks.tnl.ngsrv.com -> http://localhost:4242
Reserved subdomains live across restarts on Pro and above. On the free tier you get a random subdomain per session.
Step 3 — Point the webhook provider at the URL
Stripe
In the Stripe dashboard, go to Developers → Webhooks → Add endpoint:
https://hooks.tnl.ngsrv.com/webhook
Select the events you care about (e.g. checkout.session.completed, invoice.paid). Save.
GitHub
In a repo, go to Settings → Webhooks → Add webhook:
Payload URL: https://hooks.tnl.ngsrv.com/github
Content type: application/json
Pick "Send me everything" while developing.
Shopify, Slack, Twilio, Discord
All the same shape: paste https://hooks.tnl.ngsrv.com/<your-handler-path> into the provider's webhook configuration.
Step 4 — Trigger an event and debug
Use the provider's "send test event" button, or trigger a real one (stripe trigger checkout.session.completed). The event hits ngsrv, lands at your local handler, and you can set breakpoints inside your IDE.
Locking it down
You probably want only the webhook provider to reach this URL while you're testing. Add a security policy to your ngsrv.yml:
tunnels:
- name: webhooks
port: 4242
subdomain: hooks
security_policies:
- allow-stripe-ips
Where the policy lives in your dashboard and ships with Stripe's published IP ranges, or use a header_required policy and a signed token. All of these are on the free tier.
FAQ
Can I test Stripe webhooks with ngsrv? Yes. Run ngsrv http 4242 --subdomain hooks and paste https://hooks.tnl.ngsrv.com/webhook into Stripe's webhook endpoints page.
Does ngsrv work with the Stripe CLI? Yes. Either tunnel directly to your app, or tunnel to the Stripe CLI listener — both work because ngsrv is just an HTTP forwarder.
Can I test GitHub webhooks locally? Yes. The flow is identical: point GitHub's webhook URL at your ngsrv subdomain.
Will the URL change between restarts? Not with a reserved subdomain. Free-tier subdomains are random per session; on Pro and above you reserve hooks.tnl.ngsrv.com (or whatever you pick) and keep it.
How do I test webhooks without deploying? That's exactly what this page is. ngsrv http <port> + your local handler + the provider's "send test event" button.