A self-hostable gateway that captures, retries, and replays inbound webhooks.

       

Getting started · Documentation · Live demo · Contributing


whook sits in front of your application. Providers point at it instead of at you. It captures every inbound webhook the instant it arrives, returns a fast 2xx so the provider is happy, then forwards the event to one or more destinations with automatic retries. Every request is stored durably, so you can list, inspect, and replay anything that came in.

Stripe sends a webhook to whook, which returns a fast 200, saves the event to a durable store, and delivers it to your app

What you get

  • Durable capture before the provider is acknowledged, so no event is ever lost.
  • Exponential-backoff retries with a budget, then dead-letter for what never lands.
  • Fan-out to many destinations, each with its own filter, retries, and status.
  • A durable record of every request, queryable and replayable from an API or UI.
  • Pluggable signature verification (Stripe, GitHub), idempotent capture, metrics.
  • One static Go binary. SQLite built in, Postgres when you outgrow it.

The problem

Services like Stripe, GitHub, and Shopify notify you by sending an HTTP POST to a URL you give them. A payment succeeds, a pull request opens, an order is placed, and they POST the details to your server. That POST is the webhook, and it arrives exactly once, at a moment you do not control.

Point a provider straight at your application and it is fragile:

  • If your app is down, deploying, or briefly crashing when the webhook lands, the event is lost. Many providers retry weakly or not at all.
  • When processing fails, the original request is already gone, so debugging is blind.
  • A provider usually allows one destination URL, but billing, email, and analytics may all need the same event, which forces hand-written fan-out glue.
  • There is no simple way to replay a past webhook to reproduce a bug or recover.
Without a gateway, a webhook sent while your app is deploying breaks, the event falls away, and it is lost with no retry and no record

How it works

Capture is decoupled from delivery. An inbound request is saved durably before it is acknowledged. Delivery happens afterward, on whook's own schedule, so a destination being down never costs you an event.

Provider to ingest with a signature gate and a fast 2xx ack, to a durable store, a router, delivery workers, and your services, with dead-letter and replay branches

Reliable delivery. A failed delivery is retried on a deterministic exponential schedule (a 429 honors its Retry-After). When the retry budget is exhausted the delivery is dead-lettered, so it stops consuming resources and becomes visible for inspection and replay.

A failed delivery is retried with growing backoff gaps until one returns 200, or the budget is exhausted and it is dead-lettered

Fan-out. One captured event resolves to every matching destination as an independent delivery track, each with its own filter and status. A failing destination never blocks the others.

One event delivered to billing, email, and analytics as independent tracks, each with its own filter and status

Getting started

Install

Docker (prebuilt multi-arch image, recommended):

docker run -p 8080:8080 -v whook-data:/data ghcr.io/edaywalid/whook:latest

Docker Compose (builds the full stack from source):

cp .env.example .env   # set WHOOK_ADMIN_TOKEN and WHOOK_SECRET_KEY
docker compose up --build -d

From source (Go 1.25+):

go install github.com/edaywalid/whook/cmd/whook@latest
whook

The gateway listens on http://localhost:8080 and stores events in a local SQLite file by default. See Deployment for Postgres and scaling.

Send your first webhook

# 1. Register a source (the provider integration)
curl -X POST localhost:8080/sources -d '{"name":"stripe"}'

# 2. Point it at one or more destinations
curl -X POST localhost:8080/destinations \
  -d '{"source":"stripe","url":"https://your-app.example.com/webhooks"}'

# 3. Send a webhook to the gateway
curl -X POST localhost:8080/ingest/stripe \
  -H 'Content-Type: application/json' \
  -d '{"type":"payment.succeeded","amount":4900}'

# 4. Inspect captured events
curl localhost:8080/events

Open http://localhost:8080/ui for the dashboard: it lists events, links to a per-event detail page (payload, delivery state, attempt history, replay button), and has a dead-letter view.

Documentation

  • Configuration: every environment variable, auth, secrets, retention, and rate limiting.
  • HTTP API: endpoints, sources, destinations, the filter spec, and replay.
  • Deployment: Docker, the GHCR image, Compose, building from source, and Postgres.
  • Contributing: how to build, test, and submit changes.

Tech stack

  • Go, single static binary
  • SQLite by default (pure-Go driver, no cgo) or Postgres for scale, behind one storage interface
  • Standard library HTTP, server-rendered dashboard
  • dbmate-authored migrations, embedded and applied on startup
  • Landing page in web/ (TanStack Start, React 19, deployed at whook-gateway.netlify.app)