Documentation

Webhook Proxy relays HTTP requests from the internet to your local server and returns the real response to the caller — no port forwarding required.

Overview

When an external service (a payment provider, a CI system, a SaaS webhook) needs to reach your local development server, Webhook Proxy stands in the middle:

  1. You create a channel — a permanent public URL tied to a WebSocket tunnel.
  2. Your local process opens a WebSocket to the tunnel endpoint and listens for forwarded requests.
  3. The external service POSTs (or GETs, PATCHes, …) to your channel URL. The proxy holds the HTTP connection open, forwards the request over the tunnel, waits for your local server's response, and returns it to the caller — including status code, headers, and body.

The channel URL is stable for the lifetime of the session. Reconnecting to the tunnel resumes the same channel.

Quickstart

1. Create a channel

Click Create a channel on the home page, or navigate to /new. You will be redirected to a unique channel page like /abc1234xyz.

2. Connect your local server

Download the reference TypeScript client from the link shown on the channel page, then run it:

CHANNEL_URL=wss://webhook-proxy.manuelt.de/abc1234xyz/_tunnel \
TARGET=http://localhost:3000 \
npx tsx client.ts

The client will print Tunnel connected. and the status badge in the UI will turn green.

3. Point your webhook source at the channel URL

Use the channel URL shown in the sidebar (e.g. https://webhook-proxy.manuelt.de/abc1234xyz) as the webhook destination in the external service. All HTTP methods and sub-paths are supported — /abc1234xyz/events/push works the same as /abc1234xyz.

4. Inspect requests in real time

Incoming requests appear in the request list as they arrive. Click one to see full request and response headers and bodies. Use ↺ Replay to re-send any past request through the tunnel.

Channel UI

The channel page is split into three panes:

  • Sidebar (left) — channel URL, tunnel WebSocket URL, configuration controls (auth, auto-respond, test request).
  • Request list (middle) — live feed of all relayed requests with method, path, status, and timestamp. Click to select.
  • Detail panel (right) — full inspection of the selected request: method, path, status, duration, request headers, request body, response headers, response body. Replay and copy-as-curl actions.

The tunnel badge in the top-right of the header reflects the current connection state of the tunnel WebSocket in real time.

Auth Header

You can require an arbitrary HTTP header on all incoming requests. Enable Require auth header in the sidebar, enter the header name and expected value, and click Save.

Requests that are missing the header or provide the wrong value will receive a 401 response immediately — they will not be forwarded to the tunnel.

The auth setting lives in server memory and resets when the server restarts. It is per-channel and takes effect immediately after saving.

Auto-respond

Enable Reply 204 No Content to make the proxy immediately return 204 to every incoming request without forwarding it to the tunnel. This is useful for silencing a webhook source while you are not actively debugging.

Requests are still logged in the request list so you can see what came in.

Test Requests

The Test Request panel in the sidebar lets you send an artificial request through the tunnel directly from the browser — no external service needed.

  • Choose an HTTP method from the dropdown.
  • Enter a path (defaults to /).
  • For methods that carry a body (POST, PUT, PATCH, DELETE), a body textarea appears. If you paste valid JSON it is highlighted and content-type: application/json is added automatically.
  • Click Send. The request appears in the list and is forwarded to your local server.

TypeScript Client

The reference client is a single-file TypeScript module with no external dependencies (requires Node.js 22+). Download it from the /_client.ts link on any channel page.

Programmatic usage

import { WebhookProxy } from "./client.ts";

const proxy = new WebhookProxy({
  source: "wss://webhook-proxy.manuelt.de/abc1234xyz/_tunnel",
  target: "http://localhost:3000",
});

proxy.start();

// Later, to disconnect:
proxy.close();

Constructor options

Option Type Default Description
source string required The tunnel WebSocket URL — found on the channel page under Tunnel WebSocket. Always ends with /_tunnel.
target string required Base URL of your local server (e.g. http://localhost:3000). All request paths are resolved relative to this URL.
reconnectDelay number 3000 Milliseconds to wait before reconnecting after an unexpected disconnect. Set to a higher value if you want less aggressive reconnection.

Methods

MethodDescription
start() Opens the tunnel WebSocket and begins forwarding requests. Automatically reconnects on disconnect until close() is called.
close() Closes the WebSocket and stops reconnecting.

CLI usage

The client can be run directly as a CLI tool using environment variables:

CHANNEL_URL=wss://webhook-proxy.manuelt.de/abc1234xyz/_tunnel \
TARGET=http://localhost:3000 \
npx tsx client.ts
VariableRequiredDescription
CHANNEL_URL yes Tunnel WebSocket URL (same as the source option).
TARGET no Local server base URL. Defaults to http://localhost:3000.

The process handles SIGINT and SIGTERM for clean shutdown (Ctrl-C works as expected).

How the client forwards requests

For each request received over the WebSocket, the client:

  1. Resolves the request path against target.
  2. Strips hop-by-hop headers (host, connection, transfer-encoding) and recalculates content-length from the decoded body.
  3. Makes a standard Node.js http/https request to your local server.
  4. Encodes the response (status, headers, base64 body) and sends it back over the WebSocket so the proxy can return it to the original caller.
  5. If the local request fails, returns a 502 with a JSON error body rather than leaving the caller hanging.

Protocol

The tunnel is a plain WebSocket connection. You can implement a client in any language that supports WebSockets.

Tunnel WebSocket endpoint

wss://<host>/<channelId>/_tunnel

Only one tunnel connection per channel is active at a time. Opening a second connection closes the first.

Message format

All messages are JSON. The server sends request messages; the client sends back response messages.

Incoming (server → client):

{
  "type": "request",
  "id": "unique-request-id",
  "method": "POST",
  "path": "/events/push",
  "headers": { "content-type": "application/json", ... },
  "body": "<base64-encoded body, or null>"
}

Outgoing (client → server):

{
  "type": "response",
  "id": "unique-request-id",  // must match the request id
  "status": 200,
  "headers": { "content-type": "application/json", ... },
  "body": "<base64-encoded body, or null>"
}

Request bodies are base64-encoded to safely transport binary payloads. If there is no body, body is null.

Timeouts and error responses

If the proxy does not receive a matching response within 30 seconds it returns 504 to the original caller. If no tunnel is connected when a request arrives, the caller receives 503 immediately.

UI WebSocket endpoint

wss://<host>/<channelId>/_ui

The channel page connects here to receive live events. The server pushes tunnel_status, request, and response messages as they happen. This endpoint is read-only — the UI sends no messages.