> 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

## Présentation

Cette approche attribue un **numéro de téléphone** à votre agent. Un appelant le compose, [Azure Communication Services](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/call-automation) (ACS) répond avec un **streaming multimédia bidirectionnel**, puis un petit pont relaie l'audio PCM entre ACS et l'agent ElevenLabs à l'aide du [protocole WebSocket d'agent](/docs/fr/eleven-agents/libraries/web-sockets) standard. Il s'agit du modèle de centre de contact / IVR, similaire à un déploiement de [SIP trunking](/docs/fr/eleven-agents/phone-numbers/sip-trunking), avec ACS comme opérateur.

Cette solution se connecte également à Teams de deux façons : un utilisateur Teams disposant d'un Calling Plan peut appeler directement le numéro ACS, ou vous pouvez placer **Teams Phone Extensibility** devant ce numéro afin que les appels vers un compte de ressource Teams soient acheminés vers ACS.

> **Note**
>
> ACS provisionne des numéros PSTN uniquement dans un [ensemble limité de pays](https://learn.microsoft.com/en-us/azure/communication-services/concepts/numbers/sub-eligibility-number-capability).
> Si aucun numéro n'est disponible dans votre région, utilisez plutôt un fournisseur SIP avec le [SIP trunking](/docs/fr/eleven-agents/phone-numbers/sip-trunking), ou le [bot d'appel Graph](/docs/fr/eleven-agents/phone-numbers/microsoft-teams/graph-media-bot).

## Fonctionnement

![Un appelant compose le numéro ACS ; ACS envoie IncomingCall via Event Grid au pont, qui répond avec un streaming multimédia PCM 16k bidirectionnel et le relaie à l'agent ElevenLabs via un WebSocket](/docs/_fern-files/elevenlabs.docs.buildwithfern.com/e77a27148e217fc7dc06c25007c1c141913fc831cbfe8a2f57405137646271af/assets/images/conversational-ai/teams-acs-architecture.svg)

L'audio est en **PCM 16 kHz mono** sur les deux liaisons, le format d'entrée/sortie de l'agent étant `pcm_16000`, il est donc transmis en base64 sans rééchantillonnage.

Le pont expose les routes suivantes :

| Route                    | Objectif                                                                                            |
| ------------------------ | --------------------------------------------------------------------------------------------------- |
| `POST /api/incomingCall` | Webhook Event Grid : valide l'abonnement, puis lance `answer_call` avec streaming multimédia        |
| `POST /api/callbacks`    | Événements du cycle de vie Call Automation (`CallConnected`, `CallDisconnected`, `AddParticipant*`) |
| `GET\|WS /ws`            | Socket de streaming multimédia ACS ↔ ElevenLabs                                                     |
| `POST /api/outboundCall` | Facultatif : passe un appel sortant qui connecte le répondant à l'agent                             |

## Prérequis

1. Un abonnement Azure **payant** (MCA / EA / Pay-As-You-Go), les abonnements gratuits, d'essai ou sponsorisés ne peuvent pas acheter de numéros.
2. Une [ressource Azure Communication Services](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/create-communication-resource).
3. Un hôte HTTPS pour le pont avec un WebSocket public, Azure Container Apps, App Service ou une VM.
4. Un [agent ElevenLabs](/docs/fr/eleven-agents/quickstart) configuré en **PCM 16000 Hz** sur les deux liaisons : format de sortie TTS dans l'onglet **Voice**, format audio d'entrée utilisateur dans l'onglet **Advanced**.

## Autorisations et rôles

| Étendue          | Rôle / autorisation                            | Motif                                                                                   |
| ---------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------- |
| Azure RBAC       | **Contributor** sur le groupe de ressources    | créer la ressource ACS, l'application conteneur et l'abonnement Event Grid              |
| Abonnement Azure | **Owner ou Contributor** sur l'abonnement      | acheter des numéros de téléphone, l'option d'achat est sinon désactivée                 |
| Facturation      | Type d'abonnement **MCA / EA / Pay-As-You-Go** | les abonnements gratuits, d'essai, sponsorisés et Dev ne peuvent pas acheter de numéros |

> **Note**
>
> Avec le rôle **Contributor**, et non Owner, `az containerapp up` ne peut pas créer l'attribution de rôle
> ACR pull pour l'identité gérée. Activez plutôt l'utilisateur administrateur du registre et attachez-le, consultez l'avertissement de l'étape 2.

## Étape 1 : provisionnez la ressource ACS et le numéro

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

Achetez un numéro dans la ressource, Portal → votre ressource ACS → **Phone numbers → Get**, ou utilisez le [SDK phone-numbers](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/telephony/get-phone-number). Pour un agent qui **répond** aux appels, un numéro avec les **appels entrants** suffit ; ajoutez la capacité **sortante** si vous souhaitez également utiliser `/api/outboundCall`.

![Le panneau Phone numbers de la ressource ACS répertoriant les numéros actifs avec leurs capacités d'appel](/docs/_fern-img/9d14034ee9f81e2ebcf64e30371f914ef6f87a8da30ac250e702662f2e5e1cc1.webp)

Pour vérifier depuis la CLI, nécessite `az extension add --name communication`, et récupérer la chaîne de connexion que le pont utilise comme `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"
```

## Étape 2 : déployez le pont

Le pont est une petite application Flask + `flask-sock` utilisant `azure-communication-callautomation`. Voici le cœur du flux entrant :

**`bridge.py (extrait)`**

```python title="bridge.py (extrait)"
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"})
```

Sur le socket `/ws`, relayez PCM16 dans les deux sens : transférez les frames ACS `AudioData` à ElevenLabs sous la forme `{"user_audio_chunk": "<base64>"}`, puis renvoyez l'audio de l'agent sous la forme `{"Kind":"AudioData","AudioData":{"Data":"<base64>"},"StopAudio":null}`. La première frame envoyée par ACS est `AudioMetadata`, le format négocié, enregistrez-la dans les logs et ignorez-la. Le côté ElevenLabs utilise le [protocole WebSocket d'agent](/docs/fr/eleven-agents/libraries/web-sockets) standard.

> **Note**
>
> ACS utilise une casse JSON différente dans chaque sens : les frames entrantes qu'il **envoie** sont en camelCase (`kind`,
> `audioData.data`), tandis que les frames sortantes qu'il **attend** sont en PascalCase (`Kind`, `AudioData.Data`,
> `StopAudio`). Conservez ces deux casses distinctes, le relais ci-dessous les reproduit.

**`bridge.py : relais multimédia`**

```python title="bridge.py : relais multimédia"
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**
>
> Ce relais est volontairement minimal. Pour la production, ajoutez la journalisation, la reconnexion et une fermeture
> propre. La référence complète des messages se trouve dans la [documentation WebSocket](/docs/fr/eleven-agents/libraries/web-sockets).

> **Note**
>
> `EL_WS` se connecte à un agent **public**. Pour un agent privé, demandez au pont une URL signée de courte durée
> côté serveur, `GET /v1/convai/conversation/get-signed-url?agent_id=...` avec votre clé API, puis connectez-vous plutôt à l'URL renvoyée. Pour la [résidence des données](/docs/fr/overview/administration/data-residency), définissez `ELEVENLABS_ORIGIN` sur votre
> hôte de résidence (`wss://api.eu.residency.elevenlabs.io`, `.in.` ou `.sg.`), les requêtes d'URL signée
> utilisent l'hôte `https://` correspondant.

Déployez vers Azure Container Apps et récupérez le FQDN public :

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

Définissez ensuite `BRIDGE_PUBLIC_HOST=$FQDN` et la chaîne de connexion ACS, comme secret, dans l'application.

> **Warning**
>
> Avec le rôle **Contributor**, et non Owner, `az containerapp up` ne peut pas créer le rôle ACR
> pull pour l'identité gérée. Activez l'utilisateur administrateur du registre (`az acr update --admin-enabled true`) et attachez-le
> avec `az containerapp registry set`, puis utilisez `az containerapp update --image ...`.

## Étape 3 : acheminez IncomingCall vers le pont

Créez un abonnement Event Grid sur la ressource ACS qui publie `IncomingCall` vers le pont. La négociation de validation du pont, ci-dessus, finalise automatiquement l'abonnement.

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

L'abonnement apparaît dans le panneau **Events** de la ressource ACS :

![Le panneau Events de la ressource ACS affichant l'abonnement webhook acs-incomingcall filtré sur Microsoft.Communication.IncomingCall](/docs/_fern-img/f310aed9637e06b81906ca188350e3e721aaa5f4de69af2b3b6e7d157817a1ea.webp)

Composez le numéro, l'agent répond.

## Connexion à Teams

* **Appel direct :** un utilisateur Teams avec Teams Phone + un Calling Plan peut appeler le numéro ACS comme n'importe quel numéro externe.
* **Compte de ressource Teams (TPE) :** liez un compte de ressource Teams à la ressource ACS avec [Teams Phone Extensibility](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/tpe/teams-phone-extensibility-quickstart) afin que les appels vers le compte de ressource déclenchent le même flux `IncomingCall` → pont.

## Fin d'appel

Lorsque l'agent termine la conversation, par exemple avec son outil **End Call**, ElevenLabs ferme le WebSocket. Raccrochez la liaison ACS afin que l'appelant ne reste pas sur une ligne inactive :

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

## Transfert assisté vers un humain

Les outils de transfert natifs d'ElevenLabs s'appliquent uniquement lorsqu'ElevenLabs gère la téléphonie. Ici, l'agent déclenche donc un **outil client personnalisé**, par exemple `transfer_to_human`, que votre pont gère en **ajoutant** la personne à l'appel en cours avec `add_participant`, transfert assisté, plutôt qu'en effectuant un transfert aveugle :

```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 envoie les callbacks `AddParticipantSucceeded` / `AddParticipantFailed` à `/api/callbacks`. Renvoyez un `client_tool_result` à l'agent afin qu'il puisse prononcer sa phrase de transfert. Consultez les [outils système](/docs/fr/eleven-agents/customization/tools/system-tools/transfer-to-number) pour la configuration côté agent.

> **Tip**
>
> Définissez la protection de transfert dès le déclenchement de l'outil, avant d'appeler `add_participant`, sinon une fermeture rapide du
> WebSocket EL peut entrer en concurrence avec le raccrochage et interrompre l'appel avant que la personne ne rejoigne la conversation.

## Résolution des problèmes

#### Aucun IncomingCall n'atteint le pont

Vérifiez que l'abonnement Event Grid a été provisionné (`provisioningState: Succeeded`) et que le
`/api/incomingCall` du pont a renvoyé l'écho de validation. Vérifiez que le numéro accepte les appels entrants
et appartient à la même ressource ACS que l'abonnement. Dans l'onglet **Filters** de l'abonnement,
les types d'événements doivent inclure **Incoming Call** :

![L'onglet Filters de l'abonnement aux événements avec le type d'événement filtré sur Incoming Call](/docs/_fern-img/c7026e86d96551d713e4fd7d5753062f643fcd6a67b455866bb8f1bbb6b57eb1.webp)

#### \`CreateCallFailed\` / \`AddParticipantFailed\` pour un numéro international

Les appels sortants ACS vers certaines destinations, par exemple l'Inde, sont restreints ou intermittents. Utilisez une destination prise en charge,
ou faites passer la liaison humaine par un numéro SIP/Operator. La logique du pont n'est pas affectée,
il s'agit d'un échec au niveau de l'opérateur sur la liaison sortante.

#### L'audio est déformé ou la vitesse est incorrecte

Les deux côtés doivent être en PCM 16 kHz mono. Configurez le format d'entrée/sortie de l'agent sur `pcm_16000` ; le
pont enregistre dans les logs le format négocié depuis `conversation_initiation_metadata`.

#### Impossible d'acheter un numéro / numéro indisponible dans mon pays

L'achat de numéros nécessite un type d'abonnement payant, MCA/EA/PAYG. Si ACS ne propose pas de numéros dans
votre pays, utilisez plutôt un fournisseur [SIP](/docs/fr/eleven-agents/phone-numbers/sip-trunking).

## Liens utiles

* [Présentation d'ACS Call Automation](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/call-automation)
* [Streaming audio ACS](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/audio-streaming-concept)
* [Actions de contrôle des appels, transfert / ajout de participant](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)
* [Protocole WebSocket d'agent](/docs/fr/eleven-agents/libraries/web-sockets)
* [SIP trunking ElevenLabs](/docs/fr/eleven-agents/phone-numbers/sip-trunking)