· NGSRV Team

How to test webhooks locally without deploying

ngsrv creates a public preview link for your local service. The single biggest place that matters is webhook testing — instead of deploying to staging every time you tweak a handler, you forward the real provider's webhook straight to your laptop and debug with breakpoints.

ngsrv helps developers expose local services, share preview links, test webhooks, and get feedback faster. This is the long version of the webhook story.

What "testing webhooks locally" actually requires

A webhook is just an HTTP POST from a provider (Stripe, GitHub, Shopify, Slack, Twilio, etc.) to a URL you gave them. To run that POST against your local code, you need:

  1. A public HTTPS URL the provider can reach (no provider hits localhost).
  2. A stable URL so you don't re-paste it after every restart.
  3. Optional auth to keep randoms out.
  4. Logs you can actually read.

ngsrv ships all four. Here's the full setup.

Step 1 — Run your webhook handler locally

Pick a port. We'll use 4242 because that's the Stripe CLI's default forwarding port.

# Node / Next.js API route
npm run dev

# Python / FastAPI
uvicorn main:app --port 4242

# Go
go run ./cmd/server  # set PORT=4242

Make sure the handler logs requests so you can sanity-check.

Step 2 — Start the tunnel with a reserved subdomain

ngsrv http 4242 --subdomain hooks
# -> https://hooks.tnl.ngsrv.com

Reserved subdomains stay yours between sessions on Pro and above. This is the part that matters: Stripe / GitHub / Shopify webhook URLs hate changing, so you set this once and keep it for the life of the project.

Step 3 — Point providers at the URL

Stripe

In the Stripe dashboard:

  • Developers → Webhooks → Add endpoint
  • URL: https://hooks.tnl.ngsrv.com/webhook (or whatever path your handler uses)
  • Events: pick the ones you care about (checkout.session.completed, invoice.paid, …)

Stripe will send a test event. Watch it land in your handler.

GitHub

For a repo:

  • Settings → Webhooks → Add webhook
  • Payload URL: https://hooks.tnl.ngsrv.com/github
  • Content type: application/json
  • Secret: (set one — see below)
  • Pick "Just the push event" or "Send me everything" while developing.

Shopify, Slack, Twilio, Discord

All the same shape. URL goes into the provider's webhook configuration.

Step 4 — Verify signatures

Real webhook handlers should verify signatures. ngsrv passes the original request headers through untouched, so:

  • Stripe: verify Stripe-Signature against your endpoint secret.
  • GitHub: verify X-Hub-Signature-256 against the webhook secret.
  • Slack: verify X-Slack-Signature against your signing secret.

None of this changes because of ngsrv. The bytes the provider sent are the bytes your handler receives.

Step 5 — Filter who can reach the URL

While developing, restrict the endpoint to the provider's IPs or require a header:

tunnels:
  - name: webhooks
    port: 4242
    subdomain: hooks
    security_policies:
      - type: ip_allowlist
        ips:
          # Stripe webhook IPs (check Stripe docs for the current list)
          - "3.18.12.63/32"
          - "3.130.192.231/32"
          # ... etc

Or pair it with a header check:

security_policies:
  - type: header_required
    header: "Stripe-Signature"

Both policies are free-tier.

Step 6 — Read the logs

ngsrv emits structured JSON access logs:

{
  "ts": "2026-05-18T19:42:11Z",
  "tunnel": "webhooks",
  "method": "POST",
  "path": "/webhook",
  "status": 200,
  "duration_ms": 42,
  "req_bytes": 1822,
  "headers": {
    "stripe-signature": "t=...,v1=..."
  }
}

Pipe to jq for tidy output:

ngsrv run ngsrv.yml | jq -r 'select(.tunnel == "webhooks") | "\(.status) \(.method) \(.path) \(.duration_ms)ms"'

This is one of the actual operational reasons to prefer a tunneling tool with stable JSON logs.

What this saves you

| | Deploy-to-staging loop | ngsrv + local handler | | --- | --- | --- | | Time per change | 5–15 minutes (CI + deploy) | Hot-reload (seconds) | | Debugger | None | Full IDE breakpoints | | Inspecting request body | Read prod logs | Read in your own process | | Cost | Staging server + CI minutes | Free tier of ngsrv |