> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tavus.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Tool Delivery

> How tool calls reach your application - app messages to your frontend or HTTPS API calls to your backend.

This page applies to **both LLM tools and Perception tools**. Every tool dispatches via **exactly one** of two channels, picked per tool via the `delivery` field:

* **App message** (default) - Tavus emits a `conversation.tool_call` event (or `conversation.perception_tool_call` for 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.

<Warning>
  A tool with `delivery.app_message: true` and a `delivery.api` block at the same time is rejected. Setting both to disabled is also rejected. Pick exactly one.
</Warning>

## App message delivery

The default. Your client receives the tool call inline with the conversation events.

```json App message delivery (default) theme={null}
"delivery": { "app_message": true }
```

Each invocation arrives as a [`conversation.tool_call`](/sections/event-schemas/conversation-toolcall) event carrying a `tool_call_id`. To return a result, send a [`conversation.tool_result`](/sections/event-schemas/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`.

```json conversation.tool_result (sent by your client) theme={null}
{
  "message_type": "conversation",
  "event_type": "conversation.tool_result",
  "conversation_id": "<conversation_id>",
  "properties": {
    "tool_call_id": "<id from the tool_call event>",
    "output": "It is 72 degrees and sunny in San Francisco.",
    "status": "success"
  }
}
```

| 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. |

<Note>
  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.
</Note>

## 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** (any `auth.type` other than `hmac`) or a **customer-controlled callback** (`auth.type: hmac`).

```json API delivery theme={null}
"delivery": {
  "api": {
    "url": "https://api.example.com/tools/get_weather",
    "method": "POST",
    "auth": { "type": "hmac", "secret": "whsec_long_random_string" },
    "headers": { "X-Tenant": "acme" },
    "timeout": 10
  }
}
```

### `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](#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](/sections/conversational-video-interface/pal/llm-tool-auth). 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](#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`.                     |

<Warning>
  The `tavus_` prefix is reserved. Tool `parameters.properties` cannot declare a property whose name starts with `tavus_` - the create / update tool API rejects it with `400`. Pick a different name (e.g. `customer_conversation_id`) if you need a similar field.
</Warning>

## Request body and query string

Only arguments declared in your tool's `parameters.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's `parameters` schema defines two flat fields:

```json Tool parameters (what the LLM emits) theme={null}
{
  "type": "object",
  "properties": {
    "search_term": { "type": "string" },
    "region":      { "type": "string" }
  }
}
```

But the API expects a nested shape - `query.text` and `filters.region`. Use `body_template` to remap:

```json delivery.api with body_template theme={null}
"delivery": {
  "api": {
    "url": "https://api.example.com/search",
    "method": "POST",
    "auth": { "type": "api_key", "location": "header", "name": "X-API-Key", "value": "..." },
    "body_template": {
      "query":   { "text":   "{search_term}" },
      "filters": { "region": "{region}" }
    }
  }
}
```

When the LLM calls the tool with `{ "search_term": "pizza", "region": "tokyo" }`, Tavus sends:

```
POST https://api.example.com/search
X-API-Key: ...
Content-Type: application/json

{"query": {"text": "pizza"}, "filters": {"region": "tokyo"}}
```

`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 when `auth.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.

```json Request body (POST/PUT/PATCH) theme={null}
{
  "name": "get_current_weather",
  "arguments": "{\"city\":\"San Francisco\",\"unit\":\"celsius\"}",
  "tool_call_id": "call_abc123",
  "conversation_id": "c123456789",
  "inference_id": "inf_987654321",
  "turn_idx": 4
}
```

| 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).                     |

Request headers Tavus always sets in this mode:

| Header              | Value                                                                                                  |
| ------------------- | ------------------------------------------------------------------------------------------------------ |
| `Content-Type`      | `application/json`                                                                                     |
| `X-Tavus-Signature` | HMAC-SHA256 hex digest of the request body. See [Verifying API signatures](#verifying-api-signatures). |

Any extra `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`.                                                     |

When the status is `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 set `auth.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.

<Note>
  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.
</Note>

```python Python (Flask) [expandable] theme={null}
import hmac
import hashlib
import json
import os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["TAVUS_TOOL_SECRET"].encode("utf-8")


@app.post("/tools/get_weather")
def get_weather():
    received_sig = request.headers.get("X-Tavus-Signature", "")
    expected_sig = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(received_sig, expected_sig):
        abort(401)

    body = request.get_json()
    args = json.loads(body["arguments"])
    # ... your business logic ...
    return "It is 72 degrees and sunny in San Francisco.", 200
```

```javascript Node.js (Express) [expandable] theme={null}
import express from "express";
import crypto from "crypto";

const app = express();
const SECRET = process.env.TAVUS_TOOL_SECRET;

// IMPORTANT: capture the raw bytes; do not let middleware re-serialize.
app.use(express.raw({ type: "application/json" }));

app.post("/tools/get_weather", (req, res) => {
  const received = req.header("X-Tavus-Signature") || "";
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(req.body)
    .digest("hex");

  const ok =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!ok) return res.status(401).end();

  const body = JSON.parse(req.body.toString("utf8"));
  const args = JSON.parse(body.arguments);
  // ... your business logic ...
  res.type("text/plain").send("It is 72 degrees and sunny in San Francisco.");
});

app.listen(3000);
```

<Note>
  Replace `<api-key>` with your actual API key. You can generate one in the <a href="https://maker.tavus.io/dev/api-keys" target="_blank">PAL Maker</a>.
</Note>
