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, no verification runs: every request is accepted. Don't expose an unsecured webhook channel to the public internet; either set secret, restrict access at a reverse proxy, or run the listener bound to a private network only.
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