Tools
Letting the AI call your API during a call: defining tools, signed requests, tools that need the caller's confirmation, timeouts and idempotency.
Add tools to an agent and the AI can call your API mid-call and answer with the result: check free slots, look up a customer by phone number, make a booking. The format is generic; you can receive every tool on one URL and switch on the tool name.
If all you need is Google Calendar, kintone and similar services, use connections instead of building your own tools.
Defining tools#
"tools": [
{
"name": "find_free_slots",
"description": "予約の空きを調べる。相手が希望の日を言ったときに使う",
"url": "https://saas.example.com/voicast/tools",
"parameters": {
"properties": {
"date": { "type": "string", "description": "希望の日(YYYY-MM-DD)" },
"menu": { "type": "string", "enum": ["検診", "治療", "クリーニング"] }
},
"required": ["date"]
}
},
{
"name": "create_booking",
"description": "予約を入れる。空きを伝えて、相手が日時を選んだら使う",
"url": "https://saas.example.com/voicast/tools",
"confirm": true,
"parameters": {
"properties": {
"start": { "type": "string", "description": "開始の日時(ISO 8601)" },
"name": { "type": "string" },
"phone": { "type": "string" }
},
"required": ["start", "name"]
}
}
]| Field | Required | Description |
|---|---|---|
name |
yes | Starts with a lowercase letter; lowercase letters, digits and _, up to 40 chars. finish is reserved. Must not clash with connection tool names |
description |
yes | When to use it (up to 500 chars). The AI reads this to decide |
url |
yes | Where to call. https with a public host name only (no literal IP addresses, localhost or internal names). Up to 500 chars |
parameters |
Arguments. Up to 20 properties of type string, number, integer or boolean; enum up to 50 values |
|
confirm |
When true, the AI reads the details (date, name and so on) back and waits for the caller to agree before calling. Use it for tools that change data, such as bookings and cancellations |
An agent can have up to 10 tools. Before using a tool, the AI tells the caller it is checking (
The request#
When the AI decides to use a tool, voicast POSTs to its url.
POST https://saas.example.com/voicast/tools
content-type: application/json
user-agent: voicast-tools/1
webhook-id: msg_6f1c…
webhook-timestamp: 1790000042
webhook-signature: v1,Rk9x…
idempotency-key: toolu_01AbC…
{
"type": "tool.call",
"tool": "find_free_slots",
"tool_call_id": "toolu_01AbC…",
"arguments": { "date": "2026-10-07" },
"call": {
"id": "call_…",
"channel": "phone",
"provider_call_id": "CA…",
"from": "+819012345678",
"tenant": { "id": "ten_…", "external_id": "clinic-123" }
}
}| Field | Description |
|---|---|
tool |
The tool name |
tool_call_id |
Unique per tool call. Same as the idempotency-key header |
arguments |
The arguments the AI chose |
call.tenant.external_id |
Your customer ID, to pick the right booking calendar and so on |
call.from |
The caller's number (phone calls where your TwiML passed from) |
The response#
Return 2xx with JSON. The JSON goes to the AI as is.
{ "slots": ["2026-10-07T10:00:00+09:00", "2026-10-07T14:30:00+09:00"] }- voicast waits up to 8 seconds, then treats the call as failed.
- Non-2xx responses, non-JSON responses, timeouts and connection failures reach the AI as a failure; it apologizes and offers a callback from staff.
- Long JSON is cut at 4,000 characters before it reaches the AI. Return only what the AI needs.
- Redirects (3xx) are not followed.
- Return expected outcomes such as "no free slots" or "not found" as 2xx, e.g.
{ "slots": [] }or{ "found": false, "message": "その番号の方は見つかりませんでした" }, so the AI can say so naturally.
Verifying the signature#
Requests are signed with Standard Webhooks. The steps and code (Node.js, Python, PHP, Go) are the same as in Results; only the secret differs.
The tools signing secret (whsec_…) is one per environment. Read it in the environment settings in the dashboard (admin or above), with GET /v1/tools/secret, or with the MCP tool get_tools_secret; rotate it there, with POST /v1/tools/secret/rotate, or with rotate_tools_secret. For 24 hours after a rotation, requests carry signatures with both the old and new secret, so you have time to switch (libraries accept both secrets).
import express from 'express';
import { Webhook } from 'standardwebhooks';
const wh = new Webhook(process.env.VOICAST_TOOLS_SECRET); // whsec_…
const app = express();
app.post('/voicast/tools', express.raw({ type: 'application/json' }), async (req, res) => {
let body;
try {
body = wh.verify(req.body, req.headers);
} catch {
return res.sendStatus(401);
}
const clinic = body.call.tenant.external_id;
switch (body.tool) {
case 'find_free_slots':
return res.json({ slots: await findSlots(clinic, body.arguments.date) });
case 'create_booking':
// Run each tool_call_id only once
return res.json(await createBookingOnce(body.tool_call_id, clinic, body.arguments));
default:
return res.status(404).json({ error: 'unknown tool' });
}
});Idempotency#
Tools that change data, such as booking, should run once even if the same request arrives twice. Store the tool_call_id (the idempotency-key header) and return the earlier result for a repeated ID.
async function createBookingOnce(toolCallId, clinic, args) {
const done = await db.toolRuns.get(toolCallId);
if (done) return done.result;
const result = await createBooking(clinic, args);
await db.toolRuns.put(toolCallId, { result });
return result;
}Tools that need confirmation#
For tools with confirm: true, the AI reads the details back and calls the tool only after the caller says yes. Always set it on tools that create, change, cancel or register something. Read-only tools (checking slots, finding a customer) do not need it.
Good practice#
- The AI never answers beyond what a tool returned. Put anything it should say (prices, directions) in the response or in the agent's
faq. - Return only the personal data the AI needs (for example, not a full date of birth).
- Do nothing until the signature is verified. Reject unsigned requests and stale
webhook-timestampvalues.