> This is a page from the ElevenLabs documentation. For a complete page index, fetch https://elevenlabs.io/docs/llms.txt. For the full documentation in a single file, fetch https://elevenlabs.io/docs/llms-full.txt.

# Azure Communication Services

## Przegląd

To podejście daje agentowi **numer telefonu**. Dzwoniący wybiera ten numer, [Azure Communication Services](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/call-automation) (ACS) odbiera połączenie przez **dwukierunkowe przesyłanie strumieniowe mediów**, a mały most przekazuje audio PCM między ACS a agentem ElevenLabs przez standardowy [protokół WebSocket agenta](/docs/pl/eleven-agents/libraries/web-sockets). To model contact center / IVR — podobny do wdrożenia [SIP trunking](/docs/pl/eleven-agents/phone-numbers/sip-trunking), gdzie ACS jest operatorem.

Łączy się też z Teams na dwa sposoby: użytkownik Teams z Calling Plan może zadzwonić bezpośrednio na numer ACS albo możesz udostępnić numer przez **Teams Phone Extensibility**, aby połączenia z kontem zasobu Teams trafiały do ACS.

> **Note**
>
> ACS udostępnia numery PSTN tylko w [ograniczonej liczbie krajów](https://learn.microsoft.com/en-us/azure/communication-services/concepts/numbers/sub-eligibility-number-capability).
> Jeśli numer nie jest dostępny w twoim regionie, użyj dostawcy SIP z [SIP trunking](/docs/pl/eleven-agents/phone-numbers/sip-trunking) albo [bota połączeń Graph](/docs/pl/eleven-agents/phone-numbers/microsoft-teams/graph-media-bot).

## Jak to działa

![Dzwoniący wybiera numer ACS; ACS wysyła IncomingCall przez Event Grid do mostu, który odbiera połączenie przez dwukierunkowe przesyłanie mediów PCM 16k i przekazuje je do agenta ElevenLabs przez WebSocket](/docs/_fern-files/elevenlabs.docs.buildwithfern.com/e77a27148e217fc7dc06c25007c1c141913fc831cbfe8a2f57405137646271af/assets/images/conversational-ai/teams-acs-architecture.svg)

Audio ma format **PCM 16 kHz mono** po obu stronach (format wejścia/wyjścia agenta to `pcm_16000`), więc jest przesyłane jako base64 bez resamplingu.

Most udostępnia te ścieżki:

| Ścieżka                  | Cel                                                                                            |
| ------------------------ | ---------------------------------------------------------------------------------------------- |
| `POST /api/incomingCall` | webhook Event Grid: sprawdza subskrypcję, potem `answer_call` z przesyłaniem mediów            |
| `POST /api/callbacks`    | zdarzenia cyklu życia Call Automation (`CallConnected`, `CallDisconnected`, `AddParticipant*`) |
| `GET\|WS /ws`            | gniazdo przesyłania mediów ACS ↔ ElevenLabs                                                    |
| `POST /api/outboundCall` | Opcjonalnie: wykonuje połączenie wychodzące, które łączy odbierającego z agentem               |

## Wymagania

1. **Płatna** subskrypcja Azure (MCA / EA / Pay-As-You-Go) — bezpłatne, próbne i sponsorskie subskrypcje nie mogą kupować numerów.
2. [Zasób Azure Communication Services](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/create-communication-resource).
3. Host HTTPS dla mostu z publicznym WebSocketem (Azure Container Apps, App Service lub VM).
4. [Agent ElevenLabs](/docs/pl/eleven-agents/quickstart) ustawiony na **PCM 16000 Hz** po obu stronach: format wyjścia TTS na karcie **Voice**, format audio wejścia użytkownika na karcie **Advanced**.

## Uprawnienia i role

| Zakres            | Rola / uprawnienie                           | Po co                                                                        |
| ----------------- | -------------------------------------------- | ---------------------------------------------------------------------------- |
| Azure RBAC        | **Contributor** w grupie zasobów             | tworzenie zasobu ACS, Container App i subskrypcji Event Grid                 |
| Subskrypcja Azure | **Owner lub Contributor** w subskrypcji      | kupowanie numerów telefonów (w przeciwnym razie opcja zakupu jest wyłączona) |
| Rozliczenia       | Typ subskrypcji **MCA / EA / Pay-As-You-Go** | subskrypcje bezpłatne, próbne, sponsorskie i Dev nie mogą kupować numerów    |

> **Note**
>
> Przy roli **Contributor** (nie Owner) `az containerapp up` nie może utworzyć przypisania roli ACR pull
> dla tożsamości zarządzanej. Włącz użytkownika administratora rejestru i przypnij go zamiast tego — zobacz ostrzeżenie w kroku 2.

## Krok 1 — Utwórz zasób ACS i numer

```bash
RG=my-rg
# Register providers (once)
az provider register -n Microsoft.Communication --wait
az provider register -n Microsoft.EventGrid --wait

# Create the ACS resource
az communication create --name my-acs --resource-group $RG \
  --location global --data-location unitedstates
```

Kup numer w zasobie (Portal → twój zasób ACS → **Phone numbers → Get** albo [SDK phone-numbers](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/telephony/get-phone-number)). Dla agenta, który **odbiera** połączenia, wystarczy numer z opcją **inbound calling**; dodaj funkcję **outbound**, jeśli chcesz też używać `/api/outboundCall`.

![Widok Phone numbers zasobu ACS z listą aktywnych numerów i ich
możliwościami połączeń](/docs/_fern-img/9d14034ee9f81e2ebcf64e30371f914ef6f87a8da30ac250e702662f2e5e1cc1.webp)

Aby sprawdzić to z CLI (wymaga `az extension add --name communication`) i pobrać connection string używany przez most jako `ACS_CONNECTION_STRING`:

```bash
CONN=$(az communication list-key -n my-acs -g $RG --query primaryConnectionString -o tsv)
az communication phonenumber list --connection-string "$CONN" --query "[].phoneNumber"
```

## Krok 2 — Wdróż most

Most to mała aplikacja Flask + `flask-sock` używająca `azure-communication-callautomation`. Główna część przepływu przychodzącego:

**`bridge.py (fragment)`**

```python title="bridge.py (fragment)"
from azure.communication.callautomation import (
    CallAutomationClient, MediaStreamingOptions, StreamingTransportType,
    MediaStreamingContentType, MediaStreamingAudioChannelType, AudioFormat,
)

@app.route("/api/incomingCall", methods=["POST"])
def incoming_call():
    for event in request.get_json():
        # Event Grid subscription validation handshake
        if event.get("eventType") == "Microsoft.EventGrid.SubscriptionValidationEvent":
            return jsonify({"validationResponse": event["data"]["validationCode"]})

        if event.get("eventType") == "Microsoft.Communication.IncomingCall":
            client = CallAutomationClient.from_connection_string(ACS_CONNECTION_STRING)
            client.answer_call(
                incoming_call_context=event["data"]["incomingCallContext"],
                callback_url=f"https://{HOST}/api/callbacks",
                media_streaming=MediaStreamingOptions(
                    transport_url=f"wss://{HOST}/ws",
                    transport_type=StreamingTransportType.WEBSOCKET,
                    content_type=MediaStreamingContentType.AUDIO,
                    audio_channel_type=MediaStreamingAudioChannelType.MIXED,
                    start_media_streaming=True,
                    enable_bidirectional=True,
                    audio_format=AudioFormat.PCM16_K_MONO,
                ),
            )
    return jsonify({"status": "ok"})
```

W gnieździe `/ws` przekazuj PCM16 w obie strony: wysyłaj ramki ACS `AudioData` do ElevenLabs jako `{"user_audio_chunk": "<base64>"}`, a audio agenta odsyłaj jako `{"Kind":"AudioData","AudioData":{"Data":"<base64>"},"StopAudio":null}`. Pierwsza ramka wysyłana przez ACS to `AudioMetadata` (uzgodniony format) — zapisz ją w logach i zignoruj. Po stronie ElevenLabs używany jest standardowy [protokół WebSocket agenta](/docs/pl/eleven-agents/libraries/web-sockets).

> **Note**
>
> ACS używa innej wielkości liter w JSON w zależności od kierunku: ramki przychodzące, które **wysyła**, są w camelCase (`kind`,
> `audioData.data`), a ramki wychodzące, których **oczekuje**, w PascalCase (`Kind`, `AudioData.Data`,
> `StopAudio`). Nie mieszaj tych dwóch formatów — poniższy przekaźnik je odwzorowuje.

**`bridge.py — przekaźnik mediów`**

```python title="bridge.py — przekaźnik mediów"
import asyncio, json, os, queue, threading, websockets
from flask_sock import Sock

sock = Sock(app)
AGENT_ID = os.environ["ELEVENLABS_AGENT_ID"]
# US default; data residency: wss://api.eu.residency.elevenlabs.io, .in., or .sg.
EL_ORIGIN = os.environ.get("ELEVENLABS_ORIGIN", "wss://api.elevenlabs.io")
EL_WS = f"{EL_ORIGIN}/v1/convai/conversation?agent_id={AGENT_ID}"

@sock.route("/ws")
def media_stream(ws):
    loop = asyncio.new_event_loop()
    el = {"ws": None}
    to_acs = queue.Queue()  # outbound frames; only this handler thread touches `ws`

    async def el_session():
        async with websockets.connect(EL_WS) as elws:
            el["ws"] = elws
            await elws.send(json.dumps({"type": "conversation_initiation_client_data"}))
            async for msg in elws:
                data = json.loads(msg)
                kind = data.get("type")
                if kind == "audio":  # agent audio -> caller
                    b64 = data["audio_event"]["audio_base_64"]
                    to_acs.put({"Kind": "AudioData", "AudioData": {"Data": b64}, "StopAudio": None})
                elif kind == "ping":
                    await elws.send(json.dumps({"type": "pong", "event_id": data["ping_event"]["event_id"]}))
                elif kind == "interruption":  # barge-in
                    to_acs.put({"Kind": "StopAudio", "AudioData": None, "StopAudio": {}})

    threading.Thread(target=lambda: loop.run_until_complete(el_session()), daemon=True).start()

    # Keep all ACS-socket I/O on this one thread: receive with a short timeout,
    # then drain any audio the ElevenLabs thread queued. Sending from the other
    # thread would race flask-sock and corrupt the stream.
    try:
        while True:
            raw = ws.receive(timeout=0.02)  # None when no frame arrived this tick
            if raw:
                evt = json.loads(raw)
                if evt.get("kind") == "AudioData" and el["ws"]:  # caller audio -> agent
                    asyncio.run_coroutine_threadsafe(
                        el["ws"].send(json.dumps({"user_audio_chunk": evt["audioData"]["data"]})), loop)
            while not to_acs.empty():
                ws.send(json.dumps(to_acs.get_nowait()))
    except Exception:
        pass  # ACS socket closed
```

> **Note**
>
> Ten przekaźnik jest celowo prosty. W środowisku produkcyjnym dodaj logowanie, ponowne łączenie i łagodne
> zamykanie. Pełna lista komunikatów jest w [dokumentacji WebSocket ](/docs/pl/eleven-agents/libraries/web-sockets).

> **Note**
>
> `EL_WS` łączy się z **publicznym** agentem. W przypadku agenta prywatnego most powinien po stronie serwera pobrać krótkotrwały
> podpisany URL — `GET /v1/convai/conversation/get-signed-url?agent_id=...` z twoim kluczem API
> — i połączyć się z otrzymanym URL-em. W przypadku [rezydencji danych](/docs/pl/overview/administration/data-residency) ustaw `ELEVENLABS_ORIGIN` na host rezydencji
> (`wss://api.eu.residency.elevenlabs.io`, `.in.` lub `.sg.`) — żądania podpisanego URL używają odpowiadającego hosta `https://`.

Wdróż w Azure Container Apps i zapisz publiczny FQDN:

```bash
az containerapp up --name acs-el-bridge --resource-group $RG \
  --source . --ingress external --target-port 8080 \
  --env-vars ELEVENLABS_AGENT_ID=$AGENT_ID \
    ELEVENLABS_ORIGIN=wss://api.elevenlabs.io

FQDN=$(az containerapp show -n acs-el-bridge -g $RG \
  --query properties.configuration.ingress.fqdn -o tsv)
```

Następnie ustaw w aplikacji `BRIDGE_PUBLIC_HOST=$FQDN` oraz connection string ACS (jako sekret).

> **Warning**
>
> Przy roli **Contributor** (nie Owner) `az containerapp up` nie może utworzyć roli ACR
> pull dla tożsamości zarządzanej. Włącz użytkownika administratora rejestru (`az acr update --admin-enabled true`) i przypnij go
> przez `az containerapp registry set`, a potem uruchom `az containerapp update --image ...`.

## Krok 3 — Przekieruj IncomingCall do mostu

Utwórz subskrypcję Event Grid w zasobie ACS, która wysyła `IncomingCall` do mostu. Walidacja mostu (powyżej) automatycznie kończy tworzenie subskrypcji.

```bash
ACS_ID=$(az communication show -n my-acs -g $RG --query id -o tsv)
az eventgrid event-subscription create \
  --name acs-incomingcall \
  --source-resource-id "$ACS_ID" \
  --endpoint "https://$FQDN/api/incomingCall" \
  --endpoint-type webhook \
  --included-event-types Microsoft.Communication.IncomingCall

# Verify — should print "Succeeded"
az eventgrid event-subscription show --name acs-incomingcall \
  --source-resource-id "$ACS_ID" --query provisioningState -o tsv
```

Subskrypcja pojawi się w widoku **Events** zasobu ACS:

![Widok Events zasobu ACS z subskrypcją webhooka acs-incomingcall filtrowaną
według
Microsoft.Communication.IncomingCall](/docs/_fern-img/f310aed9637e06b81906ca188350e3e721aaa5f4de69af2b3b6e7d157817a1ea.webp)

Zadzwoń na numer — agent odbierze.

## Łączenie z Teams

* **Wybieranie bezpośrednie:** użytkownik Teams z Teams Phone + Calling Plan może zadzwonić na numer ACS jak na każdy numer zewnętrzny.
* **Konto zasobu Teams (TPE):** połącz konto zasobu Teams z zasobem ACS przez [Teams Phone Extensibility](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/tpe/teams-phone-extensibility-quickstart), aby połączenia z kontem zasobu uruchamiały ten sam przepływ `IncomingCall` → most.

## Koniec połączenia

Gdy agent kończy rozmowę (np. narzędziem **End Call**), ElevenLabs zamyka WebSocket. Rozłącz połączenie ACS, aby dzwoniący nie został na martwej linii:

```python
CallAutomationClient.from_connection_string(ACS_CONNECTION_STRING) \
    .get_call_connection(call_connection_id).hang_up(is_for_everyone=True)
```

## Ciepłe przekazanie do człowieka

Natywne narzędzia przekazywania ElevenLabs działają tylko wtedy, gdy ElevenLabs obsługuje telefonię. Tutaj agent uruchamia więc **niestandardowe narzędzie klienta** (np. `transfer_to_human`), które most obsługuje przez **dodanie** człowieka do trwającego połączenia za pomocą `add_participant` (ciepłe przekazanie), zamiast ślepego transferu:

```python
conn = client.get_call_connection(call_connection_id)
conn.add_participant(
    PhoneNumberIdentifier(human_number),
    source_caller_id_number=PhoneNumberIdentifier(your_outbound_number),
    invitation_timeout=30,
)
# then mute the bot and skip the end-of-call hangup so the human's leg survives
```

ACS wysyła wywołania zwrotne `AddParticipantSucceeded` / `AddParticipantFailed` do `/api/callbacks`. Zwróć do agenta `client_tool_result`, aby mógł wypowiedzieć komunikat o przekazaniu. Konfigurację po stronie agenta znajdziesz w [narzędziach systemowych](/docs/pl/eleven-agents/customization/tools/system-tools/transfer-to-number).

> **Tip**
>
> Ustaw blokadę transferu natychmiast po uruchomieniu narzędzia (przed wywołaniem `add_participant`), bo szybkie zamknięcie EL
> WebSocket może kolidować z rozłączeniem i zakończyć połączenie, zanim człowiek dołączy.

## Rozwiązywanie problemów

#### IncomingCall nie dociera do mostu

Sprawdź, czy subskrypcja Event Grid została utworzona (`provisioningState: Succeeded`) oraz czy
`/api/incomingCall` mostu zwróciło odpowiedź walidacyjną. Sprawdź, czy numer ma połączenia przychodzące
i należy do tego samego zasobu ACS, co subskrypcja. Na karcie **Filters** subskrypcji typy zdarzeń muszą obejmować **Incoming Call**:

![Karta Filters subskrypcji zdarzeń z typem zdarzenia filtrowanym do
Incoming
Call](/docs/_fern-img/c7026e86d96551d713e4fd7d5753062f643fcd6a67b455866bb8f1bbb6b57eb1.webp)

#### \`CreateCallFailed\` / \`AddParticipantFailed\` dla numeru międzynarodowego

Połączenia wychodzące ACS do niektórych krajów (np. Indii) są ograniczone lub niestabilne. Użyj obsługiwanego
miejsca docelowego albo obsłuż połączenie z człowiekiem przez numer SIP/Operator. Logika mostu pozostaje bez zmian —
to błąd po stronie operatora dla połączenia wychodzącego.

#### Dźwięk jest zniekształcony lub ma niewłaściwą prędkość

Obie strony muszą używać PCM 16 kHz mono. Ustaw format wejścia/wyjścia agenta na `pcm_16000`; most
zapisuje uzgodniony format z `conversation_initiation_metadata`.

#### Nie mogę kupić numeru / numer nie jest dostępny w moim kraju

Zakup numeru wymaga płatnego typu subskrypcji (MCA/EA/PAYG). Jeśli ACS nie oferuje numerów w
twoim kraju, użyj dostawcy [SIP](/docs/pl/eleven-agents/phone-numbers/sip-trunking).

## Przydatne linki

* [Przegląd ACS Call Automation](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/call-automation)
* [Przesyłanie strumieniowe audio ACS](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/audio-streaming-concept)
* [Akcje kontroli połączeń (transfer / dodawanie uczestnika)](https://learn.microsoft.com/en-us/azure/communication-services/how-tos/call-automation/actions-for-call-control)
* [Teams Phone Extensibility](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/tpe/teams-phone-extensibility-quickstart)
* [Protokół WebSocket agenta](/docs/pl/eleven-agents/libraries/web-sockets)
* [SIP trunking ElevenLabs](/docs/pl/eleven-agents/phone-numbers/sip-trunking)