Tool dispatch — outbound function-call requests
This documents the request Audact sends to a tenant's own HTTP endpoint when the in-call LLM invokes a custom tool (function call) configured on an agent.
Authentication — X-Signature
When a tool has a signing secret configured, every outbound dispatch carries:
X-Signature: <hex_hmac_sha256_of_raw_request_body>
- Algorithm: HMAC-SHA256 over the exact raw JSON body bytes that are sent.
Unlike the webhook signature (see Webhook delivery), there is no
timestamp component in the tool-dispatch signature — it is a plain HMAC of the body only, and
there is no
X-Audact-Timestamp-style companion header. - If no signing secret is configured for the tool,
X-Signatureis omitted entirely — configure a secret on the tool if you need to verify the caller. - Do not confuse this with
X-Audact-Signature(the webhook delivery signature, which does include a timestamp and supports dual-key rotation). They are two independent schemes for two independent surfaces (webhooks vs. tool dispatch). - Verification on your side: recompute
hex(hmac_sha256(secret, raw_body))and compare constant-time against theX-Signatureheader value.
Request body shape
Default (args_only = false):
json
{
"name": "<tool_name>",
"args": { "...": "LLM-supplied arguments, merged with any const fields" },
"call": {
"tenant_id": "<uuid>",
"call_id": "<uuid>",
"turn": 3,
"jurisdiction": "NL",
"direction": "inbound",
"vars": { "...": "current dynamic-variable snapshot" }
}
}call.call_idis always present whenevercallcontext is available — this is the security boundary acall_id → tenant_idguard on your endpoint should key off.call.tenant_idis also always present alongside it.call.varsis the pipeline's current dynamic-variable map (values resolved from{{varName}}substitution and any tool response variables extracted so far) — not a field namedcall.metadata. Per-call metadata is a separate feature and will be added to this object once available.call.directionis"inbound"or"outbound".call.turnis the current conversation turn number (integer, 1-based).
args_only = true: the {name, args, call} wrapper is dropped entirely and the request body is
just args — a flat JSON object with no wrapper, no name, no call. This is a deliberate,
explicit per-tool boolean (ToolConfig.ArgsOnly, JSON field args_only) that a tenant sets when
their endpoint expects a flat body; it is not a hidden dashboard toggle.
Timeout, retries, and response handling
- Per-tool timeout:
ToolConfig.Execution.TimeoutMs; when unset (zero), the default is 120,000 ms (120 s). The accepted range when set explicitly is[1, 300000]ms (max 5 minutes). - Retries: up to 3 attempts total, waiting 500 ms between attempts. A timed-out attempt is never retried — retries only apply to non-timeout errors (network errors, non-2xx responses). Retries also stop immediately if the parent (in-call) context is cancelled.
- Response body cap: 15,000 characters; longer bodies are truncated before being handed back to the LLM.
- Response variable extraction:
ToolConfig.ResponseVarsdot-paths are extracted from the JSON response into the pipeline's dynamic-variable store (string/number/bool leaves only; arrays and objects are not extracted) and become available to subsequent{{varName}}substitutions and future tool calls in the same call. - The tool HTTP client is SSRF-guarded: address lookups are validated against private, loopback and link-local ranges before dialing, on every attempt.
Example: verifying and reading a tool-dispatch request (pseudocode)
sig := request.header("X-Signature")
if sig != hex(hmac_sha256(secret, raw_body)):
reject()
body := json.parse(raw_body)
call_id := body.call.call_id # always present
tenant_id := body.call.tenant_id # always present
args := body.args # or the whole body if args_only