Skip to main content
Audact
See the offer

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-Signature is 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 the X-Signature header 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_id is always present whenever call context is available — this is the security boundary a call_id → tenant_id guard on your endpoint should key off. call.tenant_id is also always present alongside it.
  • call.vars is the pipeline's current dynamic-variable map (values resolved from {{varName}} substitution and any tool response variables extracted so far) — not a field named call.metadata. Per-call metadata is a separate feature and will be added to this object once available.
  • call.direction is "inbound" or "outbound".
  • call.turn is 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.ResponseVars dot-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

Source updated: October 19, 2026