Private Relay
Your server stays closed to the internet. A small agent, whx relay, runs inside your network. It pulls each delivery from webhooks.xyz over one outbound HTTPS connection, calls your server on the local network, and reports the result. No inbound rule, no public address, no VPN.
Set it up
- On the bin, add a destination and choose Inside my network. Enter the URL as seen from inside:
http://10.0.3.12:8080/webhooks. Private and local addresses are accepted here. - In the Private Relay box that appears, make a token. It is shown once. It delivers for this bin only.
- On a machine that can reach your server, install the CLI and start the relay:
curl -fsSL https://webhooks.xyz/cli/install.sh | sh whx relay --token whr_…
The bin page shows the token as connected within a few seconds. Against the staging service:whx relay --host staging --token whr_…(orWHX_HOST=staging; a full https:// URL works too). - Send a request to the bin. The delivery shows in the deliveries list with via relay, the status your server returned, and the time it took.
How it works
- The agent calls
GET /api/relay/<bin>/next?wait=30. The server answers at once when a delivery is due, or holds the connection up to 30 seconds and then answers empty. The agent calls again immediately. Latency from capture to your server is usually under two seconds. - Each delivery it receives is under a lease of 60 seconds. The agent calls your server with the original method, path, query, headers and body (plus
X-Webhooks-Xyz-Bin,-Request,-Delivery,-AttemptandX-Webhooks-Xyz-Via: relay), then reports the status withPOST /api/relay/<bin>/ack. - A 2xx is delivered. Anything else, a timeout or a connection error retries on the normal schedule: 30 s, 2 min, 10 min, 30 min, 2 h, 6 h, 12 h, 24 h, then it is marked failed and waits for a replay. The relay does not retry on its own; the queue does.
- If the agent stops before it reports, the lease ends and the delivery is queued again. A crash loses nothing. Two agents with two tokens can run at the same time; each delivery goes to one of them.
- Up to 10 deliveries come in one answer. The agent calls your server for up to four of them at once (
--concurrency). - Destinations on the internet are still called by webhooks.xyz directly. Only destinations marked via relay go through the agent.
Run it as a service
systemd (Linux): whx-relay.service. The unit runs as a system user with a read-only file system, no capabilities and only outbound network. The token goes in /etc/whx/relay.env (0600).
Docker: Dockerfile (distroless, non-root, the static binary only) and a compose file.
docker build -t whx-relay https://webhooks.xyz/static/relay/Dockerfile docker run -d --name whx-relay --restart unless-stopped --network host \ -e WHX_RELAY_TOKEN=whr_… whx-relay
Kubernetes: one Deployment with one replica, the token in a Secret as WHX_RELAY_TOKEN, a liveness probe on /healthz port 9229. Run two replicas with two tokens when you want no gap during node drains.
Health and metrics
The agent listens on 127.0.0.1:9229 (--health to change, empty to disable).
GET /healthzreturns200 {"ok": true, …}, or503when the last successful poll is more than two minutes old.GET /metricsin Prometheus text format:whx_relay_deliveries_total{result="delivered|failed"},whx_relay_poll_errors_total,whx_relay_last_poll_timestamp_seconds,whx_relay_up,whx_relay_info{version}.
Tokens
- A relay token (
whr_…) works for one bin and can only pull and report deliveries. It cannot read requests, change settings or see other bins. Only a SHA-256 hash is stored. - Up to 5 tokens per bin. To rotate: make a new token, start the agent with it, then revoke the old one. Both are valid in between, so there is no gap.
- Revoke a token on the bin page or with the API (
DELETE /api/v1/bins/<bin>/relay/tokens/<id>). The agent that used it stops with a clear error. - Rate limit: 600 relay calls a minute per token. The agent uses about two a minute when idle.
Security review: the answer sheet
| Inbound rules | None. The agent makes outbound HTTPS connections only. Nothing on the internet can connect to it. |
| Egress | One host, webhooks.xyz, port 443, TLS 1.2 or newer. Allow this in the egress policy; no other address is contacted. |
| Credential | One token per bin, deliver-only scope, stored hashed on our side and in your secret store on yours. Rotation without downtime. |
| What leaves your network | The HTTP status, response headers and up to 64 KB of the response body of each call, plus the time it took. Nothing else. |
| What enters | The webhook requests you asked for, exactly as the sender sent them. The agent calls only the destination URLs you configured on the bin. |
| Data retention | Requests and deliveries are kept for your plan's retention (10 days Free, 30 days Dev), then deleted. Delete a request or a bin at any time. |
| Agent | One static binary built from the cli/ source in the repository, standard library only, no dependencies, checksum in version.json. Runs as an unprivileged user; the systemd unit and container image are hardened. |
| Availability | Deliveries wait in the queue while the agent is down and resume when it reconnects. Retries for 24 hours, then replay by hand or API. |
API
# with your account token (whx_…)
POST /api/v1/bins/<bin>/destinations {"url": "http://10.0.3.12:8080/webhooks", "mode": "relay"}
GET /api/v1/bins/<bin>/relay/tokens
POST /api/v1/bins/<bin>/relay/tokens {"name": "vpc-eu-1"} → {"token": "whr_…", "run": "whx relay --token whr_…"}
DELETE /api/v1/bins/<bin>/relay/tokens/<id>
# what the agent calls, with the relay token (whr_…)
GET /api/relay/<bin>/next?wait=30&max=10
POST /api/relay/<bin>/ack {"id": 12, "status": 200, "ms": 41, "resp_headers": {…}, "resp_body": "…"}
Write your own agent if you prefer: the two calls above are the whole protocol.