Back to Zeroclaw

Webhooks

docs/book/src/channels/webhook.md

0.8.44.8 KB
Original Source

Webhooks

The webhook channel is a generic inbound/outbound HTTP adapter. It runs its own embedded HTTP server on a port you choose, accepts JSON-shaped messages, hands them to the agent, and (optionally) POSTs the agent's replies to a URL you specify. Use it as the universal adapter for any system that can produce an HTTP POST.

Not the same as the gateway's /webhook endpoint. The gateway service has its own POST /webhook for paired clients hitting the agent over HTTP, that lives under [gateway] and is described in Operations → Network deployment. This page documents the [channels.webhook] channel only.

Configuration

{{#config-fields channels.webhook}}

Full field reference: config reference.

Inbound

The channel binds 0.0.0.0:{port} and routes POST {listen_path}.

Request body (JSON):

json
{
  "sender": "alice",
  "content": "Hello, agent.",
  "thread_id": "optional-conversation-id"
}
  • sender: required, used as the message's sender identity.
  • content: required, the user message handed to the agent. Empty content returns 400.
  • thread_id: optional. If set, the agent's reply targets the same thread; otherwise replies target sender.

Success returns 200 OK. Malformed JSON or empty content returns 400. Backpressure (channel queue full) returns 503.

Signature verification

When secret is set, every inbound request must carry an X-Webhook-Signature header:

X-Webhook-Signature: sha256=<hex-encoded HMAC-SHA256 of the raw body>

The channel computes HMAC-SHA256(secret, raw_body), hex-encodes it, and compares against the header value (the sha256= prefix is stripped before decode). Mismatch or missing header returns 401.

When secret is unset, the channel refuses to start (the listener bails at startup with an error directing the operator to configure one). An enabled webhook channel always requires a configured secret. This is a deliberate fail-fast: an unauthenticated webhook listener with agent access is an open ingress that should not exist in any deployment.

Breaking change. Deployments that previously ran the listener secretless behind a reverse proxy or bound to a private network must now configure a secret in [channels.webhook.<alias>].secret. The secretless fallback path is removed: an enabled webhook listener is treated as an unconditional risk that no deployment topology should be exposed to. Operators in this situation should set a secret and either keep the listener behind their existing reverse proxy or continue binding to a private network; either is fine, the secret is what is now load-bearing.

Outbound

When send_url is set, every agent reply is delivered as an HTTP request to that URL:

{send_method} {send_url}
Authorization: {auth_header}    # only if auth_header is set
Content-Type: application/json

{
  "content": "agent reply text",
  "thread_id": "optional thread id",
  "recipient": "optional recipient id"
}
  • send_method is POST (default) or PUT. Any other value falls back to POST.
  • auth_header is sent verbatim as the Authorization header value, include the scheme yourself (e.g. Bearer xyz, Basic dXNlcjpwYXNz).
  • recipient is omitted when empty.
  • Non-2xx responses raise an error in logs; the agent reply is considered failed.

When send_url is unset, agent replies are dropped silently (logged at debug). This is the right configuration for fire-and-forget inbound flows where the response is delivered through some other channel.

Public exposure

The channel binds to 0.0.0.0 directly. To expose it on the public internet:

  1. Reverse proxy: terminate TLS at nginx / Caddy / Traefik and proxy to the channel's port. See Operations → Network deployment.
  2. Tunnel: configure [tunnel] (ngrok, cloudflare, or tailscale) and the daemon brings up the tunnel alongside the channel.
  3. Local-only: run inside a private network and have your producer hit the LAN/loopback address directly.

Always pair public exposure with secret. An unauthenticated webhook listener is an open ingress to the agent.

Outbound retries

When send_url is set, outbound delivery retries transient failures, network errors, HTTP 429, and HTTP 5xx, with exponential backoff (±25% jitter) capped by retry_max_delay_ms. Non-429 4xx responses fail immediately without retrying. When the server returns a Retry-After header on 429 or 503, that value is honored and also clamped by retry_max_delay_ms. Setting max_retries = 0 is fire-and-forget.

See also