Testhooks: A Self-Hosted Webhook Testing Tool
Why I built a lightweight, self-hostable alternative to webhook.site -- and how it works. Capture, inspect, transform, and forward webhooks in real time with a single binary.
- Testhooks is a self-hostable replacement for webhook.site -- capture, inspect, transform, and forward webhooks in real time.
- Ships as a single Go binary with the SPA embedded. Point it at a PostgreSQL database and go.
- Two modes: Server mode (persistent, always-on) and Browser mode (zero storage, privacy-first).
- Supports WASM transforms (JavaScript, Lua, Jsonnet), browser-side forwarding to localhost, and custom response scripting.
The Problem
If you have ever integrated a third-party service that sends webhooks — payment providers, CI/CD pipelines, messaging platforms — you know the pain. You need a publicly accessible URL to receive the callback, a way to inspect the payload, and some mechanism to forward it to your local development server.
The typical options are:
- webhook.site — great, but it is a SaaS. Your payloads go through someone else’s infrastructure. For many teams, that is a non-starter.
- ngrok — exposes your local port to the internet, but requires a CLI daemon, often has rate limits, and still does not give you a persistent capture UI.
- RequestBin — the original was shut down. Alternatives exist but are often unmaintained or over-engineered.
I wanted something simpler: a tool I could self-host, run on my own infrastructure, and trust with sensitive webhook payloads. That is why I built Testhooks.
What Testhooks Does
Testhooks gives you unique webhook URLs. Any HTTP request sent to those URLs is captured and streamed to your browser in real time via WebSocket. From there, you can inspect headers, body, query parameters — everything.
webhook sender --> https://hooks.example.com/h/a1b2c3d4 --> your browser (live)
But capture is just the beginning. Here is the full feature set:
Unique Webhook URLs
Each endpoint gets a short URL (/h/a1b2c3d4). Create as many as you need, each isolated with its own configuration.
Real-Time Streaming
Requests are pushed to your browser instantly via WebSocket. No polling, no refresh — you see webhooks the moment they arrive.
WASM Transforms
Transform payloads using JavaScript (QuickJS on server), or JavaScript + Lua + Jsonnet in the browser. Built-in CodeMirror editor for writing scripts.
Forwarding
Forward webhooks to localhost via browser-side fetch() (no ngrok needed) or to public URLs from the server — reliable, headless forwarding.
Two Modes: Server vs. Browser
Testhooks has two fundamentally different endpoint modes, and choosing the right one depends on your use case.
Server Mode
Requests are stored in PostgreSQL. Transforms and forwards run headlessly on the Go server — even when no browser is open. Ideal for always-on integrations and CI pipelines.
Browser Mode
Zero storage. Payloads never touch the server’s disk — all processing happens client-side in the browser. Perfect for sensitive payloads and quick debugging sessions.
Why Browser Mode Matters
Browser mode is what sets Testhooks apart from most alternatives. When you are debugging a payment webhook that contains PII or financial data, you do not want that payload stored on a server — not yours, and definitely not a third party’s. Browser mode streams the raw request over WebSocket and processes everything in the browser. When you close the tab, the data is gone.
Getting Started
The fastest way to get Testhooks running is Docker Compose. It starts the application and a PostgreSQL instance together.
Clone and start
Clone the repository and start everything with a single command:
git clone https://github.com/sarathsp06/testhooks.git
cd testhooks
docker compose up --buildCreate an endpoint
Open http://localhost:8080 in your browser. Create a new endpoint — you will get a unique URL like /h/a1b2c3d4. Choose Server mode or Browser mode depending on your needs.
Send a webhook
Point your webhook sender at the generated URL. The request will appear in your browser instantly.
curl -X POST http://localhost:8080/h/a1b2c3d4 \
-H "Content-Type: application/json" \
-d '{"event": "payment.completed", "amount": 42.00}'Inspect, transform, forward
View full request details (headers, body, query params). Optionally write a transform script to reshape the payload, or set up forwarding to relay it to your local dev server.
Architecture
Testhooks is designed to be minimal in dependencies and simple to deploy.
| Layer | Technology | Notes |
|---|---|---|
| Backend | Go (stdlib net/http) | Single binary, SPA embedded via go:embed |
| Frontend | Svelte 5 (SvelteKit SPA) | Static adapter, no SSR |
| Database | PostgreSQL | JSONB for request payloads, auto-migrations |
| Real-time | WebSockets (nhooyr.io/websocket) | Per-endpoint streaming |
| WASM (server) | QuickJS (fastschema/qjs) | Pooled JS execution for transforms |
| WASM (browser) | QuickJS + Lua + Jsonnet | Client-side transform engines |
The single-binary design is intentional. There are no static files to serve separately, no separate frontend deployment, no CDN configuration. Build the binary, point it at PostgreSQL, and it works. This makes it trivial to deploy on any PaaS (Heroku, Railway, Render) or just run it on a VPS behind a reverse proxy.
Replacing ngrok for Webhook Development
One of the most common use cases for ngrok is webhook development: you need a public URL that tunnels to your local machine so third-party services can reach your development server. Testhooks replaces this workflow without requiring a CLI or daemon.
Here is how it works:
- Deploy Testhooks to a public server (or use the hosted instance at testhooks.sarathsadasivan.com).
- Create an endpoint and point your webhook sender at it.
- In the Testhooks UI, set up browser-side forwarding to
http://localhost:3000(or wherever your dev server runs). - When a webhook arrives, your browser receives it via WebSocket and immediately
fetch()es it to your local server.
No tunnel, no port exposure, no CLI running in the background. The browser acts as the bridge between the public internet and your local machine.
Browser-side forwarding only works when your browser tab is open. For production-like scenarios or CI pipelines, use server-side forwarding instead — it runs headlessly on the Go server and reliably forwards to any public URL, even when no browser is connected.
Closing Thoughts
Testhooks started as a weekend project born out of frustration with existing tools. I wanted something that was self-hostable, privacy-respecting, and dead simple to deploy. The single-binary approach with an embedded SPA turned out to be the right call — it eliminates an entire class of deployment complexity.
The project is open source under the Apache 2.0 license. If you work with webhooks regularly — and most backend engineers do — give it a try. Contributions and feedback are welcome.