> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Function Tools

> Let your AI agents call external APIs during conversations

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

<Note>
  Function tools are the bring-your-own-API path. ThunderPhone also
  ships platform-managed tools that need no endpoint:
  [app connections](/guides/connect-apps) (HubSpot, Salesforce, Slack,
  Google Calendar, Google Sheets, Cal.com),
  [API connections](/guides/api-connections), and
  [MCP servers](/guides/mcp-servers).
</Note>

***

## Tool Schema

Each tool follows this structure:

```json theme={null}
{
  "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"
    }
  }
}
```

### 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                   | **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 — with a **20 s** timeout. 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 theme={null}
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.

<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 Body

For `POST` / `PUT` / `PATCH`, the body contains **only** the tool
arguments (no wrapper), serialized canonically (sorted keys, compact
separators):

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

### Response

Return a JSON response with the tool result:

```json theme={null}
{
  "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 theme={null}
{
  "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.

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

* 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

<CodeGroup>
  ```python Python theme={null}
  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}
  ```

  ```javascript Node.js theme={null}
  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 });
  });
  ```
</CodeGroup>

Full recipes — including the empty-body 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 theme={null}
{
  "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="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>
