---
title: "Function Tools"
description: "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

| Capability | Where it runs | Setup |
| --- | --- | --- |
| [Built-in tools](/guides/built-in-tools) | ThunderPhone | Prompt instructions; some tools also need an agent setting |
| [App connections](/guides/connect-apps) | ThunderPhone and the connected provider | Connect the account and attach approved actions |
| [API connections](/guides/api-connections) and function tools | Your HTTP API | Define the endpoint and schema, or receive function calls through a webhook |
| [MCP servers](/guides/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:

```json
{
  "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 |

<Note>
  The `endpoint` configuration is **not** sent to the AI model—it's only used by ThunderPhone to execute the tool call.
</Note>

---

## 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](/api-reference/organizations#legacy-single-url-webhook) |
| 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

```http
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](/guides/verify-webhook-signatures#signature-v2)
- `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.

<Warning>
  The signature is keyed with the org-level webhook secret from
  [`GET /v1/webhook`](/api-reference/organizations#legacy-single-url-webhook).
  If your org has never configured the legacy webhook, there is no
  secret and tool calls carry **only** `X-ThunderPhone-Call-ID` — a
  handler that hard-fails on a missing signature would reject them.
  Either configure the legacy webhook to get a secret, or put your own
  shared secret in `endpoint.headers`.
</Warning>

### 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:

```json
{"date":"2025-01-02","service":"consultation"}
```

#### Vapi

`vapi` sends Vapi's [`tool-calls` server message](https://docs.vapi.ai/tools/custom-tools)
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:

```json
{
  "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:

```json
{
  "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](https://docs.retellai.com/build/single-multi-prompt/custom-function):

```json
{
  "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](/guides/verify-webhook-signatures#verifying-tool-calls).

### Response

Return a JSON response with the tool result:

```json
{
  "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](/webhooks/events)
delivered to webhook endpoints after execution, this request **is**
the execution — your HTTP response is the tool result.

```json
{
  "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.

<Note>
  Subscribed [webhook endpoints](/webhooks/endpoints) additionally
  receive a non-blocking `telephony.tool` / `web.tool` **notification
  after** each tool executes (whichever path ran it), including the
  tool's response — useful for audit trails. See the
  [events catalog](/webhooks/events#telephony-tool).
</Note>

---

## 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:

<CodeGroup>
```python 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}
```

```javascript 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 });
});
```
</CodeGroup>

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](/guides/verify-webhook-signatures#verifying-tool-calls).

---

## Example: Complete Booking Flow

Here's a set of tools for a complete appointment booking system:

```json
{
  "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

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="Handle errors gracefully">
    Return error messages the AI can understand: `{"error": "No slots available for that date"}` rather than generic 500 errors.
  </Accordion>

  <Accordion title="Keep responses concise">
    Return only what the AI needs to continue the conversation. Large payloads slow down response times.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

---

## Related

<CardGroup cols={2}>
  <Card title="Built-in tools" icon="wrench" href="/guides/built-in-tools">
    Prompt platform-managed call actions without defining an endpoint.
  </Card>
  <Card title="App connections" icon="plug" href="/guides/connect-apps">
    Platform-managed tools for HubSpot, Salesforce, Slack, Google
    Calendar, Google Sheets, and Cal.com — no endpoint required.
  </Card>
  <Card title="MCP servers" icon="server" href="/guides/mcp-servers">
    Attach an MCP server and let the agent call its tools.
  </Card>
  <Card title="API connections" icon="code" href="/guides/api-connections">
    Reusable REST integrations you can attach to agents.
  </Card>
  <Card title="Verify webhook signatures" icon="shield-check" href="/guides/verify-webhook-signatures">
    One verification helper for webhooks and tool calls.
  </Card>
</CardGroup>
