ThunderPhone 2.0 is live.Self-serve, from 2¢/min.Read the announcement

Function Tools

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

  1. You define tools with a schema (what arguments the tool accepts)
  2. You provide an endpoint configuration (where ThunderPhone calls your API) — or leave it off to receive tool calls on your org webhook
  3. During a call, the AI decides when to use a tool based on the conversation
  4. ThunderPhone calls your endpoint with the tool arguments
  5. Your API response is fed back to the AI to continue the conversation
CapabilityWhere it runsSetup
Built-in toolsThunderPhonePrompt instructions; some tools also need an agent setting
App connectionsThunderPhone and the connected providerConnect the account and attach approved actions
API connections and function toolsYour HTTP APIDefine the endpoint and schema, or receive function calls through a webhook
MCP serversA remote MCP serverAdd 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

FieldTypeRequiredDescription
timeoutnumberNoMaximum execution time in seconds (default: 20, maximum: 180)

Function Definition

FieldTypeRequiredDescription
namestringYesUnique identifier for the tool
descriptionstringYesExplains to the AI when to use this tool
parametersobjectYesJSON Schema for tool arguments

Endpoint Configuration

FieldTypeRequiredDescription
urlstringYesYour API endpoint URL
methodstringNoHTTP method (default: POST)
headersobjectNoCustom headers to include

Two invocation paths

Which request your server receives depends on whether the tool has an endpoint:

Tool with endpointTool without endpoint
Where the request goesDirectly to endpoint.urlYour org's legacy webhook URL
BodySelected request format (bare arguments by default)telephony.tool / web.tool envelope
HeadersYour endpoint.headers + X-ThunderPhone-Call-ID + X-ThunderPhone-Signature-V2 + X-ThunderPhone-SignatureContent-Type + X-ThunderPhone-Signature-V2 + X-ThunderPhone-Signature
Signing keyOrg webhook secretOrg 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-key

Custom 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 signatures
  • X-ThunderPhone-Signature — legacy HMAC-SHA256 of the exact request-body bytes, same key, still sent for existing verifiers
  • X-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-V2 signs 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 for GET / DELETE. It is still sent, unchanged.

The SDK helpers check the V2 header, including the 5-minute window:

Python
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}
Node.js
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.