docs/book/src/channels/webhook.md
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
/webhookendpoint. The gateway service has its ownPOST /webhookfor 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.
{{#config-fields channels.webhook}}
Full field reference: config reference.
The channel binds 0.0.0.0:{port} and routes POST {listen_path}.
Request body (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.
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
secretin[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 asecretand 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.
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.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.
The channel binds to 0.0.0.0 directly. To expose it on the public internet:
[tunnel] (ngrok, cloudflare, or tailscale) and the daemon brings up the tunnel alongside the channel.Always pair public exposure with secret. An unauthenticated webhook listener is an open ingress to the agent.
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.
/webhook