> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.voicebip.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.voicebip.com/_mcp/server.

# Voice Agent with Tool-Calling

> Go from a chatbot-on-the-phone to an agent that does things — recognises intent mid-call, calls your backend through a tool, and speaks the result ("your order ships tomorrow").

**What you build:** a hosted-AI voice agent with a custom **tool** (`track_order`). When a caller asks about their order, the agent decides to call the tool, Voicebip POSTs a `tool.invocation` request to *your* endpoint, your server hits your orders API and returns JSON, and the agent speaks it — all within the call.

```
Caller: "Where's my order AC-4821?"
   │
   ▼
Agent decides → POST agent.webhook_url ──▶ your server
                { event_type:"tool.invocation",       │  hits your orders API
                  tool:{ name:"track_order",           │
                         arguments:{order_id:"AC-4821"}}}  ← { "status":"shipped", "eta":"tomorrow" }
   ┌──────────────────────────────────────────────────┘
   ▼
Agent speaks: "Your order shipped and arrives tomorrow."
```

**Who it's for:** any developer with an existing backend API (orders, bookings, account balance) who wants the phone agent to *act on live data*, not just recite static docs. This is the biggest leap in capability — the difference between a demo and a product.

> **Note**
>
> Tool-calling is a **hosted-AI** feature: Voicebip's model runs the reasoning and decides when to call your tool. It's a different path from [BYOM](/use-cases/byom-custom-llm), where *your* model runs and Voicebip is a dumb relay. An agent is either hosted-with-tools or BYOM — not both. Custom tools are dispatched to the agent's **`webhook_url`** (the same field BYOM uses); built-in tools run internally.

## Prerequisites

* A hosted-AI agent (see [Inbound support + KB](/use-cases/inbound-support-kb) for the base build).
* An HTTPS `webhook_url` on the agent to receive `tool.invocation` and return results. It must be a public address (Voicebip's SSRF guard rejects private/loopback/link-local hosts) and respond within a few seconds — the caller is waiting.
* Your workspace signing secret (dashboard → **Workspace → Signing Secret**) to verify signatures.

## Step 1 — Define the tool

`tool_definitions` is an OpenAI-compatible JSON array. Define one custom function:

```json
[
  {
    "type": "function",
    "function": {
      "name": "track_order",
      "description": "Look up the status and ETA of a customer's order by its ID.",
      "parameters": {
        "type": "object",
        "properties": {
          "order_id": { "type": "string", "description": "The order reference, e.g. AC-4821" }
        },
        "required": ["order_id"]
      }
    }
  }
]
```

Voicebip validates the model's arguments against this JSON-Schema **before** calling your webhook — if the model hallucinates a bad shape, your endpoint is never hit and the model is asked to self-correct.

Alongside your custom tool, the agent always has these **built-in tools** it can call with no webhook — use them in your prompt:

| Built-in tool               | Effect                                        |
| --------------------------- | --------------------------------------------- |
| `transfer_call`             | Transfer the call to a human / another number |
| `hang_up`                   | End the call politely                         |
| `hold_call` / `resume_call` | Place the caller on hold and resume           |

## Step 2 — Create the agent with the tool + webhook

`tool_definitions` is a **string** (stringified JSON). Custom tools dispatch to `webhook_url`, so set both. Instruct the model when to use the tool in `system_prompt`:

```json
{
  "display_name": "Acme Orders Agent",
  "ai_provider": "hosted",
  "ai_model": "<model_id>",
  "tts_voice": "<voice_id>",
  "greeting_text": "Hi, this is Acme. Ask me about your order any time.",
  "webhook_url": "https://you.example.com/tools",
  "system_prompt": "You help callers track orders. When a caller gives an order ID, call track_order. Read back the status and ETA in one sentence. If they have no ID or ask for a person, call transfer_call.",
  "tool_definitions": "[{\"type\":\"function\",\"function\":{\"name\":\"track_order\",\"description\":\"Look up the status and ETA of an order by its ID.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"order_id\":{\"type\":\"string\"}},\"required\":[\"order_id\"]}}}]"
}
```

## Step 3 — Handle `tool.invocation` on your server

When the model calls `track_order`, Voicebip POSTs to your `webhook_url` (HMAC-signed, same scheme as everything else). Verify the signature, run the tool, and return a **JSON body** — that body is fed straight back to the model, which turns it into speech.

```js
// Express — tool dispatcher
import express from "express";
import crypto from "crypto";

const app = express();
app.use(express.raw({ type: "application/json" }));

app.post("/tools", async (req, res) => {
  // 1. verify signature (timestamp . raw body)
  const sig = req.header("X-Voicebip-Signature");
  const ts  = req.header("X-Voicebip-Timestamp");
  const mac = crypto.createHmac("sha256", process.env.VOICEBIP_SIGNING_SECRET)
    .update(`${ts}.${req.body}`).digest("hex");
  if (sig !== `sha256=${mac}`) return res.sendStatus(401);

  const evt = JSON.parse(req.body);
  if (evt.event_type !== "tool.invocation") return res.sendStatus(204);

  // 2. dispatch by tool name
  const { name, arguments: args } = evt.tool;
  if (name === "track_order") {
    const order = await myOrdersApi.get(args.order_id);   // your backend
    return res.json(order ? { status: order.status, eta: order.eta }
                          : { error: "not_found" });
  }
  res.json({ error: "unknown_tool" });
});
app.listen(3000);
```

The inbound `tool.invocation` request body:

```json
{
  "event_type": "tool.invocation",
  "call_id": "call_5xk…",
  "agent_id": "agt_…",
  "workspace_id": "ws_xyz123",
  "tool": {
    "tool_call_id": "call_9a…",
    "name": "track_order",
    "arguments": { "order_id": "AC-4821" }
  }
}
```

Your response is just the tool's result as JSON — return `200` with any JSON object:

```json
{ "status": "shipped", "eta": "tomorrow" }
```

> **Note**
>
> **How your response is used.** Any 2xx JSON body is handed back to the model verbatim. A non-JSON 2xx body is wrapped as `{"result": "<your text>"}`. A `4xx`/`5xx` becomes an error result the model can apologise for (and it can fall back to `transfer_call`). **Speed matters** — the caller hears silence while your tool runs; keep the handler fast and return `{ "error": "…" }` rather than hanging.

## Step 4 — Recursive tool loops

The agent can chain tools: call `track_order`, then based on the result decide to call `transfer_call`. Voicebip runs the tool loop up to a bounded depth per turn so a misbehaving prompt can't loop forever. Design tools to resolve in one or two hops.

## Test it

`tool.invocation` is dispatched to your `webhook_url` in a live call, so it isn't part of the `POST /v1/webhooks/test` catalogue (which covers `call.*` / `message.*` lifecycle events). Two ways to exercise it:

1. **Chat in the playground** and watch the model decide to call the tool:
   ```bash
   curl -X POST https://api.voicebip.com/v1/agents/agt_…/playground \
     -H "Authorization: Bearer pk_xxx" -H "Content-Type: application/json" \
     -d '{ "message": "Where is order AC-4821?" }'
   ```
2. **Unit-test your handler** by POSTing a sample `tool.invocation` body (above) to your own endpoint with a valid signature.

## Troubleshooting

| Symptom                                           | Likely cause                                                                                                                                 |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Agent never calls the tool                        | `system_prompt` doesn't say when to; or the tool `description` is vague. Be explicit: "when the caller gives an order ID, call track_order." |
| `missing_webhook` / tool falls back with an error | No `webhook_url` set on the agent — custom tools require it.                                                                                 |
| `ssrf_blocked`                                    | Your `webhook_url` resolves to a private/loopback/link-local address — use a public host.                                                    |
| Caller hears a long silence                       | Handler too slow — return fast, offload slow work.                                                                                           |
| `401` on your endpoint                            | Signature must HMAC `"{timestamp}.{rawBody}"` over the **raw** body, not re-serialized JSON.                                                 |

## Next steps

* Ground the agent's *answers* in your docs too → [Inbound support + KB](/use-cases/inbound-support-kb).
* Prefer to run your own model end-to-end? → [BYOM](/use-cases/byom-custom-llm).
* Route different intents to different specialised agents → [Multi-agent routing](/use-cases/multi-agent-routing).