WebhookSpy is a lightweight webhook inspector powered by Bun, Elysia, Alpine.js, Tailwind CSS, and 11ty. It stores data in SQLite by default and supports Redis for cloud deployments. It gives you permanent HTTP endpoints that you can use forever for testing, stream payloads over Server-Sent Events (SSE), and present the captures in a modern, polished UI.
- One-click endpoints – Generate unique webhook URLs and matching inspector links directly from the homepage.
- Permanent URLs – Your webhook URL never expires. Data clears after 7 days of inactivity, but the same URL keeps working automatically.
- Live streaming inspector – Requests appear instantly via SSE with a sidebar list and detailed headers/body view.
- REST API with OpenAPI docs – Full programmatic access via REST API with interactive documentation at
/docs. - Request filtering API – Query captured requests by method, path, body content, or headers for easy test assertions.
- Smart retention – Keeps the last 100 requests per endpoint. Older requests are automatically pruned.
- Theme aware UI – Supports automatic light/dark detection plus a manual theme switcher.
- Secure endpoints – Optionally protect endpoints with access keys for testing sensitive webhooks.
- Security hardened – Rate limiting, security headers (CSP, X-Frame-Options), and XSS protection built-in.
┌────────────┐ captures/streams ┌───────────────────────────┐
│ HTTP client│ ──────────────────────▶ │ Elysia server (server.ts) │
└────────────┘ │ • REST + webhook catcher │
│ • OpenAPI docs at /docs │
│ • Pluggable storage │
└──────┬──────────┬─────────┘
│ serves │ reads/writes
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ 11ty site │ │ SQLite (default) │
│ Tailwind + │ │ or Redis │
│ Alpine + SSE │ │ (STORAGE_BACKEND │
└──────────────┘ │ =redis) │
└──────────────────┘
The Elysia server (running on Bun) exposes /api endpoints with auto-generated OpenAPI documentation at /docs, captures any HTTP request to /{endpointId}, and delegates persistence to a StorageAdapter — SQLite by default, Redis for cloud/multi-instance deployments. The frontend is generated with 11ty/Nunjucks templates and Alpine.js for interactivity, compiled into _site/ and served by the same process.
Option A: Docker (recommended)
- Docker with Docker Compose
Option B: Local Bun installation
- Bun 1.1+ (includes
bun install,bun run, and bun-provided TypeScript runtime). - macOS/Linux with SQLite available (Bun bundles SQLite so nothing extra is required).
# clone the repo, then
git clone https://github.com/djedi/WebHookSpy.git
cd WebhookSpy
# start the development server
./devThat's it! The script builds the site and starts the server with hot reload.
# clone the repo, then
git clone https://github.com/djedi/WebHookSpy.git
cd WebhookSpy
# install dependencies
bun install
# build the static site (11ty output lives in _site/)
bun run build
# start the dev server
bun run devThe first build generates _site/index.html and _site/endpoint/index.html, which the Bun server serves along with the API endpoints.
Once the server starts, visit http://localhost:8147 to generate endpoints. The workflow:
- Click Quick Endpoint for public testing or Secure Endpoint for access-key protected inspection.
- Point any HTTP client/webhook provider at the webhook URL (
/{id}). - Open the inspector URL (
/inspect/{id}) in a browser to watch requests stream in live.
For secure endpoints, copy and save the access key shown after creation—it's only displayed once. Share the key with teammates via URL (/inspect/{id}?key=whspy_...) or they can enter it manually.
Use the /api/endpoints/{id}/requests endpoint to programmatically retrieve and filter captured requests—useful for test assertions and CI/CD pipelines.
# Get all captured requests
curl "http://localhost:8147/api/endpoints/{id}/requests"
# Filter by HTTP method
curl "http://localhost:8147/api/endpoints/{id}/requests?method=POST"
# Filter by body text (substring match)
curl "http://localhost:8147/api/endpoints/{id}/requests?body=order_id"
# Filter by JSON body key existence
curl "http://localhost:8147/api/endpoints/{id}/requests?body_key=user_id"
# Filter by JSON body key:value
curl "http://localhost:8147/api/endpoints/{id}/requests?body_value=status:completed"
# Filter by header existence
curl "http://localhost:8147/api/endpoints/{id}/requests?header_key=x-signature"
# Filter by header name:value
curl "http://localhost:8147/api/endpoints/{id}/requests?header_value=content-type:application/json"
# Filter by query param existence
curl "http://localhost:8147/api/endpoints/{id}/requests?query_key=rand"
# Filter by query param key:value
curl "http://localhost:8147/api/endpoints/{id}/requests?query_value=rand:24052"
# Combine filters and limit results
curl "http://localhost:8147/api/endpoints/{id}/requests?method=POST&body_key=event&limit=1"| Parameter | Description |
|---|---|
method |
Filter by HTTP method (GET, POST, etc.) |
path |
Filter by request path (substring match) |
body |
Filter by body text (substring match) |
body_key |
Filter by JSON body key existence |
body_value |
Filter by JSON body key:value (format: key:value) |
query_key |
Filter by query param key existence |
query_value |
Filter by query param key:value (format: key:value) |
header_key |
Filter by header existence (case-insensitive) |
header_value |
Filter by header name:value (format: name:value) |
limit |
Limit number of results returned |
# ensure the site is built first
bun run build
# start without file watching
bun run startPull and run the pre-built image from Docker Hub:
docker run -d \
--name webhookspy \
-p 8147:8147 \
-v webhookspy-data:/app/data \
--restart unless-stopped \
xhenxhe/webhookspy:latestThen visit http://localhost:8147 (or your server's IP/domain).
Create a docker-compose.yml file:
services:
webhookspy:
image: xhenxhe/webhookspy:latest
container_name: webhookspy
ports:
- '8147:8147'
volumes:
- webhookspy-data:/app/data
restart: unless-stopped
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8147/']
interval: 30s
timeout: 3s
retries: 3
volumes:
webhookspy-data:Then run:
docker compose up -dservices:
webhookspy:
image: xhenxhe/webhookspy:latest
container_name: webhookspy
volumes:
- webhookspy-data:/app/data
restart: unless-stopped
networks:
- caddy
caddy:
image: caddy:latest
container_name: caddy
ports:
- '80:80'
- '443:443'
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy-data:/data
- caddy-config:/config
restart: unless-stopped
networks:
- caddy
networks:
caddy:
volumes:
webhookspy-data:
caddy-data:
caddy-config:Create a Caddyfile:
webhooks.yourdomain.com {
reverse_proxy webhookspy:8147
}
- Go to Stacks → Add stack
- Name it
webhookspy - Paste this in the Web editor:
version: '3.8'
services:
webhookspy:
image: xhenxhe/webhookspy:latest
container_name: webhookspy
ports:
- '8147:8147'
volumes:
- webhookspy-data:/app/data
restart: unless-stopped
volumes:
webhookspy-data:- Click Deploy the stack
| Variable | Default | Description |
|---|---|---|
PORT |
8147 |
Port the server listens on |
STORAGE_BACKEND |
(none) | Set to redis to use Redis instead of SQLite |
REDIS_URL |
(none) | Redis connection URL — required when STORAGE_BACKEND=redis |
| Environment | URL format |
|---|---|
| Local Docker | redis://redis:6379 |
| Azure Cache (TLS) | rediss://:<password>@<name>.redis.cache.windows.net:6380 |
| Self-hosted (no TLS) | redis://<host>:6379 |
The rediss:// scheme (double-s) enables TLS, which Azure Cache for Redis requires by default.
# Clone the repo
git clone https://github.com/djedi/WebHookSpy.git
cd WebhookSpy
# Build locally
./build
# Build and push to Docker Hub
./build push -t v1.0.0
# Multi-platform build (amd64 + arm64)
./build multi -t v1.0.0For testing webhooks with sensitive data, create a Secure Endpoint:
- Access keys are generated server-side with a
whspy_prefix - Keys are hashed (bcrypt) before storage—we never store plaintext keys
- The inspector and SSE stream require the access key to view requests
- Webhook capture still works without the key (requests are recorded, just not viewable without auth)
Important: Secure endpoints follow the same 7-day inactivity expiration as regular endpoints. If your endpoint expires, your access key becomes invalid—the endpoint will be auto-recreated as a new, unprotected endpoint if someone hits the URL again. To keep a secure endpoint alive, ensure it receives activity (webhook requests, inspector visits, or API calls) at least once every 7 days.
To prevent abuse, WebhookSpy enforces per-IP rate limits:
| Action | Limit |
|---|---|
| Endpoint creation | 10 per minute |
| Webhook requests | 100 per minute |
Exceeding limits returns HTTP 429 with a Retry-After header.
All responses include security headers:
Content-Security-Policy– Restricts script/style sourcesX-Frame-Options: DENY– Prevents clickjackingX-Content-Type-Options: nosniff– Prevents MIME sniffingX-XSS-Protection: 1; mode=block– Legacy XSS protectionReferrer-Policy: strict-origin-when-cross-origin
- Use Secure Endpoints when testing webhooks with API keys, tokens, or PII
- Public endpoints display a warning banner reminding users not to send sensitive data
- All JSON payloads are HTML-escaped before rendering to prevent XSS
WebhookSpy supports two storage backends selected via the STORAGE_BACKEND environment variable.
- Data lives in
data/webhookspy.sqlite. The directory and database are created automatically. - To reset local data, stop the server and delete the
data/directory.
- Requires
REDIS_URLpointing to a running Redis instance. - Endpoint keys are stored as Redis hashes with native TTL — data survives container restarts automatically.
- For local development with Redis, start the dev container with the redis profile:
./dev --profile redis(also uncomment the env vars indocker-compose.dev.yml).
- Activity-based expiration: The expiration timer resets to 7 days on each new webhook request.
- Request limit: Each endpoint keeps up to 100 requests. Older ones are automatically pruned.
- Auto-recreation: If an endpoint expires and you hit the same URL again, it is automatically recreated — so URLs effectively work forever.
src/
server.ts # Elysia server (API endpoints, SSE, static file serving)
storage.ts # StorageAdapter interface, types, and backend factory
adapters/
sqlite.ts # SQLite backend (default)
redis.ts # Redis backend (STORAGE_BACKEND=redis)
site/ # 11ty templates, layout, assets, Alpine components
assets/ # static assets copied to /assets in the build output
_site/ # generated static files (build artifact)
data/ # SQLite database directory (created on demand)
| Command | Description |
|---|---|
./dev |
Start the dev server (builds site + watches) |
./dev -d |
Start in detached mode (background) |
./dev down |
Stop and remove containers |
./dev restart |
Restart the containers |
./dev rebuild |
Rebuild container from scratch and start |
./dev logs |
Show container logs |
./dev logs -f |
Follow container logs |
./dev shell |
Open a shell inside the container |
./dev status |
Show container status and resource usage |
./dev clean |
Remove containers, volumes, and images |
./dev --help |
Show all available commands |
| Command | Description |
|---|---|
./build |
Build Docker image locally |
./build push |
Build and push to Docker Hub |
./build push -t v1.0.0 |
Build and push with specific tag |
./build release -t v1.0.0 |
Tag as version + latest, then push |
./build multi -t v1.0.0 |
Multi-platform build (amd64 + arm64) and push |
./build --no-cache |
Build without cache |
./build --dry-run |
Show commands without executing |
| Script | Description |
|---|---|
bun run dev |
Start the Bun server with watch mode. |
bun run start |
Start the Bun server once (no watching). |
bun run build |
Build the 11ty site into _site/. |
Contributions are welcome whether it's bug reports, docs, or new features.
- Fork the repository and create a feature branch (
git checkout -b feature/my-idea). - Run
bun installonce, then usebun run devor./devfor a live server while you work. - Execute
./testto run the Bun test suite (add--watch,--changed, or--reportfor coverage reports) and keep it green. - Please run
bun run buildbefore opening a PR to ensure the 11ty site still builds. - Open a pull request that describes the change, how to test it, and links to any related issues.
Not sure where to start? Check the issue tracker for good first issue or help wanted labels, or open a discussion with your proposal.
- Questions / ideas: GitHub Discussions or start a new issue.
- Bug reports: include Bun version, OS, reproduction steps, and relevant logs/SSE output in a GitHub issue.
- Security concerns: please email the maintainer or open a private security advisory instead of a public issue.
- Inspector page shows “Inspector unavailable”: Run
bun run buildso_site/endpoint/index.htmlexists. - No requests appear: Make sure you are hitting the webhook URL (e.g.,
http://localhost:8147/abcdef123...) and that the inspector tab stays open to keep the SSE connection alive. - Theme switch stuck: The theme toggle stores its choice in
localStorageunderwebhookspy-theme. Clear it if you need to reset to system defaults.
WebhookSpy is released under the MIT License. Feel free to fork, deploy, and build on it in your own stack.
Happy debugging!
