Skip to navigation

Azure Communication Services

Laat gebruikers een telefoonnummer bellen dat je ElevenLabs-agent beantwoordt, via ACS Call Automation.

Overzicht

Met deze aanpak krijgt je agent een telefoonnummer. Een beller belt dit nummer, Azure Communication Services (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. Dit is het contactcenter-/IVR-patroon — dezelfde opzet als een implementatie met 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.

ACS levert PSTN-nummers alleen in een beperkt aantal landen. Als er in je regio geen nummer beschikbaar is, gebruik dan een SIP-provider met SIP- trunking, of de Graph calling- 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
Inkomende oproep → ACS → bridge → ElevenLabs

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:

RouteDoel
POST /api/incomingCallEvent Grid-webhook: valideert het abonnement en voert daarna answer_call met mediastreaming uit
POST /api/callbacksLevenscyclusevents van Call Automation (CallConnected, CallDisconnected, AddParticipant*)
GET|WS /wsACS-media-streaming socket ↔ ElevenLabs
POST /api/outboundCallOptioneel: 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.
  3. Een HTTPS-host voor de bridge met een openbare WebSocket (Azure Container Apps, App Service of een VM).
  4. Een ElevenLabs-agent 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

BereikRol / machtigingWaarom
Azure RBACContributor voor de resourcegroepde ACS-resource, Container App en Event Grid-abonnement maken
Azure-abonnementOwner of Contributor voor het abonnementtelefoonnummers kopen (anders is de koopoptie uitgeschakeld)
FactureringAbonnementstype MCA / EA / Pay-As-You-Gogratis-, proef-, sponsor- en Dev-abonnementen kunnen geen nummers kopen

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

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

Telefoonnummers in de ACS-resource — de kolom Calling toont de richting van elk nummer

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:

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

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

Deze relay is bewust minimaal. Voeg voor productie logging, herverbinding en gecontroleerd afsluiten toe. De volledige berichtreferentie staat in de WebSocket- documentatie.

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

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.

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.

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

ACS-resource → Events → Event Subscriptions

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, 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:

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:

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 voor de configuratie aan de agentzijde.

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

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

Event subscription → Filters → Incoming Call

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.

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.

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-provider.