> 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

## Overzicht

Met deze aanpak krijgt je agent een **telefoonnummer**. Een beller belt dit nummer, [Azure Communication Services](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/call-automation) (ACS) neemt op met **bidirectionele mediastreaming**, en een kleine bridge stuurt PCM-audio door tussen ACS en de ElevenLabs-agent via het standaard [agent WebSocket-protocol](/docs/nl/eleven-agents/libraries/web-sockets). Dit is het contactcenter-/IVR-patroon — dezelfde opzet als een implementatie met [SIP-trunking](/docs/nl/eleven-agents/phone-numbers/sip-trunking), met ACS als provider.

Het verbindt ook op twee manieren met Teams: een Teams-gebruiker met een Calling Plan kan het ACS-nummer rechtstreeks bellen, of je kunt het nummer koppelen aan **Teams Phone Extensibility**, zodat oproepen naar een Teams-resourceaccount naar ACS worden gerouteerd.

> **Note**
>
> ACS levert PSTN-nummers alleen in een [beperkt aantal landen](https://learn.microsoft.com/en-us/azure/communication-services/concepts/numbers/sub-eligibility-number-capability).
> Als er in je regio geen nummer beschikbaar is, gebruik dan een SIP-provider met [SIP- trunking](/docs/nl/eleven-agents/phone-numbers/sip-trunking), of de [Graph calling- bot](/docs/nl/eleven-agents/phone-numbers/microsoft-teams/graph-media-bot).

## Zo werkt het

![Een beller belt het ACS-nummer; ACS stuurt IncomingCall via Event Grid naar de bridge, die opneemt met bidirectionele PCM 16k-mediastreaming en deze via een WebSocket doorstuurt naar de ElevenLabs-agent](/docs/_fern-files/elevenlabs.docs.buildwithfern.com/e77a27148e217fc7dc06c25007c1c141913fc831cbfe8a2f57405137646271af/assets/images/conversational-ai/teams-acs-architecture.svg)

Audio is op beide kanten **PCM 16 kHz mono** (het invoer-/uitvoerformaat van de agent is `pcm_16000`), en wordt dus als base64 doorgestuurd zonder resampling.

De bridge biedt deze routes:

| Route                    | Doel                                                                                              |
| ------------------------ | ------------------------------------------------------------------------------------------------- |
| `POST /api/incomingCall` | Event Grid-webhook: valideert het abonnement en voert daarna `answer_call` met mediastreaming uit |
| `POST /api/callbacks`    | Levenscyclusevents van Call Automation (`CallConnected`, `CallDisconnected`, `AddParticipant*`)   |
| `GET\|WS /ws`            | ACS-media-streaming socket ↔ ElevenLabs                                                           |
| `POST /api/outboundCall` | Optioneel: plaats een uitgaande oproep die de opnemende persoon met de agent verbindt             |

## Vereisten

1. Een **betaald** Azure-abonnement (MCA / EA / Pay-As-You-Go) — gratis/proef-/sponsorabonnementen kunnen geen nummers kopen.
2. Een [Azure Communication Services-resource](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/create-communication-resource).
3. Een HTTPS-host voor de bridge met een openbare WebSocket (Azure Container Apps, App Service of een VM).
4. Een [ElevenLabs-agent](/docs/nl/eleven-agents/quickstart) ingesteld op **PCM 16000 Hz** aan beide kanten: TTS-uitvoerformaat op het tabblad **Voice**, audio-invoerformaat van de gebruiker op het tabblad **Advanced**.

## Machtigingen en rollen

| Bereik           | Rol / machtiging                             | Waarom                                                                  |
| ---------------- | -------------------------------------------- | ----------------------------------------------------------------------- |
| Azure RBAC       | **Contributor** voor de resourcegroep        | de ACS-resource, Container App en Event Grid-abonnement maken           |
| Azure-abonnement | **Owner of Contributor** voor het abonnement | telefoonnummers kopen (anders is de koopoptie uitgeschakeld)            |
| Facturering      | Abonnementstype **MCA / EA / Pay-As-You-Go** | gratis-, proef-, sponsor- en Dev-abonnementen kunnen geen nummers kopen |

> **Note**
>
> Met **Contributor** (niet Owner) kan `az containerapp up` de ACR-pullroltoewijzing voor de managed identity niet maken.
> Schakel in plaats daarvan de registry admin-gebruiker in en koppel die — zie de waarschuwing in stap 2.

## Stap 1 — De ACS-resource en het nummer inrichten

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

Koop een nummer in de resource (Portal → je ACS-resource → **Phone numbers → Get**, of via de [phone-numbers SDK](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/telephony/get-phone-number)). Voor een agent die oproepen **beantwoordt**, volstaat een nummer met **inbound calling**; voeg de mogelijkheid **outbound** toe als je ook `/api/outboundCall` wilt gebruiken.

![De blade Phone numbers van de ACS-resource met actieve nummers en hun belmogelijkheden](/docs/_fern-img/9d14034ee9f81e2ebcf64e30371f914ef6f87a8da30ac250e702662f2e5e1cc1.webp)

Om dit via de CLI te verifiëren (vereist `az extension add --name communication`) en de connection string op te halen die de bridge als `ACS_CONNECTION_STRING` gebruikt:

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

## Stap 2 — De bridge implementeren

De bridge is een kleine Flask- + `flask-sock`-app die `azure-communication-callautomation` gebruikt. De kern van de inkomende flow:

**`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"})
```

Stuur op de `/ws`-socket PCM16 in beide richtingen door: stuur ACS-frames van `AudioData` door naar ElevenLabs als `{"user_audio_chunk": "<base64>"}`, en stuur de audio van de agent terug als `{"Kind":"AudioData","AudioData":{"Data":"<base64>"},"StopAudio":null}`. Het eerste frame dat ACS verstuurt is `AudioMetadata` (het overeengekomen formaat) — log dit en negeer het. De ElevenLabs-kant gebruikt het standaard [agent WebSocket-protocol](/docs/nl/eleven-agents/libraries/web-sockets).

> **Note**
>
> ACS gebruikt per richting verschillende JSON-hoofdlettergebruik: inkomende frames die het **verstuurt** gebruiken camelCase (`kind`,
> `audioData.data`), terwijl uitgaande frames die het **verwacht** PascalCase gebruiken (`Kind`, `AudioData.Data`,
> `StopAudio`). Houd de twee vormen gescheiden — de onderstaande relay volgt dit.

**`bridge.py — mediarelay`**

```python title="bridge.py — mediarelay"
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**
>
> Deze relay is bewust minimaal. Voeg voor productie logging, herverbinding en gecontroleerd
> afsluiten toe. De volledige berichtreferentie staat in de [WebSocket- documentatie](/docs/nl/eleven-agents/libraries/web-sockets).

> **Note**
>
> `EL_WS` maakt verbinding met een **openbare** agent. Laat de bridge voor een privéagent server-side een kortdurende
> ondertekende URL aanvragen — `GET /v1/convai/conversation/get-signed-url?agent_id=...` met je API-
> sleutel — en maak in plaats daarvan verbinding met de geretourneerde URL. Stel bij [data- residency](/docs/nl/overview/administration/data-residency) `ELEVENLABS_ORIGIN` in op je
> residency-host (`wss://api.eu.residency.elevenlabs.io`, `.in.` of `.sg.`) — aanvragen voor ondertekende URL's
> gebruiken de bijbehorende `https://`-host.

Implementeer in Azure Container Apps en leg de openbare FQDN vast:

```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)
```

Stel vervolgens `BRIDGE_PUBLIC_HOST=$FQDN` en de ACS-connection string (als secret) in voor de app.

> **Warning**
>
> Met **Contributor** (niet Owner) kan `az containerapp up` de ACR-pullrol voor de managed identity niet maken.
> Schakel de registry admin-gebruiker in (`az acr update --admin-enabled true`) en koppel deze
> met `az containerapp registry set`. Gebruik daarna `az containerapp update --image ...`.

## Stap 3 — IncomingCall naar de bridge routeren

Maak een Event Grid-abonnement voor de ACS-resource dat `IncomingCall` naar de bridge stuurt. De validatiehandshake van de bridge (hierboven) voltooit het abonnement automatisch.

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

Het abonnement verschijnt onder de blade **Events** van de ACS-resource:

![De blade Events van de ACS-resource met het webhookabonnement acs-incomingcall, gefilterd
op
Microsoft.Communication.IncomingCall](/docs/_fern-img/f310aed9637e06b81906ca188350e3e721aaa5f4de69af2b3b6e7d157817a1ea.webp)

Bel het nummer — de agent neemt op.

## Verbinden met Teams

* **Rechtstreeks bellen:** een Teams-gebruiker met Teams Phone + een Calling Plan kan het ACS-nummer bellen zoals elk extern nummer.
* **Teams-resourceaccount (TPE):** koppel een Teams-resourceaccount aan de ACS-resource met [Teams Phone Extensibility](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/tpe/teams-phone-extensibility-quickstart), zodat oproepen naar het resourceaccount dezelfde flow `IncomingCall` → bridge starten.

## Einde van het gesprek

Wanneer de agent het gesprek beëindigt (bijvoorbeeld met de tool **End Call**), sluit ElevenLabs de WebSocket. Hang de ACS-kant op zodat de beller niet op een dode lijn achterblijft:

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

## Warme overdracht naar een persoon

De ingebouwde overdrachtstools van ElevenLabs werken alleen wanneer ElevenLabs de telefonie beheert. Hier activeert de agent dus een **aangepaste clienttool** (bijvoorbeeld `transfer_to_human`) die je bridge afhandelt door de persoon met `add_participant` **toe te voegen** aan het actieve gesprek (warm), in plaats van een blinde overdracht:

```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 stuurt callbacks voor `AddParticipantSucceeded` / `AddParticipantFailed` naar `/api/callbacks`. Stuur een `client_tool_result` terug naar de agent zodat deze de overdrachtszin kan uitspreken. Zie [systeemtools](/docs/nl/eleven-agents/customization/tools/system-tools/transfer-to-number) voor de configuratie aan de agentzijde.

> **Tip**
>
> Stel de overdrachtsbeveiliging in zodra de tool wordt geactiveerd (vóór je `add_participant` aanroept), anders kan een snelle sluiting van de EL-
> WebSocket concurreren met het ophangen en de oproep verbreken voordat de persoon deelneemt.

## Problemen oplossen

#### Geen IncomingCall bereikt de bridge

Controleer of het Event Grid-abonnement is ingericht (`provisioningState: Succeeded`) en of de
`/api/incomingCall` van de bridge de validatie-echo heeft teruggegeven. Controleer of het nummer inbound
calling ondersteunt en zich in dezelfde ACS-resource bevindt als waarop het abonnement is ingesteld. Op het tabblad
**Filters** van het abonnement moeten de eventtypen **Incoming Call** bevatten:

![Het tabblad Filters van het eventabonnement, met het eventtype gefilterd op Incoming
Call](/docs/_fern-img/c7026e86d96551d713e4fd7d5753062f643fcd6a67b455866bb8f1bbb6b57eb1.webp)

#### \`CreateCallFailed\` / \`AddParticipantFailed\` voor een internationaal nummer

Uitgaande ACS-oproepen naar bepaalde bestemmingen (zoals India) zijn beperkt of onregelmatig. Gebruik een ondersteunde
bestemming, of laat de menselijke kant via een SIP-/Operator-nummer lopen. De logica van de bridge blijft ongewijzigd —
het gaat om een fout op providerniveau aan de uitgaande kant.

#### Audio is vervormd of heeft de verkeerde snelheid

Beide kanten moeten PCM 16 kHz mono gebruiken. Stel het invoer-/uitvoerformaat van de agent in op `pcm_16000`; de
bridge logt het overeengekomen formaat uit `conversation_initiation_metadata`.

#### Kan geen nummer kopen / nummer niet beschikbaar in mijn land

Voor het kopen van nummers is een betaald abonnementstype nodig (MCA/EA/PAYG). Als ACS geen nummers aanbiedt in
je land, gebruik dan een [SIP](/docs/nl/eleven-agents/phone-numbers/sip-trunking)-provider.

## Handige links

* [Overzicht van ACS Call Automation](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/call-automation)
* [ACS-audiostreaming](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/audio-streaming-concept)
* [Acties voor gespreksbeheer (overdragen / deelnemer toevoegen)](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)
* [Agent WebSocket-protocol](/docs/nl/eleven-agents/libraries/web-sockets)
* [ElevenLabs SIP-trunking](/docs/nl/eleven-agents/phone-numbers/sip-trunking)