Back to blog

Receiving Webhooks Locally with Hookbridge

webhookstutorialsproduct

You are building a webhook handler. Your application is running happily on localhost:3000, and a request from another terminal reaches it just fine. Then you try to connect Stripe, GitHub, or another webhook provider and hit an immediate problem: the provider needs a public URL, but your application only exists on your laptop.

You could deploy every change before testing it. You could open a port through your firewall. Neither makes for a pleasant development loop.

The Hookbridge CLI fills in the missing connection. It gives the provider a public Hookbridge URL, receives the webhook in the cloud, and then forwards the request to the application running on your computer.

The complete path looks like this:

Webhook provider
Public Hookbridge receive URL
Hookbridge stores the webhook
Hookbridge CLI on your laptop
Your local webhook handler

That final step is the useful part: your own application handles the request. You can exercise your real signature verification, database writes, background jobs, error handling, and everything else your deployed webhook handler will eventually do—without deploying it first.

In this tutorial, we will build a tiny local receiver, connect it to Hookbridge, and send a webhook through the complete path.

What You Need

You will need a Hookbridge account, an API key created in the Hookbridge console, and an application with an HTTP route that can accept a POST request.

We will use a small Node.js receiver so you can follow the tutorial from an empty directory. If you already have a webhook handler, use that instead and skip ahead to Install the Hookbridge CLI.

Start a Local Webhook Receiver

Create a directory and install Express:

mkdir webhook-demo
cd webhook-demo
npm init -y
npm install express

Create a file named server.mjs:

import express from "express";

const app = express();

app.post("/webhooks", express.raw({ type: "*/*" }), (req, res) => {
  const body = req.body.toString("utf8");

  console.log("Headers:", req.headers);
  console.log("Body:", body);

  res.status(200).send("OK");
});

app.listen(3000, () => {
  console.log("Listening on http://localhost:3000/webhooks");
});

We are reading the body as raw bytes deliberately. Providers commonly sign the exact bytes they send, so a production handler should verify the signature before parsing or changing the body. Hookbridge preserves the webhook body, headers, and content type when forwarding it to your local application.

Start the receiver:

node server.mjs

You should see:

Listening on http://localhost:3000/webhooks

This route works locally, but a webhook provider cannot reach it yet.

Install the Hookbridge CLI

On macOS or Linux with Homebrew, install the CLI with:

brew install hookbridge/tap/hb

Then verify the installation:

$ hb version
hb version v1.1.0

Prebuilt binaries for macOS, Linux, and Windows are also available from the Hookbridge CLI releases.

Log In

Run the login command and paste the API key you created in the Hookbridge console:

hb login

The CLI verifies the key and saves it in ~/.hookbridge/config.json. You only need to do this once. For a non-interactive environment, you can pass the key directly:

hb login --api-key "$HOOKBRIDGE_API_KEY"

Connect Hookbridge to Your Local Route

Leave the Node server running. In a second terminal, tell the CLI where it should forward incoming webhooks:

hb listen --forward http://localhost:3000/webhooks

On the first run, the CLI creates an endpoint and prints its public receive URL:

Creating CLI endpoint... done

Endpoint: CLI Endpoint (01a002f8-e0fa-787e-9ddb-59861fc6517c)

Webhook URL: https://receive.hookbridge.io/v1/webhooks/receive/01a002f8-e0fa-787e-9ddb-59861fc6517c/SECRET32

Paste this URL into your webhook provider's settings.
Forwarding to http://localhost:3000/webhooks
Ready. Waiting for webhooks...
Connected via WebSocket (real-time)

Copy the webhook URL when it is created. Treat the full URL as a credential: the secret at the end allows anyone who has it to send requests to the endpoint, so do not commit it to source control or publish it in logs.

The CLI has now opened an outbound connection to Hookbridge. You do not need to expose a port on your laptop or make your local server publicly accessible.

Send a Webhook Through the Complete Path

Before configuring a real provider, use curl to prove that every part is connected. In a third terminal, send a request to the public webhook URL printed by hb listen:

curl -X POST "YOUR_HOOKBRIDGE_WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d '{"event":"order.created","id":"evt_001"}'

Hookbridge accepts and queues the request:

{"data":{"message_id":"01a002f7-3068-7afb-94db-629e29faf2a3","status":"queued"},"meta":{"request_id":"CHpYUhJZCYcEP4Q="}}

In the terminal running hb listen, you will see the result returned by your local application:

21:11:06  POST  →  200  5ms  application/json  (40 bytes)

That line says the CLI forwarded a JSON POST containing 40 bytes, your local handler returned 200, and the local request took 5 milliseconds.

The terminal running the Node receiver will show the headers and body that reached your application:

Body: {"event":"order.created","id":"evt_001"}

You have now sent a webhook to a public cloud URL and handled it with code running only on your laptop.

You may have noticed two different status codes during this test. The original sender receives 202 Accepted after Hookbridge has safely queued the webhook. Your local application’s 200, 401, 500, or other response happens afterward. The CLI reports that local response so you can see how your handler behaved.

Replace curl with a Real Provider

Once the test request works, paste the same Hookbridge webhook URL into your provider’s webhook settings. It might be a Stripe endpoint, a GitHub repository webhook, a Shopify subscription, or any service that can send an HTTP webhook.

Then trigger an event in the provider. The path does not change:

  1. The provider sends the webhook to Hookbridge.
  2. Hookbridge stores it and streams it to the CLI.
  3. The CLI sends an HTTP POST to http://localhost:3000/webhooks.
  4. Your local application runs its normal webhook-handling code.
  5. The CLI displays the status and latency returned by your application.

From here, the development loop is short: edit your handler, restart your application when necessary, trigger another event, and immediately see how the new code responds.

The receive URL is stable across CLI sessions, so you do not have to update the provider every time you restart hb listen. Hookbridge shows the secret URL when it creates the endpoint; save it in your provider’s settings at that point.

See the Full Request with Verbose Mode

The default output stays compact so you can leave it running while you work. When a signature fails or a payload does not look the way you expected, restart the listener with -v:

hb listen --forward http://localhost:3000/webhooks -v

Each webhook will include its headers and formatted body:

21:09:58  POST  →  200  4ms  application/json  (55 bytes)
  Headers:
    accept: */*
    content-length: 55
    content-type: application/json
    user-agent: curl/8.7.1
    x-forwarded-for: 203.0.113.10
    x-signature: t=1,v1=abc123
  Body:
  {
    "event": "refund.created",
    "id": "evt_789",
    "amount": 1500
  }

This is especially useful when implementing signature verification. You can inspect the provider’s real signature header while your application verifies it against the original request body.

Verbose output may contain secrets or customer data, so be careful when copying it into an issue, chat, or build log.

Inspect Webhooks Before Your Application Exists

Sometimes you need to learn what a provider sends before you have written the handler. In that case, listen without forwarding:

hb listen --no-forward -v

The CLI will display incoming requests but will not try to send them to a local URL. This is useful for exploring payload shapes, identifying event types, and checking which headers a provider includes before deciding what your application needs to do.

When the handler is ready, restart the listener with --forward and point it at the correct route.

Debug Local Failures

The CLI makes local connection and application failures visible. If the target server is not running, for example, you will see an error instead of a status code:

21:10:18  POST  →  ERR  connection refused or timeout: Post "http://localhost:9999": dial tcp [::1]:9999: connect: connection refused

If your application responds but its code fails, you will see the status it returned:

21:14:32  POST  →  500  12ms  application/json  (328 bytes)

That feedback helps separate two common problems: a request that never reached your process and a request that reached it but failed inside the handler.

The webhook remains stored by Hookbridge even when the local handoff fails. The CLI also reconnects automatically if its WebSocket connection drops and falls back to polling when a real-time connection is unavailable. Events that arrive while the CLI is briefly disconnected can be delivered after it reconnects.

Common Ways to Use the CLI

The CLI is useful anywhere the real webhook needs to exercise code that has not been deployed yet.

  • Build a new integration. Send real provider events through parsing, validation, and business logic while you work locally.
  • Test signature verification. Receive the original body and provider headers instead of constructing an approximation by hand.
  • Debug response codes. See whether the handler returned 200, rejected the request with 401, failed with 500, or timed out.
  • Exercise side effects. Confirm that a webhook updates your development database, creates a background job, sends a test notification, or changes application state as expected.
  • Explore an unfamiliar API. Use --no-forward -v to inspect real events before building the receiver.
  • Test a route other than /. Use --forward for paths such as http://localhost:8080/webhooks/billing.
  • Automate integration tests. The CLI can produce an NDJSON event stream with --json, and ephemeral endpoints can expire automatically after a CI run.

For simple applications listening at the root of port 3000, the command is even shorter:

hb listen

Port 3000 is the default. You can use --port 8080 for another port or --forward when you need a complete URL with a specific path.

Stop Listening

Press Ctrl+C when you are finished:

Shutting down. 1 webhook(s) received.

Stopping the CLI closes the connection from your laptop. It does not expose your local server and it does not require you to remove a firewall rule or shut down a public tunnel.

A Faster Webhook Development Loop

The Hookbridge CLI does not replace your application or simulate the webhook handler. It connects a real webhook to the real code you are developing locally.

Instead of deploying a half-finished handler just so a provider can reach it, you can keep the entire feedback loop on your laptop: make a change, trigger an event, inspect the request, and see the response. When the handler behaves correctly, deploy it with considerably fewer unknowns.

Install the CLI with brew install hookbridge/tap/hb, or download a binary from GitHub Releases. For more detail on the available commands, visit the Hookbridge CLI overview.