A disposable webhook inspector: generate a URL, register it with any service that fires webhooks, and watch requests arrive in real time with their full content formatted.
Live demo: https://lucianookdp.github.io/webhook-inspector/
- Live capture — any HTTP method, any content type, headers and query string included, showing up in about a second.
- Signature verification — set a secret per endpoint and check that a request really came from who it claims (GitHub, Shopify, and a few others).
- Configurable response — reply with a custom status, body and content-type instead of the default
200 ok. - Replay/forward — resend a captured request to any URL you choose, safely (it won't let you point it at your own internal network).
- Filter and export — search by method, path, body or headers, and export what's shown to a JSON file.
- Named endpoints — give an endpoint a memorable name (
/w/my-webhook) instead of a random id. - Multi-instance ready — works the same whether you run one server or several.
- Usage dashboard — a password-protected admin page (
/admin.html) shows how much the site is being used. See Usage stats.
Requirements: Node 20+, a Postgres database.
server/.env:
DATABASE_URL=postgres://user:password@host/dbname?sslmode=require
PORT=3001
TRUST_PROXY=127.0.0.1,::1
REDIS_URL=redis://localhost:6379
REDIS_URL is optional for local use (rate limiting just falls back to in-memory) but is needed for any real deployment. TRUST_PROXY can be left at the default above for local use — see the production section below for what to set it to when deploying.
web/.env (optional, defaults to http://localhost:3001):
VITE_API_BASE=http://localhost:3001
cd server && npm install && npm run migrate && npm run dev
cd web && npm install && npm run devOpen http://localhost:5173.
Endpoints expire after 24 hours, so nothing captured here sticks around longer than the debugging session it was for.
The app connects to the database with a limited account that can only read and write the data it needs — it can't change the database structure. server/sql/roles.sql creates that account once; run it as the database owner. npm run migrate (which does set up the database structure) uses a separate, more privileged connection (MIGRATION_DATABASE_URL) so the two never mix. For local development, both variables can just point at the same database.
Set ADMIN_TOKEN (a random value — openssl rand -hex 32) to turn on a small stats page at /admin.html: how many endpoints exist, how many requests have been captured, day by day. It never shows anyone's actual captured data, just counts. Leave ADMIN_TOKEN unset to turn the page off entirely.
The server runs as a Docker container (these steps use Railway, including its own Postgres and Redis) and the frontend publishes as a static site on GitHub Pages.
- Add a Postgres database to the Railway project.
- Open its Console tab in the Railway dashboard (a terminal into the database itself) and run
server/sql/roles.sqlthere, after filling in a real password and database name:psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f <file>. This sets up the limited account the app will connect as. - Nothing else to do here — the app sets up its own database structure automatically the first time it starts.
One Railway quirk: Railway's Postgres uses a certificate the app doesn't automatically trust, so it'll fail to connect at first with a "self-signed certificate" error. Fix: in that same Console tab, run cat /var/lib/postgresql/data/certs/root.crt, copy everything between -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----, and paste it into the server's DATABASE_CA_CERT variable (step 2 below).
- Create a Railway service from this repo, with root directory set to
server. Railway will pick up its settings automatically. - Add Redis to the project and point the server's
REDIS_URLat it (Railway lets you reference it directly rather than copy-pasting the value). - Set the server's environment variables (see
server/.env.examplefor the full list and explanations):NODE_ENV=productionDATABASE_URL— the limited account from step 1MIGRATION_DATABASE_URL— the database owner accountDATABASE_CA_CERT— see the certificate quirk aboveWEB_ORIGIN— the frontend's URL (step 3 below)ADMIN_TOKEN— optional, see Usage statsTRUST_PROXY— see below
- Deploy, and check that
GET /healthreturns200 ok.
Setting TRUST_PROXY correctly: this setting tells the app which IP addresses are allowed to say "here's the visitor's real address" — get it wrong and either rate limiting breaks, or someone can fake their IP. Railway doesn't publish a fixed address for this, so you have to find it yourself: temporarily add a line of code that logs the incoming connection's address, deploy, send it a couple of test requests, and see what shows up. Then set TRUST_PROXY to that (Railway typically uses addresses in 100.64.0.0/10, and sometimes routes through a second internal hop too — check for both). Afterwards, confirm it worked: send a request with a fake X-Forwarded-For header and make sure the captured request still shows your real address, not the fake one.
A GitHub Actions workflow (.github/workflows/deploy-pages.yml) already builds and publishes the web/ folder whenever main changes.
- In the repo's Settings > Pages, set Source to GitHub Actions (one-time).
- In Settings > Secrets and variables > Actions > Variables, add
API_BASE_URLset to the server's Railway URL from step 2. - Push to
main(or run the workflow manually from the Actions tab). The site goes live athttps://<username>.github.io/<repo>/. - Set the server's
WEB_ORIGIN(step 2) tohttps://<username>.github.io— just that, without the repo name — and redeploy the server.
Anyone who has (or guesses) an endpoint's URL can send it anything — there's no login required to send requests to it. If one is being misused, an operator can shut it off immediately by marking it disabled directly in the database:
UPDATE endpoints SET disabled = true WHERE id = '<endpoint id>';To report abuse of the hosted demo (https://lucianookdp.github.io/webhook-inspector/), contact lucianokdp@gmail.com.
