---
title: "Build a tool integration (API)"
description: "Let your agent call your APIs mid-conversation — search a database, create a ticket, look up an order."
---

A **tool integration** is a reusable HTTP endpoint that an agent can
invoke during a call. You give ThunderPhone a JSON-schema description
of the tool plus an endpoint URL; the agent decides when to call it
based on the conversation, and ThunderPhone makes the outbound HTTP
request from its servers and returns the response to the agent.

<Note>
  The dashboard covers most tool needs without this API: **Connections
  → Apps** connects Slack, HubSpot, Salesforce, Google Calendar,
  Google Sheets, and Cal.com in a few OAuth clicks; **Connections →
  APIs** turns any HTTP API into an agent action (paste a cURL command
  and an AI wizard drafts the tool, with a built-in Test Request); and
  **Connections → MCP** adds MCP servers. See
  [Connections](/guides/concepts#connections). This guide is the raw
  API underneath the APIs surface.
</Note>

This guide walks through building a weather-lookup tool end-to-end.

## Anatomy of a tool

Two pieces:

1. **The schema** — an OpenAI-style function definition
   (`{type: "function", function: {name, description, parameters}}`)
   that tells the LLM what the tool does and what arguments it takes.
2. **The endpoint** — the URL ThunderPhone's servers call when the
   LLM decides to use the tool. The request is JSON POST with the
   LLM's chosen arguments as the body.

## 1. Choose an editor

<CardGroup cols={2}>
  <Card title="Dashboard" icon="window-maximize">
    Open **Connections → APIs**, create or edit the API connection,
    switch the parameter editor to **JSON**, and add the format there.
  </Card>
  <Card title="Integrations API" icon="plug">
    Create the spec with `POST /v1/integrations`, or update it with
    `PATCH /v1/integrations/{id}`.
  </Card>
</CardGroup>

Both paths create a saved integration. Attach that integration to the
agent after saving it. The Agents API has no writable inline `tools`
field. This guide uses the integrations API path.

## 2. Create the integration

```bash
curl -X POST https://api.thunderphone.com/v1/integrations \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "display_name": "Weather API",
    "spec": {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Return the current weather for a zip code.",
        "parameters": {
          "type": "object",
          "properties": {
            "zip": { "type": "string", "description": "5-digit US ZIP code" }
          },
          "required": ["zip"]
        }
      }
    },
    "endpoint_url":    "https://api.example.com/weather",
    "endpoint_method": "GET",
    "headers": [
      { "key": "X-Api-Key", "value": "your-provider-key" }
    ]
  }'
```

Save the returned `id` (a UUID). Header values are write-only: responses
list header names only, and a later `PATCH` can send a header without a
`value` to keep the stored one.

<Tip>
  Spend real effort on the `description` of the tool and of each
  parameter. The LLM uses these strings at runtime to decide whether
  and how to call the tool. Vague descriptions → vague tool calls.
</Tip>

### Declare `format: "email"` on address parameters

A parameter that receives an email address should say so in its
schema:

```json
"email": { "type": "string", "format": "email", "description": "The caller's email address" }
```

`format` is more than a hint. For a resolved email schema, before your
endpoint is called ThunderPhone trims the value, lower-cases the
domain, turns the standalone English words `at`, `dot`, `underscore`,
`dash`, and `hyphen` into their characters, and removes whitespace
directly around `@`, `.`, `_`, and `-`. The words have the same meaning
whether or not the transcript already contains a literal `@`:
`"john dot smith at gmail dot com"` becomes
`john.smith@gmail.com`.

Any other internal whitespace is rejected rather than silently joined.
Spoken separator words are English-only; non-English or unrecognized
spaced forms fail closed. Valid internationalized domains and SMTPUTF8
local parts are accepted. Punycode input remains punycode and Unicode
domain input remains Unicode after parser normalization, so your API
receives the caller-supplied conventional representation. If the final
value is invalid, the tool is **not called**. The agent receives
`invalid_email_argument` telling it
to confirm the spelling with the caller and resend the literal address.

An omitted optional email is untouched. `null`, an empty string, or a
whitespace-only string is also untouched when the property is optional
or nullable; the same values are rejected for a required, non-nullable
email.

Local schema references such as `#/$defs/email` and
`#/definitions/email`, plus `anyOf`, `oneOf`, and `allOf`, are inspected
with cycle and depth limits. A non-local or unresolvable `$ref` is a
known enforcement limit and is passed through unchanged, as is a call
whose tool snapshot has no usable schema. Keep email schemas local when
you need the gate to apply.

Parameters without an enforced email format are passed through exactly
as the model produced them.

Supported formats are `date-time`, `time`, `date`, `duration`,
`email`, `hostname`, `ipv4`, `ipv6`, and `uuid`; only `email` is
normalized and enforced today.

## 3. Sandbox-test the endpoint

Before you link the integration to an agent, fire a signed request
from ThunderPhone's servers to confirm connectivity:

```bash
curl -X POST https://api.thunderphone.com/v1/integrations/test-request \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url":    "https://api.example.com/weather?zip=94110",
    "method": "GET",
    "headers": { "X-Api-Key": "your-provider-key" }
  }'
```

```json Response
{
  "ok": true,
  "status": 200,
  "elapsed_ms": 187,
  "response_headers": { "content-type": "application/json" },
  "response_preview": "{\"temperature_f\": 64, ...}"
}
```

This test also hardens ThunderPhone's SSRF guards — requests to
localhost or private IP ranges return `400 code=url_not_allowed`.

## 4. Link the integration to an agent

Attach via `integration_ids` when you create or update an agent:

```bash
curl -X PATCH https://api.thunderphone.com/v1/agents/12 \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "integration_ids": ["f9b5a1a4-..."]
  }'
```

You can link many integrations to one agent. The agent's prompt can
reference them by name — "use `get_weather` when the caller asks
about conditions" — or it can discover them implicitly from the
schema descriptions.

## 5. Implement the endpoint

When the agent invokes the tool, ThunderPhone sends a signed POST to
your `endpoint_url`:

```
POST /weather HTTP/1.1
Host: api.example.com
X-Api-Key: your-provider-key
X-ThunderPhone-Signature-V2: t=<unix seconds>,v1=<HMAC-SHA256 hex>
X-ThunderPhone-Signature: <HMAC-SHA256 hex>
X-ThunderPhone-Call-ID: 987654321
Content-Type: application/json

{"zip": "94110"}
```

Your server responds with JSON that gets handed back to the LLM:

```json
{"temperature_f": 64, "condition": "Partly cloudy", "wind_mph": 8}
```

The LLM ingests that response and speaks a human summary to the
caller.

<Warning>
  The signature is computed over the raw request body using the same
  `secret` as your webhook endpoint. **Verify it** — tool endpoints
  are internet-facing and subject to the same spoofing concerns as
  webhooks. See
  [Verify webhook signatures](/guides/verify-webhook-signatures).
</Warning>

## 6. Test the loop

Run a [mic session](/api-reference/mic-sessions) against the agent
and ask the question your tool handles ("What's the weather in
94110?"). The call's transcript shows the full round trip:

```json
{
  "call_id": 987654321,
  "transcripts": [
    { "role": "user",
      "content": "What's the weather in 94110?" },
    { "role": "tool_call",
      "content": "{\"tool_call\": \"get_weather\", \"arguments\": {\"zip\": \"94110\"}}" },
    { "role": "tool_response",
      "content": "{\"tool_name\": \"get_weather\", \"response\": {\"temperature_f\": 64, \"condition\": \"Partly cloudy\"}}" },
    { "role": "agent",
      "content": "It's 64 degrees and partly cloudy." }
  ]
}
```

You can pull this via
[`GET /v1/calls/{call_id}/transcript`](/api-reference/calls#get-transcript);
the raw event stream (with per-entry timing and audio offsets) is at
[`GET /v1/calls/{call_id}/history`](/api-reference/calls#get-history).

In call-listing and analytics surfaces, ThunderPhone now reports test calls separately from real traffic. Browser Talk tests and simulations can reuse a real phone number and may look inbound, but they do not count toward production call totals, grades, or issue metrics.

## Common gotchas

<AccordionGroup>
  <Accordion title="Agent never calls the tool">
    The LLM decides based on the tool's description. If the caller's
    question doesn't match the description, the model won't invoke
    the tool. Tighten the description (add common synonyms and
    phrasing) or mention it explicitly in the agent prompt ("When the
    caller asks about weather, use `get_weather`.").
  </Accordion>

  <Accordion title="Tool returns too much data">
    Responses over 6 kB are truncated in the transcript preview. Return
    just the fields the LLM needs — not your whole row.
  </Accordion>

  <Accordion title="Timeouts">
    Tool endpoints have a 10-second default timeout. If you need longer,
    handle it asynchronously: return `{"status": "pending", "request_id": "..."}`
    and surface the result via a separate tool call.
  </Accordion>

  <Accordion title="Versioning">
    Every integration `PATCH` creates a new revision. Inspect
    [`GET /v1/integrations/{id}/versions`](/api-reference/integrations#version-history)
    to see who changed what. If you break a tool's schema, you can
    roll back manually by PATCH-ing an older snapshot back in.
  </Accordion>
</AccordionGroup>

---

## Next steps

<CardGroup cols={2}>
  <Card title="Integrations reference" icon="plug" href="/api-reference/integrations">
    CRUD, transfer, version history.
  </Card>
  <Card title="Function Tools spec" icon="screwdriver-wrench" href="/tools/overview">
    Full JSON schema grammar and the signed-endpoint contract.
  </Card>
  <Card title="Verify signatures" icon="shield-check" href="/guides/verify-webhook-signatures">
    Apply the webhook-signature pattern to tool endpoints.
  </Card>
  <Card title="Transcript + history API" icon="phone" href="/api-reference/calls">
    Inspect the full round-trip of a tool call.
  </Card>
</CardGroup>
