> ## Documentation Index
> Fetch the complete documentation index at: https://hobbyist-e43fa225.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect a voice agent to a Twilio phone number

> Connect a Microsoft Foundry voice-first agent to a Twilio phone number so callers reach the agent over the phone.

export const ZonePivot = ({group, options = [], defaultValue, label = "Choose an experience"}) => {
  const values = options.map(option => option.id);
  const optionKey = options.map(option => `${option.id}:${option.title}`).join("|");
  const [activePivot, setActivePivot] = useState(defaultValue || values[0]);
  const slugify = value => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
  const resolvePivot = () => {
    if (typeof window === "undefined") return defaultValue || values[0];
    const params = new URLSearchParams(window.location.search);
    const requested = params.get("pivots");
    if (requested) {
      const requestedIds = requested.split(",").map(value => value.trim()).filter(Boolean);
      const match = requestedIds.find(id => values.includes(id));
      if (match) return match;
    }
    const hash = window.location.hash.replace(/^#/, "");
    if (hash) {
      const match = options.find(option => option.id === hash || slugify(option.title) === hash);
      if (match) return match.id;
    }
    try {
      const stored = window.localStorage.getItem(`foundry-zone-pivot:${group}`);
      if (values.includes(stored)) return stored;
    } catch {
      return defaultValue || values[0];
    }
    return defaultValue || values[0];
  };
  const publishPivotChange = value => {
    if (typeof window === "undefined") return;
    window.dispatchEvent(new CustomEvent("foundry-zone-pivot-change", {
      detail: {
        group,
        value
      }
    }));
  };
  const syncTableOfContents = () => {
    if (typeof window === "undefined") return;
    window.requestAnimationFrame(() => {
      const toc = document.getElementById("table-of-contents-content");
      if (!toc) return;
      const links = Array.from(toc.querySelectorAll('a[href^="#"]'));
      for (const link of links) {
        const item = link.closest("li");
        const rawId = link.getAttribute("href")?.slice(1);
        if (!item || !rawId) continue;
        let id = rawId;
        try {
          id = decodeURIComponent(rawId);
        } catch {}
        item.style.display = document.getElementById(id) ? "" : "none";
      }
    });
  };
  useEffect(() => {
    const resolvedPivot = resolvePivot();
    setActivePivot(resolvedPivot);
    publishPivotChange(resolvedPivot);
    window.setTimeout(syncTableOfContents, 0);
  }, [group, defaultValue, values.join("|"), optionKey]);
  const selectPivot = value => {
    setActivePivot(value);
    if (typeof window !== "undefined") {
      try {
        window.localStorage.setItem(`foundry-zone-pivot:${group}`, value);
      } catch {}
      const url = new URL(window.location.href);
      const current = url.searchParams.get("pivots");
      const preserved = current ? current.split(",").map(id => id.trim()).filter(id => id && !values.includes(id)) : [];
      url.searchParams.set("pivots", [...preserved, value].join(","));
      window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`);
    }
    publishPivotChange(value);
    window.setTimeout(syncTableOfContents, 0);
  };
  if (options.length < 2) return null;
  return <div className="not-prose my-6 border-b border-slate-200 pb-3 dark:border-slate-800">
      <div className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
        {label}
      </div>
      <div className="flex flex-wrap gap-2" role="tablist" aria-label={label}>
        {options.map(option => {
    const selected = option.id === activePivot;
    return <button key={option.id} type="button" role="tab" aria-selected={selected} onClick={() => selectPivot(option.id)} className={`rounded-md border px-3 py-1.5 text-sm font-medium transition ${selected ? "border-slate-900 bg-slate-900 text-white shadow-sm dark:border-slate-100 dark:bg-slate-100 dark:text-slate-950" : "border-slate-200 bg-white text-slate-700 hover:border-slate-400 hover:text-slate-950 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-200 dark:hover:border-slate-500"}`}>
              {option.title}
            </button>;
  })}
      </div>
    </div>;
};

export const ZoneContent = ({group, value, options = [], values = [], defaultValue, children}) => {
  const optionKey = options.map(option => `${option.id}:${option.title}`).join("|");
  const [activePivot, setActivePivot] = useState(defaultValue || values[0]);
  const slugify = value => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
  const resolvePivot = () => {
    if (typeof window === "undefined") return defaultValue || values[0];
    const params = new URLSearchParams(window.location.search);
    const requested = params.get("pivots");
    if (requested) {
      const requestedIds = requested.split(",").map(value => value.trim()).filter(Boolean);
      const match = requestedIds.find(id => values.includes(id));
      if (match) return match;
    }
    const hash = window.location.hash.replace(/^#/, "");
    if (hash) {
      const match = options.find(option => option.id === hash || slugify(option.title) === hash);
      if (match) return match.id;
    }
    try {
      const stored = window.localStorage.getItem(`foundry-zone-pivot:${group}`);
      if (values.includes(stored)) return stored;
    } catch {
      return defaultValue || values[0];
    }
    return defaultValue || values[0];
  };
  useEffect(() => {
    setActivePivot(resolvePivot());
  }, [group, defaultValue, values.join("|"), optionKey]);
  useEffect(() => {
    const onPivotChange = event => {
      if (event.detail?.group === group && values.includes(event.detail.value)) {
        setActivePivot(event.detail.value);
      }
    };
    window.addEventListener("foundry-zone-pivot-change", onPivotChange);
    return () => window.removeEventListener("foundry-zone-pivot-change", onPivotChange);
  }, [group, values.join("|")]);
  if (activePivot !== value) return null;
  return <>{children}</>;
};

You can now directly import a phone number you purchased from Twilio and connect it to your voice agent.

<Info>
  Items marked (preview) in this article are currently in public preview. This preview is provided without a service-level agreement, and we don't recommend it for production workloads. Certain features might not be supported or might have constrained capabilities. For more information, see [Supplemental Terms of Use for Microsoft Azure Previews](https://azure.microsoft.com/support/legal/preview-supplemental-terms/).
</Info>

## Prerequisites

* Existing phone numbers purchased from [Twilio](https://www.twilio.com/phone-numbers).
* Twilio **Account SID** and **Auth Token**.
* A saved voice-first agent in a Foundry project that passes browser testing. See [Quickstart: Create a voice-first prompt agent](/get-started/prompt-voice-agent).

<ZonePivot group="api__foundry-portal" options={[{"id": "foundry-portal", "title": "Foundry Portal"}, {"id": "api", "title": "Api"}]} defaultValue="foundry-portal" />

<ZoneContent group="api__foundry-portal" value="foundry-portal" options={[{"id": "foundry-portal", "title": "Foundry Portal"}, {"id": "api", "title": "Api"}]} values={["foundry-portal", "api"]} defaultValue="foundry-portal">
  ## Connect a number in the Foundry portal

  The portal walks you through selecting the Azure Communication Services resource, choosing secure delivery, and entering the identifier. It then creates the project connection, the telephony binding, and the Event Grid subscription for you.

  ### Open the phone-number channel

  1. In Foundry, open **Build** > **Agents**.
  2. Select the voice-first agent.
  3. Open **Channels**.
  4. In **Phone numbers**, select **Add a number**.
  5. Select **Twilio**.

  ### Add the number

  Enter your Twilio *Account SID* and the primary *Auth Token*.

  The number appears in the **Select phone numbers** card after provisioning succeeds.
</ZoneContent>

<ZoneContent group="api__foundry-portal" value="api" options={[{"id": "foundry-portal", "title": "Foundry Portal"}, {"id": "api", "title": "Api"}]} values={["foundry-portal", "api"]} defaultValue="foundry-portal">
  ## Connect a number by using the API

  Replace `{projectEndpoint}` with your Foundry project endpoint and `{agentName}` with the voice agent's name. For each Foundry request, send a Microsoft Entra bearer token for `https://ai.azure.com/` and the `Foundry-Features: VoiceAgents=V1Preview` header.

  ### Create the binding

  Create the binding on the agent. Set `connection` to the project connection name. The optional `phone_number` is a display number for the resource account.

  ```http theme={null}
  POST {projectEndpoint}/agents/{agentName}/telephony/bindings?api-version=v1
  Authorization: Bearer <access-token>
  Foundry-Features: VoiceAgents=V1Preview
  Content-Type: application/json

  {
    "provider": "twilio",
    "connection": "my-acs-connection",
    "resource_account_object_id": "00000000-0000-0000-0000-000000000000",
    "phone_number": "+12065550123",
    "label": "Support line"
  }
  ```

  Don't send the `28:orgid:` prefix, `identifier`, or `provider_config` in this request.

  The service returns `201 Created`, the binding's `id` and `incoming_call_url`, and an `ETag` response header. Save the binding ID for later requests. Use the returned `incoming_call_url` in the next step instead of constructing a callback path.

  ### Read or update the binding

  Read the binding to get its current properties and `ETag`:

  ```http theme={null}
  GET {projectEndpoint}/agents/{agentName}/telephony/bindings/{bindingId}?api-version=v1
  Authorization: Bearer <access-token>
  Foundry-Features: VoiceAgents=V1Preview
  ```

  Use `PATCH` with `application/merge-patch+json` to update the binding. For example, suspend new inbound calls without deleting the binding:

  ```http theme={null}
  PATCH {projectEndpoint}/agents/{agentName}/telephony/bindings/{bindingId}?api-version=v1
  Authorization: Bearer <access-token>
  Foundry-Features: VoiceAgents=V1Preview
  Content-Type: application/merge-patch+json
  If-Match: <etag-from-latest-binding-read>

  {
    "status": "suspended"
  }
  ```

  Set `status` to `active` to accept new calls again. Read the latest `ETag` before each update. You can't change the binding's provider.
</ZoneContent>

## Configure transfer to a person

An agent that can't complete a request should reach a human rather than end the call. Configure named transfer targets for the agent. These targets are separate from its telephony bindings.

First, read the current target list and its `ETag` response header:

```http theme={null}
GET {projectEndpoint}/agents/{agentName}/telephony/transfer_targets?api-version=v1
Authorization: Bearer <access-token>
Foundry-Features: VoiceAgents=V1Preview
```

Replace the target list with `PUT`, using the returned `ETag` in `If-Match`. Include every target you want to keep: this operation replaces the entire list, and an empty array clears it.

```http theme={null}
PUT {projectEndpoint}/agents/{agentName}/telephony/transfer_targets?api-version=v1
Authorization: Bearer <access-token>
Foundry-Features: VoiceAgents=V1Preview
Content-Type: application/json
If-Match: <etag-from-latest-transfer-targets-read>

{
  "transfer_targets": [
    {
      "name": "billing",
      "description": "Billing and payment questions",
      "destination": { "kind": "pstn", "value": "+14255550111" }
    },
    {
      "name": "operator",
      "description": "A person at the front desk",
      "destination": { "kind": "twilio", "value": "28:orgid:00000000-0000-0000-0000-000000000000" }
    }
  ]
}
```

A `twilio` destination can be a Twilio user, or the resource account of a call queue or auto attendant. That's how a voice agent hands a caller back into an existing Twilio call flow.

Give each target a `description` that says when to use it. The agent chooses based on that text.

Transfer requests select a target from the agent's configured list rather than supplying an arbitrary destination.

## Manage a live call

List the agent's calls, and use the service-generated call `id` as `{callId}` in subsequent requests:

```http theme={null}
GET {projectEndpoint}/agents/{agentName}/telephony/calls?api-version=v1
Authorization: Bearer <access-token>
Foundry-Features: VoiceAgents=V1Preview
```

To transfer an active call to the configured `operator` target:

```http theme={null}
POST {projectEndpoint}/agents/{agentName}/telephony/calls/{callId}:transfer?api-version=v1
Authorization: Bearer <access-token>
Foundry-Features: VoiceAgents=V1Preview
Content-Type: application/json

{ "target": "operator" }
```

To end an active call, send a separate request with no request body:

```http theme={null}
POST {projectEndpoint}/agents/{agentName}/telephony/calls/{callId}:end?api-version=v1
Authorization: Bearer <access-token>
Foundry-Features: VoiceAgents=V1Preview
```

Both operations return the call record. To inspect the call's current status and lifecycle events, use `GET {projectEndpoint}/agents/{agentName}/telephony/calls/{callId}?api-version=v1`.

## Configure audio for phone calls

Phone networks carry narrowband audio, so tune the agent for the channel:

* Set `noise_reduction` to `azure_deep_noise_suppression` for contact center traffic.
* Increase `silence_duration_ms`. Callers on a phone pause more than callers at a keyboard.
* Add `phrase_list` hints for the identifiers callers read aloud, such as order or policy numbers.
* Attach the `end_conversation` system tool so the agent can end a completed call.

See [Configure a voice agent](/agents/configure-voice-agent).

## Test the call

1. From a controlled caller, call the configured number.
2. Confirm the agent answers.
3. Complete a short conversation.
4. Interrupt the agent.
5. Use one safe tool or knowledge path.
6. Test the approved human-handoff behavior.
7. End the call.
8. Confirm that monitoring and trace data appear.

Record:

* Test timestamp and time zone.
* Called number.
* Callee identifier.
* Azure Communication Services resource ID.
* Agent name and version.
* Event Grid delivery result.
* Azure Communication Services call correlation ID.

## Trace phone calls

When a call arrives through telephony, the session's root trace span records the provider, the provider's call ID, and the dialed and calling numbers. You can correlate a Foundry session with a record in your telephony provider.

Caller and callee phone numbers are personal data. Review who can read your project's Application Insights resource before you enable content capture. See [Voice agent tracing, monitoring, and evaluation](/agents/voice-agent-observability).

## Troubleshoot telephony

| Symptom                                                                                    | What to check                                                                                             |
| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| Busy signal and no services event                                                          | Twilio number activation.                                                                                 |
| Call connects but is silent                                                                | Media endpoint reachability, selected model and voice, output device path, and required PCM media format. |
| Call disconnects unexpectedly                                                              | Callback reachability, media errors, service limits, and correlation logs.                                |
| The wrong agent answers                                                                    | Confirm the agent that owns the binding collection and inspect that agent's active version.               |
| The number doesn't appear after refresh                                                    | Query active bindings and confirm provisioning completed; don't rely only on temporary browser state.     |
| A binding update, deletion, or transfer-target replacement fails with a precondition error | Read the resource again, review any concurrent changes, and use the latest `ETag` in `If-Match`.          |

## Disconnect a number

<ZoneContent group="api__foundry-portal" value="foundry-portal" options={[{"id": "foundry-portal", "title": "Foundry Portal"}, {"id": "api", "title": "Api"}]} values={["foundry-portal", "api"]} defaultValue="foundry-portal">
  1. Open the agent **Channels** tab.
  2. In **Phone numbers**, open the number's actions menu.
  3. Select **Disconnect number**.
  4. Confirm.
</ZoneContent>

<ZoneContent group="api__foundry-portal" value="api" options={[{"id": "foundry-portal", "title": "Foundry Portal"}, {"id": "api", "title": "Api"}]} values={["foundry-portal", "api"]} defaultValue="foundry-portal">
  Read the binding to get its current `ETag`, and then delete it by using that value in `If-Match`.

  ```http theme={null}
  GET {projectEndpoint}/agents/{agentName}/telephony/bindings/{bindingId}?api-version=v1
  Authorization: Bearer <access-token>
  Foundry-Features: VoiceAgents=V1Preview
  ```

  ```http theme={null}
  DELETE {projectEndpoint}/agents/{agentName}/telephony/bindings/{bindingId}?api-version=v1
  Authorization: Bearer <access-token>
  Foundry-Features: VoiceAgents=V1Preview
  If-Match: <etag-from-latest-binding-read>
  ```

  A successful deletion returns `204 No Content`.
</ZoneContent>

Disconnecting removes the Foundry binding. It doesn't:

* Release the Twilio number.
* Remove the Twilio resource account.
* Delete Azure Communication Services.
* Remove the bot or app registration.
* Revoke Twilio Phone Extensibility consent.

Coordinate upstream cleanup separately.

## Security checklist

* Use Microsoft Entra authentication and managed identities.
* Don't share Azure Communication Services keys, connection strings, tokens, bot secrets, or portal cookies.
* Validate that the Event Grid topic is the intended Azure Communication Services resource.
* Use the exact public Foundry project webhook endpoint.
* Keep webhook audience and Event Grid delivery identity concepts separate.
* Use a dedicated single-tenant webhook application when governance requires it.
* Don't persist service-generated call callback or media tokens.
* Confirm the binding target before update or deletion.
* Apply recording, consent, disclosure, retention, and privacy requirements.
* Prevent sensitive caller data from being read aloud or unnecessarily stored.

## Related content

* [Configure a voice agent](/agents/configure-voice-agent)
* [Publish and share a voice-first agent](/agents/voice-agent-channels-publish)
* [Voice agent tracing, monitoring, and evaluation](/agents/voice-agent-observability).
* [Best practices for voice-first agents](/agents/voice-agent-best-practice)
* [Call automation in Azure Communication Services](https://learn.microsoft.com/azure/communication-services/concepts/call-automation/call-automation)
* [Azure Event Grid security and authentication](https://learn.microsoft.com/azure/event-grid/security-authentication)
