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
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 | Bare tool arguments | telephony.tool / web.tool envelope |
| Headers | Your endpoint.headers + X-ThunderPhone-Call-ID + X-ThunderPhone-Signature | Content-Type + 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: abc123...
X-ThunderPhone-Call-ID: 987654321
X-Api-Key: your-api-keyCustom headers from your endpoint.headers are always included
verbatim, plus two ThunderPhone-namespaced headers:
X-ThunderPhone-Signature— HMAC-SHA256 of the exact request-body bytes, keyed with your org webhook secretX-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 Body
For POST / PUT / PATCH, the body contains only the tool
arguments (no wrapper), serialized canonically (sorted keys, compact
separators):
{"date":"2025-01-02","service":"consultation"}For GET / DELETE, the arguments are sent as query parameters
and the body is empty — the signature is then 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 over the raw body, like every other webhook.
Signature Verification
Direct tool calls are signed the same way as webhooks:
- HMAC-SHA256 over the exact request-body bytes (the canonical JSON — sorted keys, no extra whitespace)
- Keyed with your org webhook secret
GET/DELETEtools sign the empty byte string
import hmac
import hashlib
def verify_tool_call(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/appointments/search")
async def search_appointments(request: Request):
body = await request.body()
signature = request.headers.get("X-ThunderPhone-Signature", "")
if not verify_tool_call(body, signature, WEBHOOK_SECRET):
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}app.post('/appointments/search', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-thunderphone-signature'] || '';
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!signature ||
signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
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 — including the empty-body 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
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.