Minimal Go application to receive webhook and HTTP requests, inspect them in a small built-in UI, and persist captured traffic to SQLite.
- Server-rendered UI at
/ - JSON API for creating receivers and reading paginated captured requests
- Public ingest endpoint at
/hooks/{id} - SQLite persistence
- Automatic deletion 48 hours after webhook creation
- Each webhook keeps only its newest 100 captured requests
- Optional basic auth
- Optional header token
- Optional HMAC SHA-256 verification
- Additive request validation: if multiple auth checks are configured, all of them must pass
- Failed auth attempts are still captured with a 401 result and a non-secret error message for debugging
- Basic per-IP rate limiting
- Message filtering by outcome:
all,accepted,rejected
From source. Requires Go 1.25+:
The app requires WEBHOOK_RECEIVER_ENCRYPTION_KEY, which must contain base64 or hex encoded 32-byte key material.
export WEBHOOK_RECEIVER_ENCRYPTION_KEY="$(openssl rand -base64 32)"
go run main.goOpen the UI at http://localhost:8080.
By default, data is persisted to ./webhook-receiver.db.
Override the location with WEBHOOK_RECEIVER_STORE_PATH:
WEBHOOK_RECEIVER_ENCRYPTION_KEY="$(openssl rand -base64 32)" \
WEBHOOK_RECEIVER_STORE_PATH=/var/lib/webhook-receiver/webhook-receiver.db \
go run main.goBasic-auth passwords and header-token values are stored as bcrypt hashes. HMAC secrets are stored encrypted in the database.
Optional runtime settings:
WEBHOOK_RECEIVER_PUBLIC_BASE_URLUse this absolute URL when returningdetailUrl,hookUrl, andmessagesUrl. Set this in any deployed environment. Without it, the app only emits absolute URLs for trusted local loopback requests and otherwise falls back to relative paths.WEBHOOK_RECEIVER_CLIENT_IP_HEADEROptional request header to use for client IP detection in the rate limiter. If it is unset, the app usesRemoteAddr.WEBHOOK_RECEIVER_LISTEN_ADDROverride the listen address. Default::8080.
Create an open receiver:
curl \
--header "Content-Type: application/json" \
--request POST \
--data '{}' \
https://webhook-receiver.devmino.cloud/api/webhooksResponse:
{
"id": "010d1338-5323-4e3d-93a9-4277bae8d7c4",
"detailUrl": "https://webhook-receiver.devmino.cloud/webhooks/010d1338-5323-4e3d-93a9-4277bae8d7c4",
"hookUrl": "https://webhook-receiver.devmino.cloud/hooks/010d1338-5323-4e3d-93a9-4277bae8d7c4",
"messagesUrl": "https://webhook-receiver.devmino.cloud/api/webhooks/010d1338-5323-4e3d-93a9-4277bae8d7c4/messages",
"expiresAt": "2026-03-23T12:00:00Z"
}Create a receiver with basic auth:
curl \
--header "Content-Type: application/json" \
--request POST \
--data '{"username":"username","password":"password"}' \
https://webhook-receiver.devmino.cloud/api/webhooksCreate a receiver with a header token:
curl \
--header "Content-Type: application/json" \
--request POST \
--data '{"tokenName":"Auth-Token","tokenValue":"token"}' \
https://webhook-receiver.devmino.cloud/api/webhooksCreate a receiver with token and HMAC enabled:
curl \
--header "Content-Type: application/json" \
--request POST \
--data '{"tokenName":"Auth-Token","tokenValue":"token","hmacHeader":"X-Hub-Signature-256","hmacSecret":"secret"}' \
https://webhook-receiver.devmino.cloud/api/webhooksSend requests to the public endpoint:
curl \
--header "Content-Type: application/json" \
--request POST \
--data '{"information":"content"}' \
https://webhook-receiver.devmino.cloud/hooks/WEBHOOK_IDIf a header token is configured:
curl \
--header "Content-Type: application/json" \
--header "Auth-Token: token" \
--request POST \
--data '{"information":"content"}' \
https://webhook-receiver.devmino.cloud/hooks/WEBHOOK_IDIf HMAC is configured, compute a SHA-256 signature over the raw request body and send it in the configured header:
BODY='{"information":"content"}'
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac 'secret' -hex | sed 's/^.* //')
curl \
--header "Content-Type: application/json" \
--header "X-Hub-Signature-256: sha256=$SIGNATURE" \
--request POST \
--data "$BODY" \
https://webhook-receiver.devmino.cloud/hooks/WEBHOOK_IDIf a delivery fails webhook auth, the receiver still records that attempt so it can be inspected later. The stored message will include statusCode: 401 and an error describing which check failed, without persisting secret header values.
Each webhook keeps only its newest 100 captured requests. Once that limit is exceeded, the oldest captured requests are deleted automatically.
Read captured requests for one receiver:
curl "https://webhook-receiver.devmino.cloud/api/webhooks/WEBHOOK_ID/messages?page=1&pageSize=25&outcome=all"Example response:
{
"webhookId": "WEBHOOK_ID",
"expiresAt": "2026-03-23T12:00:00Z",
"outcome": "all",
"messages": [
{
"method": "POST",
"path": "/hooks/WEBHOOK_ID",
"payload": "{\"information\":\"content\"}",
"statusCode": 200,
"headers": {
"Accept": [
"*/*"
],
"Content-Length": [
"25"
],
"Content-Type": [
"application/json"
],
"User-Agent": [
"curl/8.0.1"
]
},
"time": "2026-03-21T12:00:00Z"
}
],
"page": 1,
"pageSize": 25,
"totalMessages": 1,
"totalPages": 1,
"hasNextPage": false,
"hasPreviousPage": false
}Use outcome=accepted or outcome=rejected to focus on successful deliveries or rejected attempts.
There is no global list endpoint. Keep detailUrl, hookUrl, or messagesUrl if you want to come back to the webhook before it expires.
The built-in rate limiter allows 300 requests per 5 minutes per IP address across the app. By default it uses RemoteAddr. If WEBHOOK_RECEIVER_CLIENT_IP_HEADER is set, the app will use that header when it contains a valid IP address and otherwise fall back to RemoteAddr.