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

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
BodyBare tool argumentstelephony.tool / web.tool envelope
HeadersYour endpoint.headers + X-ThunderPhone-Call-ID + X-ThunderPhone-SignatureContent-Type + 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: abc123...
X-ThunderPhone-Call-ID: 987654321
X-Api-Key: your-api-key

Custom 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 secret
  • 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 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 / DELETE tools sign the empty byte string
Python
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}
Node.js
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.