The self-hosted webhook relay. Receive Stripe, GitHub, and other webhooks on localhost—without paying for ngrok.
Stripe ──→ your-server.fly.io ──→ tunnel ──→ localhost:3000
- Free forever — No $8/mo subscription, no signup, no limits
- Self-hosted — Your webhooks never touch third-party servers
- Replay included — Re-send any webhook for debugging (ngrok charges extra)
- Route by path — Send
/apito port 3000,/webhooksto port 4000 - Beautiful TUI — Live request stream with Catppuccin colors
1. Deploy the server (on any VPS, Fly.io, Railway, etc.)
hookshot server --port 8080 --public-url https://hooks.yoursite.com2. Connect locally
hookshot client -s https://hooks.yoursite.com -t http://localhost:3000✓ Connected!
Public URL: https://hooks.yoursite.com/t/a1b2c3d4-...
Forwarding: http://localhost:3000
────────────────────────────────────────────
[15:04:05] → POST /webhooks/stripe (d08ba939)
[15:04:05] ← 200 OK (12ms)
3. Point your webhook provider to your public URL. Done.
go install github.com/lance0/hookshot/cmd/hookshot@latestOr grab a binary from Releases.
hookshot client -s https://hooks.yoursite.com --tui┌────────────────────────────────────────────────────────────────────┐
│ hookshot tunnel: a1b2c3d4 ● connected │
│ https://hooks.yoursite.com/t/a1b2c3d4-... │
├────────────────────────────────────────────────────────────────────┤
│ REQUESTS [r]eplay [/]filter │
│ ──────────────────────────────────────────────────────────────── │
│ ▸ POST /webhooks/stripe 200 12ms just now d08ba939 │
│ POST /webhooks/github 200 45ms 2s ago f4a21c87 │
│ POST /webhooks/slack 500 8ms 5s ago b7e93d12 │
├────────────────────────────────────────────────────────────────────┤
│ REQUEST DETAIL │
│ ──────────────────────────────────────────────────────────────── │
│ POST /webhooks/stripe │
│ Content-Type: application/json │
│ {"type":"payment_intent.succeeded","amount":2000} │
└────────────────────────────────────────────────────────────────────┘
↑/↓ navigate r replay / filter q quit
Inspect headers, bodies, and response times. Replay any request with r.
| Feature | Description |
|---|---|
| Webhook Relay | Forward external webhooks to localhost over WebSocket |
| Request Replay | Re-send any webhook with one keypress |
| Path Routing | Route different paths to different local ports |
| TUI Inspector | Live request stream with search/filter |
| Auth Tokens | Secure your relay with bearer tokens |
| TLS/HTTPS | Native TLS support for production |
| Config Files | YAML config for both server and client |
| Graceful Shutdown | Clean disconnect on Ctrl+C |
| SQLite Storage | Persistent request history across restarts |
Create hookshot.yaml:
server:
port: 8080
public_url: https://hooks.yoursite.com
token: your-secret-token # Recommended: require auth
database: /var/lib/hookshot/requests.db # Optional: persist requests
# max_body_size: 10485760 # Optional: 10MB default
# max_message_size: 10485760 # Optional: 10MB default
# allowed_origins: [] # Optional: restrict WebSocket origins
# tls_cert: /path/to/cert.pem # Optional: enable HTTPS
# tls_key: /path/to/key.pem
client:
server: https://hooks.yoursite.com
token: your-secret-token
target: http://localhost:3000
# Or route by path:
# routes:
# - path: /api
# target: http://localhost:3000
# - path: /webhooks
# target: http://localhost:4000# Server
hookshot server [flags]
-p, --port int Port (default 8080)
--public-url Public URL for display
--token Auth token (recommended for production)
-d, --database SQLite database path (default: in-memory)
--max-body-size Max webhook body size (default 10MB)
--max-message-size Max WebSocket message size (default 10MB)
--allowed-origins Allowed WebSocket origins (default: all)
--tls-cert TLS certificate path
--tls-key TLS key path
# Client
hookshot client [flags]
-s, --server Server URL (required)
-t, --target Local target (default localhost:3000)
--token Auth token
--tui Interactive mode
-v, --verbose Show request/response bodies
# Utilities
hookshot requests --server URL --tunnel ID # List recent requests
hookshot replay --server URL --tunnel ID --request ID # Replay a request| Endpoint | Description |
|---|---|
POST /t/{tunnel}/* |
Receive webhooks |
GET /api/tunnels/{id}/requests |
List stored requests |
POST /api/tunnels/{id}/requests/{req}/replay |
Replay a request |
GET /health |
Health check |
Hookshot is hardened for internet exposure:
- UUID tunnel IDs — 36-character IDs prevent guessing
- Request ownership — Replay API validates tunnel ownership
- Size limits — Configurable body/message limits (default 10MB)
- Origin validation — WebSocket origin checks
- No query tokens — Auth only via headers (no URL leakage)
For production, always set --token and use TLS.
One command to deploy your own relay:
# Clone and deploy
git clone https://github.com/lance0/hookshot.git
cd hookshot
fly launch --copy-config --name my-hookshot
# Create persistent volume for request history
fly volumes create hookshot_data --size 1
# Set your auth token
fly secrets set TOKEN=your-secret-token
# Deploy and get your URL
fly deploy
fly statusYour relay is now live at https://my-hookshot.fly.dev. Connect with:
hookshot client -s https://my-hookshot.fly.dev --token your-secret-token# With persistent storage
docker run -p 8080:8080 -v hookshot-data:/data hookshot server --token=secret
# Or in-memory only
docker run -p 8080:8080 -e DATABASE_PATH= hookshot server --token=secretgit clone https://github.com/lance0/hookshot.git
cd hookshot
go build -o hookshot ./cmd/hookshot
./hookshot server --port 8080| ngrok | Hookshot | |
|---|---|---|
| Price | $8-20/mo | Free |
| Self-hosted | No | Yes |
| Replay | Paid add-on | Included |
| Path routing | No | Yes |
| Your data | Their servers | Your servers |
Hookshot is purpose-built for webhook development. For general HTTP tunneling or TCP proxying, ngrok may be a better fit.
MIT — use it however you want.
Built with Go. Themed with Catppuccin.