delivery field:
- App message (default) - Tavus emits a
conversation.tool_callevent (orconversation.perception_tool_callfor perception tools) on the data channel; your frontend handles it. - API call - Tavus makes an HTTPS request to a URL you configure, either a customer-controlled callback or a third-party API directly. Auth + body shape is configured per tool.
App message delivery
The default. Your client receives the tool call inline with the conversation events.App message delivery (default)
conversation.tool_call event carrying a tool_call_id. To return a result, send a conversation.tool_result event back with the matching tool_call_id - that’s how Tavus pairs the result with the in-flight dispatch and applies the configured on_resolve.
conversation.tool_result (sent by your client)
| Field | Type | Required | Description |
|---|---|---|---|
tool_call_id | string | ✅ | Must match the tool_call_id from the original conversation.tool_call event. |
output | string | object | ❌ | The tool result. Strings are passed through; objects are JSON-serialized. |
status | string | ❌ | "success" (default) or "error". On error, the PAL acknowledges the failure instead of speaking the result. |
If your client never sends a result, the dispatch eventually drops out of context. There’s no hard timeout on this path - the PAL just won’t have the data.
API delivery
Tavus calls an HTTPS endpoint each time the tool fires. The exact request shape depends on whether you’re hitting a third-party API directly (anyauth.type other than hmac) or a customer-controlled callback (auth.type: hmac).
API delivery
delivery.api fields
| Field | Type | Required | Description |
|---|---|---|---|
url | string | ✅ | HTTPS URL Tavus will call when the tool fires. Must be publicly reachable; private/loopback/link-local addresses are rejected. May contain {placeholders} in the path and query string - see URL templating. The hostname must be static (no placeholders). |
method | string | ❌ | One of GET, POST, PUT, PATCH, DELETE, HEAD. Defaults to POST. |
headers | object | ❌ | Extra request headers as a {string: string} map. Sent verbatim with every dispatch. |
timeout | number | ❌ | Seconds Tavus will wait for your endpoint to respond, 0 < timeout <= 60. Defaults to 10. This is also the watchdog deadline: if the request hasn’t returned by then, the tool call is marked timeout. |
auth | object | ❌ | How Tavus authenticates to your endpoint. See Tool Authentication. Defaults to no auth headers. |
body_template | object | ❌ | JSON object with {placeholders} in its string values. Renders to the request body and overrides the default auto-routing (see Request body). Only valid with POST / PUT / PATCH; pairing it with GET / HEAD / DELETE is rejected. |
query_params | object | ❌ | Static and templated query-string parameters as {string: string}. Values containing {placeholders} are interpolated from LLM arguments; plain strings pass through. |
content_type | string | ❌ | Override the request Content-Type header (defaults to application/json). Useful for application/x-www-form-urlencoded and similar. |
URL templating
{placeholders} in the URL path or query string are filled at request time from the LLM’s tool arguments and URL-encoded. The hostname must be static.
Reserved system placeholders
In addition to the LLM’s tool arguments, Tavus injects a small set of system placeholders at request time. They’re available everywhere placeholders are interpolated - URL,query_params values, and body_template. Useful for idempotency keys, correlation IDs in your logs, and Tavus-side tracing.
| Placeholder | Filled with |
|---|---|
{tavus_conversation_id} | The Tavus conversation ID (c…). |
{tavus_tool_call_id} | Unique ID for this specific tool invocation. Stable across retries. |
{tavus_inference_id} | The LLM turn (inference) that emitted the tool call. |
{tavus_turn_idx} | Integer index of the conversational turn. |
{tavus_tool_name} | The tool’s name as registered at /v2/tools. |
Request body and query string
Only arguments declared in your tool’sparameters.properties ride along. Body and query string are decided independently - you can combine them in any way.
Body
The HTTP method decides whether a body is sent.GET / HEAD / DELETE never carry a body - body_template on those methods is rejected at validation time.
| Method | body_template | Body |
|---|---|---|
GET / HEAD / DELETE | not allowed | empty |
POST / PUT / PATCH | unset | Declared arguments not consumed by URL placeholders become the JSON body. Content-Type: application/json. |
POST / PUT / PATCH | set | Renders the template with arguments and uses that as the body. Use this when the API expects a nested or renamed shape. |
Query string
query_params applies to every method - you can attach a query string to a POST as easily as to a GET.
| Method | query_params | Query string |
|---|---|---|
GET / HEAD / DELETE | unset | Declared arguments not consumed by URL placeholders auto-route here. |
GET / HEAD / DELETE | set | Only the entries you list reach the query string. Auto-route is skipped so your named entries aren’t duplicated. |
POST / PUT / PATCH | unset | empty (body holds the args). |
POST / PUT / PATCH | set | Only the entries you list reach the query string. Body is decided separately by body_template or auto-route. |
Example - reshape flat tool args into a nested API body
Suppose the tool’sparameters schema defines two flat fields:
Tool parameters (what the LLM emits)
query.text and filters.region. Use body_template to remap:
delivery.api with body_template
{ "search_term": "pizza", "region": "tokyo" }, Tavus sends:
body_template is a regular JSON object - write it as one, no escaping. Each {placeholder} inside a string value is replaced with the matching LLM argument. If a string is exactly one placeholder ("{count}"), the substituted value preserves its native type - so "{count}" with count: 10 produces 10 (number), not "10" (string). Non-string values in the template (numbers, bools, null) pass through unchanged.
Tavus callback shape
Used whenauth.type: hmac is set. Tavus ignores body_template / query_params / URL templating in this mode and sends a fixed JSON envelope, signed with HMAC-SHA256.
Request body (POST/PUT/PATCH)
| Field | Type | Description |
|---|---|---|
name | string | The tool’s name. |
arguments | string | Tool arguments as a JSON-encoded string (the LLM’s raw output). Parse this on your side. |
tool_call_id | string | Unique ID for this specific invocation. |
conversation_id | string | The Tavus conversation ID. |
inference_id | string | The inference (LLM turn) that produced the call. |
turn_idx | integer | Index of the conversational turn (groups events from the same turn). |
| Header | Value |
|---|---|
Content-Type | application/json |
X-Tavus-Signature | HMAC-SHA256 hex digest of the request body. See Verifying API signatures. |
delivery.api.headers you configured are merged in.
Response Tavus expects
The same response handling applies to both API modes:| Status | Outcome |
|---|---|
2xx | Treated as success. Response body is used as the tool result (see on_resolve for how it is consumed). |
5xx | One retry after a short backoff. If the retry also fails, the call is marked error. |
401 (oauth2_client_credentials only) | One retry with a freshly fetched token. |
| Other non-2xx | No retry; marked error. |
Timeout (no response within delivery.api.timeout seconds) | Marked timeout. |
| Connection error | One retry. If the retry also fails, marked error. |
error or timeout and on_resolve is not fire_and_forget, the PAL acknowledges the failure to the user (no result content is fed back to the LLM).
Verifying API signatures
When you setauth.type: "hmac" with an auth.secret, Tavus signs the exact request body bytes with HMAC-SHA256 and sends the hex digest in X-Tavus-Signature. Verify it before trusting the payload.
The signing input is the raw body Tavus sends, which is canonical JSON with keys sorted alphabetically. Verify against the raw bytes you received, not a re-serialized version - any re-encoding can change byte order and break the signature.
Python (Flask)
Node.js (Express)
Replace
<api-key> with your actual API key. You can generate one in the PAL Maker.
