Install
openclaw skills install webhook-relayWebhook Relay integration, driven by the `relay` CLI and the public bin API. Receive webhooks on a stable public HTTPS endpoint and forward them to a service on localhost or a private network with no public IP (the relay agent performs the last hop), or expose any local/internal HTTP or TCP service to the internet over a public tunnel. Capture and inspect incoming webhooks with a free, no-signup bin to debug exactly what a provider sends, verify HMAC signatures, and mock responses. Also: forward server-side to public URLs (no agent), fan out one webhook to many destinations, transform payloads with JavaScript functions, and schedule recurring (cron) webhooks. Use when the user wants to forward, tunnel, debug, transform, or schedule webhooks with Webhook Relay.
openclaw skills install webhook-relayWebhook Relay moves HTTP traffic across network boundaries. It gives you stable public endpoints and a lightweight agent so you can receive provider webhooks anywhere, expose private services to the internet, and see exactly what is being sent — without opening firewall ports or owning a public IP.
This skill covers three primary jobs and a few supporting ones:
localhost, a LAN host, or a Kubernetes service.Supporting: forward server-side to public URLs (no agent), fan out to many destinations, transform payloads with JavaScript functions, and schedule recurring (cron) webhooks.
Official docs: https://webhookrelay.com/docs — most pages also render as plain
markdown by appending .md to the URL.
https://my.webhookrelay.com/v1/webhooks/<id>)
that receives requests. This is the URL you hand to a provider.internal
outputs (localhost / private network) require the agent to be running;
public outputs are delivered server-side with no agent.The bin (debugging) needs no install — it's plain curl. Everything else
uses the relay CLI.
# Install — see https://webhookrelay.com/docs/installation/cli
# macOS/Linux quick install, or download a binary from the docs page.
relay login # interactive; or set RELAY_KEY / RELAY_SECRET
relay bucket ls # confirm you're authenticated
For CI/servers, set the RELAY_KEY and RELAY_SECRET environment variables
instead of relay login.
A provider needs to reach code running on localhost, a private LAN host, or a
Kubernetes service that has no public IP. Webhook Relay gives you a public input
URL; the agent holds an outbound connection and performs the final hop to your
private destination. Nothing inbound is opened on your firewall.
Provider ──POST──▶ https://my.webhookrelay.com/v1/webhooks/<id> (input, public)
│ (streamed down the agent's outbound connection)
▼
relay agent on your machine
│
▼
http://localhost:8080/webhook (output, internal)
An internal output requires the agent to be running — it is the agent that delivers to the private destination.
relay forwardCreates a bucket + public input + internal output, starts the agent, and subscribes to the stream — all in one command. Ideal for local development.
relay forward --bucket my-app http://localhost:8080/webhook
my-app is created if it doesn't exist.https://my.webhookrelay.com/v1/webhooks/2a1b…) — give this to the
provider (Stripe dashboard, GitHub webhook settings, etc.), never
localhost.--type internal is the default. Re-attach to an existing bucket and relay all
its configured outputs with relay forward --bucket my-app (no destination).
Useful flags: --bucket/-b, --function/-f <name|id> (attach a transform),
--no-agent (configure only), and --max-retries / --retry-wait-min /
--retry-wait-max (retry when the destination returns >= 500).
relay bucket create my-app
relay input create --bucket my-app "default public endpoint" # prints the URL
relay output create local-app --bucket my-app \
--destination http://localhost:8080/webhook # internal (default)
relay forward -b my-app # foreground agent
# …or run the agent as a background OS service:
relay service install && relay service start
Name every output — the name is the first positional argument (
local-appabove). Omitting it creates an empty-named output, and adding a second un-named output to the same bucket fails withoutput with name '' already exists(which also breaks fan-out).
Inspect / clean up:
relay bucket ls
relay bucket inspect my-app
relay input ls # shows the public endpoint URLs
relay output ls
relay bucket rm my-app -f # -f also removes the bucket's inputs/outputs
For Kubernetes ingress (exposing in-cluster services), see relay ingress.
A tunnel publishes a public hostname (e.g. https://myapp.webrelay.io) and
routes all traffic hitting it to a destination your machine can reach. The
agent keeps an outbound connection open, so no inbound ports or public IP are
required. Unlike webhook forwarding, a tunnel proxies any inbound HTTP or TCP
traffic — ideal for dev servers, demos, local APIs, and TCP services (SSH,
databases).
Internet ──▶ https://myapp.webrelay.io (public tunnel host)
│ (over the agent's outbound connection)
▼
relay agent ──▶ http://localhost:3000 (your service)
relay connect# Expose a local web app; a public *.webrelay.io host is assigned and printed.
relay connect http://localhost:3000
# Pin a friendly subdomain and enable HTTPS at the edge:
relay connect --name myapp --subdomain myapp --crypto flexible \
http://localhost:3000
# → https://myapp.webrelay.io (reuse the same --name to keep the same host)
The agent runs in the foreground and logs each request. Ctrl-C to stop; re-run
the full relay connect … <destination> command to bring it back on the
same hostname (running relay connect --name myapp alone falls back to
http://127.0.0.1:80 and you'll get 502s).
Common flags:
--name/-n — stable identity; reuse to keep the same host.--subdomain/-s / --host/-H — preferred subdomain or full custom host.--crypto/-c — TLS mode: flexible (HTTPS at the edge, HTTP to your service
— most common), full, full-strict, tls-pass-through.--region/-r — pick a region (e.g. eu, us-west) to lower latency.--username/-u / --password/-p — protect the tunnel with HTTP basic auth.--rewrite-host-header — set the Host header sent to your service (needed by
many vhost-based apps and dev servers, e.g. --rewrite-host-header localhost).--protocol — http (default) or tcp (expose SSH, databases, etc.).--group/-g — group tunnels so one agent serves several at once.--no-agent — create the configuration only, don't start the agent.# Share a Vite dev server with the right Host header
relay connect -n dev -s dev -c flexible --rewrite-host-header localhost \
http://localhost:5173
# Password-protect a demo
relay connect -n demo -s demo -c flexible -u alice -p s3cret http://localhost:8080
# Expose a private LAN host (run the agent on a machine that can reach it)
relay connect -n grafana -s grafana -c flexible http://10.0.0.5:3000
# TCP tunnel (e.g. SSH)
relay tunnel create ssh-box --protocol tcp --destination tcp://localhost:22
relay connect --name ssh-box --protocol tcp tcp://localhost:22
Explicit, persistent setup mirrors forwarding:
relay tunnel create myapp --destination http://localhost:3000 \
--subdomain myapp --crypto flexible --region eu
relay tunnel ls
relay tunnel inspect myapp
relay connect --name myapp --crypto flexible http://localhost:3000
relay tunnel rm myapp
A bin is a throwaway public endpoint that captures every HTTP request sent to it, so you can see exactly what a provider sends, reproduce a payload, or mock an endpoint's response while building the real handler.
https://bin.webhookrelay.com — no auth, CORS enabled.B=https://bin.webhookrelay.com
# 1. Create a bin, capture its ID
BIN=$(curl -s -X POST $B/v1/bins | jq -r .id)
# 2. The public receiver URL — give it to any sender (accepts ANY method):
echo "$B/v1/webhooks/$BIN"
# 3. Send a test request
curl -s -X POST "$B/v1/webhooks/$BIN" -H 'Content-Type: application/json' -d '{"hello":"world"}'
# 4. Read back every captured request as JSON
curl -s "$B/v1/bins/$BIN" | jq '.requests'
Open the same bin in a browser UI:
https://webhookrelay.com/webhook-bin?bin=<BIN_ID>
Each captured request has id (sortable ULID), receivedAt (Unix seconds),
method, header (map of name → { key, values[] }), query (raw string),
body (raw string), ip, and responseStatus.
# Most recent request's body
curl -s "$B/v1/bins/$BIN" | jq -r '.requests | sort_by(.receivedAt) | last | .body'
# Block until exactly one request arrives, then print it (live SSE stream)
curl -sN "$B/v1/events?stream=$BIN" | grep -m1 '^data:' | sed 's/^data: //' | jq .
PUT /v1/bins/{id} configures the reply — custom status/body/headers, latency,
and probabilistic failures (test a sender's retry logic):
curl -s -X PUT "$B/v1/bins/$BIN" -H 'Content-Type: application/json' -d '{
"id": "'"$BIN"'",
"response": {
"status": 201,
"body": "{\"ok\":true}",
"delay": 250,
"header": { "Content-Type": { "key": "Content-Type", "values": ["application/json"] } },
"failures": [ { "percentage": 10, "status": 500, "body": "simulated failure" } ]
}
}'
POST /v1/hmac with { algorithm, secret, body } where body is base64
encoded (algorithms: md5, sha1, sha256, sha512). Returns
{ "signature": "<hex>" }. Prefix as the provider expects (GitHub:
sha256=<hex>; Stripe builds t=…,v1=<hex> over "{t}.{body}").
RAW=$(curl -s "$B/v1/bins/$BIN" | jq -r '.requests | sort_by(.receivedAt) | last | .body')
# Pipe base64 through `tr -d '\n'` — GNU base64 wraps at 76 cols and would
# embed newlines in the body, producing a wrong signature.
SIG=$(curl -s -X POST "$B/v1/hmac" -H 'Content-Type: application/json' -d "$(jq -nc \
--arg s "$WEBHOOK_SECRET" --arg b "$(printf %s "$RAW" | base64 | tr -d '\n')" \
'{algorithm:"sha256", secret:$s, body:$b}')" | jq -r .signature)
echo "expected: sha256=$SIG"
Bin endpoint reference: POST /v1/bins (create), GET /v1/bins/{id} (read +
requests), PUT /v1/bins/{id} (configure response), DELETE /v1/bins/{id},
(any) /v1/webhooks/{id} (receiver), GET /v1/events?stream={id} (SSE),
POST /v1/hmac. Bodies are capped at 500 KB; the service is rate limited (429
when flooded).
When the destination is already on the internet, use a public output. It is delivered server-side, so no agent runs and it works 24/7.
relay forward --type public --bucket to-slack \
https://hooks.slack.com/services/T000/B000/XXXX
The CLI configures it and exits. Pair with a function to reshape the payload.
Add several outputs to the same bucket; every received webhook is delivered to all of them.
relay output create slack -b alerts --type public -d https://hooks.slack.com/services/…
relay output create discord -b alerts --type public -d https://discord.com/api/webhooks/…
relay output create ingest -b alerts --type public -d https://example.com/ingest
A function runs server-side on each request passing through an input/output.
Your code runs against a global request object r (no wrapper function):
// Convert a generic JSON webhook into a Slack message.
const data = JSON.parse(r.body)
r.setBody(JSON.stringify({ text: "New event: " + (data.message || "n/a") }))
r.setHeader("Content-Type", "application/json")
Read: r.body, r.method, r.path, r.headers, r.query, r.formData.
Mutate the forwarded request: r.setBody, r.setHeader/r.deleteHeader,
r.setMethod, r.setPath, r.setRawQuery. Control the response:
r.setResponseStatus, r.setResponseBody, r.setResponseHeader. Drop a
request with r.stopForwarding(). Read secrets with cfg.get("KEY"). Always
JSON.stringify before r.setBody.
relay function test -f spec.yaml -v # test locally (set driver: js)
relay function create --name to-slack --driver js --source to-slack.js
relay output create -b to-slack --type public -d https://hooks.slack.com/services/… \
--function to-slack # attach to an output
A cron fires a webhook on a schedule (5-field cron expression + IANA timezone). Good for heartbeats, scheduled reports, and timed reminders.
relay cron create hourly-ping \
--schedule "0 * * * *" --timezone "Europe/London" \
--method POST --destination https://example.com/webhook \
--payload '{"ping":"hourly"}' --header Content-Type=application/json
relay cron ls
relay cron update hourly-ping --enabled=false # pause without deleting
relay cron rm hourly-ping
payload is a string (JSON-encode objects). Attach a function with
--function to build/sign the payload at send time. Manage in the dashboard at
https://my.webhookrelay.com/cron, or via the REST API with relay api /v1/crons.
python3 -m http.server 8080), run relay forward -b my-app http://localhost:8080, then
curl -X POST https://my.webhookrelay.com/v1/webhooks/<id> -d '{"hi":1}' and
watch the agent log + local server receive it.https://<host> in a browser; requests appear in
the agent's terminal. Connection errors usually mean the local service is down
or --rewrite-host-header doesn't match what the app expects.BIN=$(curl -s -X POST $B/v1/bins | jq -r .id); curl -s -X POST "$B/v1/webhooks/$BIN" -d '{"smoke":1}'; curl -s "$B/v1/bins/$BIN" | jq '.requests | length' should be >= 1.The dashboard at https://my.webhookrelay.com shows buckets, delivery logs, and lets you replay/retry failed deliveries.
localhost.--bucket / tunnel --name to keep the same public URL across
restarts.cfg.get(...), not in source; never send secrets to a
bin (it's public and temporary).Plain-markdown docs (append .md to most pages to read directly):