Open in
Function Tools
Give your AI agents function tools that call external APIs mid-conversation — fetch customer data, book appointments, update records — with typed parameters.
Function tools allow your AI agents to invoke external APIs during phone calls. Use them to look up customer data, check availability, book appointments, or perform any action your backend supports.
How It Works
- You define tools with a schema (what arguments the tool accepts)
- You provide an
endpointconfiguration (where ThunderPhone calls your API) — or leave it off to receive tool calls on your org webhook - During a call, the AI decides when to use a tool based on the conversation
- ThunderPhone calls your endpoint with the tool arguments
- Your API response is fed back to the AI to continue the conversation
| Capability | Where it runs | Setup |
|---|---|---|
| Built-in tools | ThunderPhone | Prompt instructions; some tools also need an agent setting |
| App connections | ThunderPhone and the connected provider | Connect the account and attach approved actions |
| API connections and function tools | Your HTTP API | Define the endpoint and schema, or receive function calls through a webhook |
| MCP servers | A remote MCP server | Add the server, discover its tools, and attach it to the agent |
Tool Schema
Each tool follows this structure:
{
"type": "function",
"function": {
"name": "search_appointments",
"description": "Find available appointment slots for a given date",
"parameters": {
"type": "object",
"properties": {
"date": {
"type": "string",
"description": "Date in YYYY-MM-DD format"
},
"service": {
"type": "string",
"description": "Type of service (e.g., 'consultation', 'follow-up')"
}
},
"required": ["date"]
}
},
"endpoint": {
"url": "https://api.example.com/appointments/search",
"method": "POST",
"headers": {
"X-Api-Key": "your-api-key"
}
},
"timeout": 120
}Tool Configuration
| Field | Type | Required | Description |
|---|---|---|---|
timeout | number | No | Maximum execution time in seconds (default: 20, maximum: 180) |
Function Definition
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier for the tool |
description | string | Yes | Explains to the AI when to use this tool |
parameters | object | Yes | JSON Schema for tool arguments |
Endpoint Configuration
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Your API endpoint URL |
method | string | No | HTTP method (default: POST) |
headers | object | No | Custom headers to include |
Two invocation paths
Which request your server receives depends on whether the tool has an
endpoint:
Tool with endpoint | Tool without endpoint | |
|---|---|---|
| Where the request goes | Directly to endpoint.url | Your org's legacy webhook URL |
| Body | Selected request format (bare arguments by default) | telephony.tool / web.tool envelope |
| Headers | Your endpoint.headers + X-ThunderPhone-Call-ID + X-ThunderPhone-Signature-V2 + X-ThunderPhone-Signature | Content-Type + X-ThunderPhone-Signature-V2 + X-ThunderPhone-Signature |
| Signing key | Org webhook secret | Org webhook secret |
Both paths are blocking — the AI is waiting mid-sentence for the
result. The default timeout is 20 s; set the tool's top-level
timeout to allow a longer execution, up to the 180 s platform
maximum. Keep handlers fast. A mix is fine:
on a call whose org has a webhook URL, tools with an endpoint are
called directly and the rest fall back to the webhook.
Direct endpoint calls
When the AI invokes a tool that has an endpoint, ThunderPhone sends
a request to your URL:
Request Headers
POST /appointments/search HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-ThunderPhone-Signature-V2: t=1767225600,v1=5f2c...
X-ThunderPhone-Signature: abc123...
X-ThunderPhone-Call-ID: 987654321
X-Api-Key: your-api-keyCustom headers from your endpoint.headers are always included
verbatim, plus these ThunderPhone-namespaced headers:
X-ThunderPhone-Signature-V2— timestamped HMAC-SHA256 over the method, path, query and body, keyed with your org webhook secret. Verify this one and reject requests more than 5 minutes old; see Verify webhook signaturesX-ThunderPhone-Signature— legacy HMAC-SHA256 of the exact request-body bytes, same key, still sent for existing verifiersX-ThunderPhone-Call-ID— The current call ID
Content-Type: application/json is set unless your endpoint.headers
override it — a custom Content-Type wins.
Request format
Each API connection has a request_format setting. Choose vapi or
retell when an imported tool should keep calling an endpoint built for
that platform. New and existing connections default to standard.
For POST / PUT / PATCH, ThunderPhone serializes the selected
envelope canonically (sorted keys, compact separators). Both signatures
cover these exact bytes.
Standard
standard sends only the tool arguments, with no wrapper. This is the
default and preserves ThunderPhone's original request body byte for byte:
{"date":"2025-01-02","service":"consultation"}Vapi
vapi sends Vapi's tool-calls server message
in the shape Vapi documents. The call.id is the ThunderPhone call ID. The
tool-call id is unique to the invocation and is reused to match the
endpoint's result. The call appears in both toolCallList and
toolWithToolCallList, with function.arguments as a JSON object:
{
"message": {
"type": "tool-calls",
"timestamp": 1725000000123,
"call": { "id": "987654321" },
"toolCallList": [
{
"id": "call_00112233445566778899aabbccddeeff",
"type": "function",
"function": {
"name": "search_appointments",
"arguments": { "date": "2025-01-02", "service": "consultation" }
}
}
],
"toolWithToolCallList": [
{
"type": "function",
"function": { "name": "search_appointments" },
"toolCall": {
"id": "call_00112233445566778899aabbccddeeff",
"type": "function",
"function": {
"name": "search_appointments",
"arguments": { "date": "2025-01-02", "service": "consultation" }
}
}
}
]
}
}Vapi-only fields such as assistant, artifact and the full call object
are not sent.
Return Vapi's response envelope with the matching tool-call ID:
{
"results": [
{
"toolCallId": "call_00112233445566778899aabbccddeeff",
"result": "{\"available_slots\":[\"9:00 AM\",\"2:00 PM\"]}"
}
]
}ThunderPhone gives the AI the unwrapped result string. It also accepts a
JSON value for compatibility with endpoints that already return one. An
error in place of result is returned to the AI as a tool error. If the
endpoint returns a malformed or mismatched Vapi envelope, ThunderPhone logs
a warning and falls back to its standard response parsing instead of failing
the call.
Retell
retell sends the request documented for a Retell custom
function:
{
"name": "search_appointments",
"call": { "call_id": "987654321" },
"args": { "date": "2025-01-02", "service": "consultation" }
}The call object includes only call_id. ThunderPhone does not include
Retell-specific fields it cannot honestly supply at dispatch, such as
agent_id, transcript, transcript_object, metadata, or dynamic
variables. Retell accepts any response body; ThunderPhone passes it
through using the standard response handling.
For GET / DELETE, the arguments are sent as query parameters
and the body is empty regardless of request_format. The V2 signature
covers the query string as sent; the legacy signature is computed over
the empty byte string. See
Verify webhook signatures.
Response
Return a JSON response with the tool result:
{
"available_slots": ["9:00 AM", "2:00 PM", "4:30 PM"],
"timezone": "America/Los_Angeles"
}The response is formatted and provided to the AI to continue the
conversation. Non-JSON responses are wrapped as {"data": "<text>"};
timeouts and connection failures are reported to the AI as errors, so
the agent can apologize and move on rather than stall.
Webhook-mode dispatch
Tools without an endpoint are dispatched to your org's legacy
webhook URL as a signed telephony.tool (phone calls) or web.tool
(web calls) request. Unlike the audit notifications
delivered to webhook endpoints after execution, this request is
the execution — your HTTP response is the tool result.
{
"type": "telephony.tool",
"data": {
"call_id": 987654321,
"tool_name": "search_appointments",
"arguments": { "date": "2026-04-21" },
"from_number": "+14155550199",
"to_number": "+15551234567"
}
}web.tool carries origin_domain instead of from_number /
to_number. Respond with the tool result as JSON — the same response
contract as direct endpoint calls. The request is signed with the org
webhook secret, with both signature headers, like every other webhook.
Signature Verification
Direct tool calls are signed the same way as webhooks, keyed with your org webhook secret:
X-ThunderPhone-Signature-V2signs the timestamp, method, path and query as sent, and the exact body bytes. Reject requests whose timestamp is more than 5 minutes from your clock.X-ThunderPhone-Signature(legacy) signs only the body bytes, the empty byte string forGET/DELETE. It is still sent, unchanged.
The SDK helpers check the V2 header, including the 5-minute window:
import json
from fastapi import FastAPI, HTTPException, Request
from thunderphone import verify_signature
app = FastAPI()
@app.post("/appointments/search")
async def search_appointments(request: Request):
body = await request.body()
query = request.scope.get("query_string", b"").decode("latin-1")
path = request.url.path + (f"?{query}" if query else "")
if not verify_signature(
request.headers.get("X-ThunderPhone-Signature-V2"),
secret=WEBHOOK_SECRET,
method=request.method,
path=path,
body=body,
):
raise HTTPException(status_code=401)
data = json.loads(body)
date = data["date"]
# Look up availability
slots = await get_available_slots(date)
return {"available_slots": slots}import express from "express";
import { verifySignature } from "@thunderphone/sdk";
app.post('/appointments/search', express.raw({type: 'application/json'}), (req, res) => {
const ok = verifySignature({
header: req.header('X-ThunderPhone-Signature-V2'),
secret: WEBHOOK_SECRET,
method: req.method,
path: req.originalUrl,
body: req.body,
});
if (!ok) return res.status(401).send('Invalid signature');
const { date, service } = JSON.parse(req.body);
// Look up availability
const slots = getAvailableSlots(date, service);
res.json({ available_slots: slots });
});Full recipes — the signed string, a worked example, code without the
SDK, the GET / DELETE case and the no-secret caveat — are in Verify webhook signatures.
Example: Complete Booking Flow
Here's a set of tools for a complete appointment booking system:
{
"tools": [
{
"type": "function",
"function": {
"name": "search_appointments",
"description": "Find available appointment slots",
"parameters": {
"type": "object",
"properties": {
"date": { "type": "string", "description": "YYYY-MM-DD" },
"service": { "type": "string" }
},
"required": ["date"]
}
},
"endpoint": {
"url": "https://api.example.com/appointments/search",
"method": "POST",
"headers": { "X-Api-Key": "key" }
}
},
{
"type": "function",
"function": {
"name": "book_appointment",
"description": "Book an appointment at a specific time",
"parameters": {
"type": "object",
"properties": {
"date": { "type": "string", "description": "YYYY-MM-DD" },
"time": { "type": "string", "description": "HH:MM format" },
"customer_name": { "type": "string" },
"customer_phone": { "type": "string" }
},
"required": ["date", "time", "customer_name"]
}
},
"endpoint": {
"url": "https://api.example.com/appointments/book",
"method": "POST",
"headers": { "X-Api-Key": "key" }
}
},
{
"type": "function",
"function": {
"name": "cancel_appointment",
"description": "Cancel an existing appointment",
"parameters": {
"type": "object",
"properties": {
"confirmation_number": { "type": "string" }
},
"required": ["confirmation_number"]
}
},
"endpoint": {
"url": "https://api.example.com/appointments/cancel",
"method": "POST",
"headers": { "X-Api-Key": "key" }
}
}
]
}Best Practices
Write clear descriptions
The description field helps the AI understand when to use the tool. Be specific about what it does and when it's appropriate.
Handle errors gracefully
Return error messages the AI can understand: {"error": "No slots available for that date"} rather than generic 500 errors.
Keep responses concise
Return only what the AI needs to continue the conversation. Large payloads slow down response times.
Use required fields wisely
Mark fields as required only when truly necessary. The AI will ask the user for required information before calling the tool.
Related
Prompt platform-managed call actions without defining an endpoint.
Platform-managed tools for HubSpot, Salesforce, Slack, Google Calendar, Google Sheets, and Cal.com — no endpoint required.
Attach an MCP server and let the agent call its tools.
Reusable REST integrations you can attach to agents.
One verification helper for webhooks and tool calls.