The idea
A small self-hosted service that sits in front of your webhook endpoints, logs every inbound request verbatim (headers, raw body, timestamp, source), and gives you a UI to browse that history and replay any single event against a target URL with one click. It's the "request bin" idea taken further: instead of just viewing payloads, you can actually re-fire them at a fresh handler without waiting for the real event to recur.
Why build this
Webhook-driven integrations are miserable to develop against because the events that trigger bugs are rarely reproducible on demand — a Stripe dispute, a GitHub check_suite failure, a Shopify inventory sync edge case. Developers currently either poke around in each vendor's dashboard (if it even keeps a log) or write throwaway scripts to fake payloads that drift from what the vendor actually sends. A local capture-and-replay proxy fixes both problems: real payloads, replayable anytime, independent of any vendor's retention window. It's also useful for onboarding new handlers — point a webhook at the proxy for a day, then replay the captured traffic against a new implementation until it behaves.
Stack sketch
- Backend: Go with
net/http, one HTTP listener that accepts any path under/hooks/:idand stores the request - Storage: SQLite (via
mattn/go-sqlite3), one row per captured request — raw body as BLOB, headers as JSON, no parsing or interpretation at capture time - UI: server-rendered HTML with htmx for the list/detail/replay interactions, no SPA build step
- Replay: re-issues the stored request verbatim (method, headers, body) against a configurable target URL, with the option to swap only the host so signature headers stay valid
- Exposure: routed through the existing Traefik instance with its own subdomain and TLS, same pattern as other services in the stack
- Auth: single shared token in a header for both capturing and viewing, since this is a single-operator tool, not multi-tenant
Scope for v1
- Capture any POST to
/hooks/<endpoint-id>and persist it immediately, responding 200 before any processing so vendors never see a slow or failed capture - List view per endpoint, newest first, with method/status/timestamp/body size
- Detail view showing headers and pretty-printed body (JSON when possible, raw otherwise)
- One-click replay to a target URL set per endpoint (e.g.,
http://localhost:3000/webhooks/stripefor local dev) - Signature headers (Stripe-Signature, X-Hub-Signature-256, etc.) pass through untouched on replay
- Out of scope for v1: multi-tenant accounts, alerting/notifications, payload diffing, retention policies beyond a manual delete button, and any built-in tunneling — point real webhooks at the server's own public endpoint since it's already behind Traefik
Where it could go
The obvious next step is a "compare" view: replay the same event against two target URLs (old handler vs. new) and diff the resulting response codes and side effects, which turns this into a lightweight regression tool for webhook handlers. After that, transformation rules — tweak specific fields in a captured payload before replay, so a captured payment_intent.succeeded event can be mutated into edge cases (different currency, missing metadata) without hand-writing new fixtures. Longer term, a CLI companion (webhook-replay pull stripe --since 2h) that pulls captured events into a local fixtures directory would let teams commit real payloads into their test suites.
Watch out for
Captured bodies can contain live secrets and PII (customer emails, payment metadata), so this tool needs its own access control and should never be pointed at a shared or public network by default. It's also easy to fat-finger a replay target and accidentally send a real event at a production handler, causing duplicate charges or duplicate order processing — the UI should make the configured target environment (local/staging/prod) impossible to miss before the replay button is enabled.