π¬ WebhookHub is a lightweight, self-hosted service for receiving, logging, and forwarding webhooks.
Use it to debug, inspect, replay, and route incoming webhooks during development or in production. No third-party services, no cloud lock-in β just full control.
When working with external services (Stripe, GitHub, Telegram, Shopify, etc.), developers often face the same pain points:
- β Where did the webhook go? Why didnβt my service receive it?
- π How do I replay a webhook for debugging or recovery?
- π How do I inspect payloads and headers easily?
- π‘ How can I fan-out one webhook to multiple services?
WebhookHub provides a simple, developer-friendly solution to these problems.
- β
Receive webhooks at
/hook/:source - β Log full payloads, headers, timestamps
- β Replay any webhook via Web UI
- β Forwarding rule per source
- β Optional incoming/outgoing HMAC signing (Stripe-style header format)
- β Web dashboard with filters, pagination
- β Secure login (admin account)
- β Postgres + GORM backend
- β Dockerized and ready to deploy
- Accept and log webhooks
- View logs with filters and pagination
- Replay webhooks on demand
- Add/edit/delete forwarding rules
- Delete individual webhook logs
- Admin auth (session cookie + bcrypt)
- PostgreSQL + GORM backend
- Docker + compose setup
- HMAC signature verification (e.g., Stripe-style)
- Delivery status tracking + metrics
- Dead-letter queue
- Configurable retry/backoff policy
- Dead-letter queue management UI
- Advanced search and filters
- Retention / cleanup policies
- Explicit database error handling
- Recovery of interrupted deliveries with database leases
- Bounded delivery worker pool and graceful shutdown
- Request body limits and source/target validation
- Required secure configuration with fail-fast validation
- HTTP method restrictions and CSRF protection
- Liveness and readiness endpoints
- CI and critical unit/integration tests
- Export and bulk redelivery tools
- Telegram integration
- OpenAPI schema
- Plugin system for custom processors
| Component | Technology |
|---|---|
| Language | Go |
| Database | PostgreSQL (via GORM) |
| UI | HTML + HTMX |
| Auth | SecureCookie + bcrypt |
| Container | Docker + Compose |
git clone https://github.com/Paramoshka/WebhookHub.git
cd webhookhub
cp .env.sample .env
# Fill every required value in .env before starting the service.
docker-compose up -d --buildWebhookHub uses a 32-byte secret key to sign session cookies.
You must set this in your .env file as SESSION_KEY.
To generate a secure random key:
openssl rand -hex 32WebhookHub validates configuration at startup and exits if a required value is missing or unsafe.
Required values:
ADMIN_EMAILADMIN_PASSWORDβ at least 12 charactersSESSION_KEYβ at least 32 characters; generate it withopenssl rand -hex 32POSTGRES_HOST,POSTGRES_PORT,POSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB
Production deployments behind HTTPS should set COOKIE_SECURE=true. PostgreSQL TLS can be configured with POSTGRES_SSLMODE (default: disable).
ADMIN_EMAIL and ADMIN_PASSWORD are authoritative bootstrap credentials: on startup WebhookHub creates the single admin or updates that account to match the configured values.
Runtime limits and delivery workers:
PORT=8080
MAX_BODY_BYTES=1048576
DELIVERY_WORKERS=4
DELIVERY_POLL_INTERVAL=1s
DELIVERY_LEASE_DURATION=30s
SHUTDOWN_TIMEOUT=10sDelivery is at-least-once. A database lease lets another worker recover a webhook left in processing after a crash. A duplicate remains possible if the target accepted a request but WebhookHub stopped before persisting the result.
Replay and delete requests for a webhook currently in processing are rejected with HTTP 409 Conflict so an active delivery cannot be changed underneath a worker.
Health endpoints:
GET /healthzreports that the process is running.GET /readyzreports readiness only when PostgreSQL responds.- The container healthcheck runs
webhookhub healthcheck, which checks/readyzwithout requiring shell utilities in the image.
Open Inspect from the logs, DLQ, or metrics to view /webhooks/{id}. The page
shows request headers, JSON with Formatted/Raw views, copy controls, and delivery
history. Status, attempts, and the latest saved response refresh every five
seconds without replacing the request payload. Replay queues a new delivery;
it does not mean the receiver has accepted the webhook yet.
Download payload (GET /webhooks/{id}/payload, login required) saves the exact
stored bytes. Binary bodies show a hex preview of up to 256 bytes. Clipboard
access depends on browser permissions and a secure context (HTTPS or localhost);
if copying is unavailable, the page provides a manual-copy message. Existing
/partials/webhook/{id} links continue to open the full detail page.
Retention cleanup is optional and disabled by default. Configure via .env:
# Retention cleanup
RETENTION_ENABLED=false
RETENTION_DAYS=30
RETENTION_INTERVAL=24h
RETENTION_BATCH_SIZE=300When enabled, expired webhooks are removed in batches every interval by received_at.
Active deliveries and queued retries are retained. Cleanup locks each batch before
deleting it: a webhook successfully requeued first is retained; replay returns
404 if cleanup deleted the webhook first.
/dashboard includes advanced filters in the UI and the same parameters are available via:
GET /partials/webhooks
Query params:
source: exact match on webhook source.status: exact match on status (pending,processing,retrying,success,failed,skipped,dead_lettered).q: full-text search acrosssource, payload, headers, last error, and DLQ reason.from: lower bound forreceived_at(supportsRFC3339,RFC3339Nano,2006-01-02T15:04,2006-01-02).to: upper bound forreceived_at(supports the same formats asfrom; date-only values are interpreted as end-of-day).sort: one ofid_desc(default),received_desc,received_asc,id_asc.page: page number (default1), 10 items per page.
Automatic refresh preserves the current page and applied filters. Apply and Reset start at page 1; Reset clears the filters.
Payload search is case-insensitive text matching over UTF-8 content, including
existing events. Binary payloads and bodies containing zero bytes are excluded
from payload text matching, but remain searchable by their other fields and are
stored and forwarded unchanged. Startup creates or replaces the PostgreSQL
function webhookhub_payload_text(bytea); the database user must have permission
to create functions in the application's schema and own the function on upgrades.
Example:
curl "http://localhost:8080/partials/webhooks?source=stripe&status=failed&q=payment&from=2026-07-01&to=2026-07-11T23:59&sort=received_desc&page=2"Copyright (C) 2025-2026 Ivan Parfenov.
WebhookHub is available under two licensing options:
- Open-source license: GNU AGPL v3 only. You may use, modify, distribute, and operate WebhookHub commercially under the AGPL. If you modify it and users interact with your version over a network, section 13 requires you to prominently offer those users the corresponding source code at no charge.
- Commercial license: alternative terms are available for users that want to keep their modifications closed or cannot comply with the AGPL. Contact ivan.parfenov.42a@gmail.com.
Commercial use does not by itself require a commercial license. Third-party dependencies remain subject to their own licenses.
Contributions are accepted under the Contributor License Agreement and the process described in CONTRIBUTING.md.
