Blog

How to test webhooks locally without redeploying

· SyncYak team

  • tunnels
  • webhooks
  • local-development

You are building a webhook handler and the sender will only talk to a public HTTPS URL. Your code runs on localhost. The usual workaround is to deploy a half-finished handler to staging, trigger an event, read the logs, change one line and deploy again. Each loop takes minutes, and you still do not see what the sender actually posted.

A tunnel removes that loop. It gives your local port a public HTTPS address, so the provider calls your laptop directly and you debug with your own editor and breakpoints. This guide covers how to set that up, what to check in the first requests, and how to keep the setup stable from one day to the next.

Why webhooks are awkward to develop locally

Three things make webhooks harder than a normal API call:

  • The sender initiates. Stripe, GitHub and most CRMs call you. Nothing can reach localhost:3000 from outside your network.
  • The sender wants HTTPS. Most providers refuse plain HTTP endpoints for anything beyond a throwaway test.
  • The payload is the bug. When a handler fails, the cause is usually a field you did not expect, a header you ignored or a retry you did not plan for. You need the raw request, not your interpretation of it.

A tunnel solves the first two. A request inspector solves the third.

Step 1: Start your handler and the tunnel

Run your handler on a port, then forward that port. With the SyncYak CLI:

$ syncyak http 3000
Forwarding https://quiet-yak-4821.syncyak.cloud
        to http://localhost:3000

Everything sent to the public address arrives at http://localhost:3000 until you stop the command with Ctrl+C. The CLI runs on Windows, macOS and Linux.

A random address is fine for a one-off check. If you will be back tomorrow, reserve a name instead (see step 4).

Step 2: Register the public URL with the sender

Paste the address plus your handler's path into the provider's webhook settings. For example, if your route is /webhooks/stripe, the endpoint is:

https://quiet-yak-4821.syncyak.cloud/webhooks/stripe

Then trigger a real test event. Most providers have a "send test webhook" button or a CLI command to fire a sample event. Stripe's webhook documentation describes the events and the signature header it sends with each one.

Step 3: Read the first request before you write more code

Open the request inspector and look at the first request the sender made. Check these things in order:

  1. The path and method. A 404 or 405 almost always means the route in your app does not match the URL you registered.
  2. The headers. Note the signature header and any event or delivery ID. You will need both.
  3. The raw body. Compare it with what your parser produced. Signature checks run over the exact bytes the sender posted, so a framework that re-serializes JSON before you verify can make every valid request fail.
  4. Your response. Webhook senders treat anything other than a quick 2xx as a failure and retry. If your handler does slow work, acknowledge first and process afterward.

The inspector lists method, path, status and duration for each request, with the full request and response including headers and bodies. That is the evidence you would otherwise reconstruct from server logs. How long requests are kept depends on your plan: 24 hours on Free, 72 hours on Hobby, 7 days on Pro and 30 days on Business.

Step 4: Keep the address across restarts

A random address changes every time you restart the tunnel, which means editing the webhook URL in every provider each morning. Reserve a subdomain instead:

$ syncyak http 3000 myapp.syncyak.live
Forwarding https://myapp.syncyak.live
        to http://localhost:3000

Configure https://myapp.syncyak.live/webhooks/stripe once and it keeps working whenever you start the tunnel again. Reserved subdomains are available from the Hobby plan.

Step 5: Test the failure cases on purpose

The happy path is the easy part. Before you call the handler done, deliberately try these:

  • A duplicate delivery. Providers can send the same event more than once. Your handler should recognize an event ID it has already processed and return 2xx without repeating the work.
  • An invalid signature. Edit a byte of the secret in your local configuration and confirm the handler rejects the request with a 4xx.
  • A slow or failing response. Return a 500 once and confirm that the sender's retry arrives and is handled correctly. The inspector shows each attempt as a separate request.
  • An out-of-order event. An "updated" event can arrive before the "created" one. Check that your code does not assume order.

Doing this against a local process is faster than doing it on staging, because you can stop on the exact line that fails.

Tunnels for more than Stripe

The same setup works for any service that calls your server:

  • OAuth callbacks. Register the public HTTPS address as the redirect URI so the login flow completes against your local app.
  • CRM events. Receive Follow Up Boss or other CRM webhooks while you build the consumer.
  • Client demos. Send a client a link to a build running on your machine, and open it on your own phone to check mobile layout.

How SyncYak helps

SyncYak Tunnel puts a local port on a public HTTPS address with syncyak http 3000, keeps every request and response in the inspector, and lets you reserve a subdomain so webhook settings survive restarts. The Free plan includes 1 tunnel and 2 GB of traffic a month, which covers most webhook development. Hobby adds 3 tunnels, 3 reserved subdomains and 25 GB a month for $8 a month. See /pricing for every plan.

If you are specifically sending test events to a handler, the webhook testing page walks through the same flow. Agencies running one tunnel per client can read tunnels for agencies.

Install the CLI, forward your handler's port and send your first test event: create a free SyncYak account.

Start free with SyncYak · All posts