Azure Communication Services

Ermöglichen Sie Nutzern, eine Telefonnummer anzurufen, die Ihr ElevenLabs-Agent über ACS Call Automation entgegennimmt.

Überblick

Mit diesem Ansatz erhält Ihr Agent eine Telefonnummer. Ein Anrufer wählt sie, Azure Communication Services (ACS) nimmt den Anruf mit bidirektionalem Media Streaming an, und eine kleine Bridge leitet PCM-Audio zwischen ACS und dem ElevenLabs-Agenten über das standardmäßige Agent-WebSocket-Protokoll weiter. Dies entspricht dem Contact-Center-/IVR-Muster – derselben Struktur wie bei einer SIP-Trunking-Bereitstellung, wobei ACS der Carrier ist.

Außerdem verbindet es sich auf zwei Arten mit Teams: Ein Teams-Nutzer mit Calling Plan kann die ACS-Nummer direkt wählen, oder Sie schalten Teams Phone Extensibility vor die Nummer, sodass Anrufe an ein Teams-Ressourcenkonto in ACS weitergeleitet werden.

ACS stellt PSTN-Nummern nur in einer begrenzten Anzahl von Ländern bereit. Wenn in Ihrer Region keine Nummer verfügbar ist, verwenden Sie stattdessen einen SIP-Anbieter mit SIP Trunking oder den Graph-Anruf- Bot.

So funktioniert es

Ein Anrufer wählt die ACS-Nummer; ACS sendet IncomingCall über Event Grid an die Bridge, die den Anruf mit bidirektionalem PCM-16k-Media-Streaming annimmt und ihn über einen WebSocket an den ElevenLabs-Agenten weiterleitet
Eingehender Anruf → ACS → Bridge → ElevenLabs

Audio ist auf beiden Seiten PCM 16 kHz Mono (das Ein-/Ausgabeformat des Agenten ist pcm_16000) und wird daher ohne Resampling als base64 durchgereicht.

Die Bridge stellt diese Routen bereit:

RouteZweck
POST /api/incomingCallEvent-Grid-Webhook: validiert das Abonnement und führt dann answer_call mit Media Streaming aus
POST /api/callbacksCall-Automation-Lebenszyklusereignisse (CallConnected, CallDisconnected, AddParticipant*)
GET|WS /wsACS-Media-Streaming-Socket ↔ ElevenLabs
POST /api/outboundCallOptional: startet einen ausgehenden Anruf, der den Angerufenen mit dem Agenten verbindet

Voraussetzungen

  1. Ein kostenpflichtiges Azure-Abonnement (MCA / EA / Pay-As-You-Go) – kostenlose Test- oder Sponsoring-Abonnements können keine Nummern kaufen.
  2. Eine Azure Communication Services-Ressource.
  3. Einen HTTPS-Host für die Bridge mit einem öffentlichen WebSocket (Azure Container Apps, App Service oder eine VM).
  4. Einen ElevenLabs-Agenten, der auf beiden Seiten auf PCM 16000 Hz eingestellt ist: TTS-Ausgabeformat im Tab Voice, Audioeingabeformat des Nutzers im Tab Advanced.

Berechtigungen und Rollen

BereichRolle / BerechtigungWarum
Azure RBACContributor für die RessourcengruppeACS-Ressource, Container App und Event-Grid-Abonnement erstellen
Azure-AbonnementOwner oder Contributor für das AbonnementTelefonnummern kaufen (anderenfalls ist die Kaufoption deaktiviert)
AbrechnungAbonnementstyp MCA / EA / Pay-As-You-Gokostenlose, Test-, Sponsoring- und Dev-Abonnements können keine Nummern kaufen

Mit Contributor (nicht Owner) kann az containerapp up die ACR-Pull- Rollenzuweisung für die verwaltete Identität nicht erstellen. Aktivieren Sie stattdessen den Admin-Nutzer der Registry und binden Sie ihn ein – siehe die Warnung in Schritt 2.

Schritt 1 – ACS-Ressource und Nummer bereitstellen

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

Kaufen Sie in der Ressource eine Nummer (Portal → Ihre ACS-Ressource → Phone numbers → Get oder über das phone-numbers SDK). Für einen Agenten, der Anrufe annimmt, genügt eine Nummer mit eingehenden Anrufen. Fügen Sie die Fähigkeit ausgehend hinzu, wenn Sie auch /api/outboundCall verwenden möchten.

Die Ansicht Phone numbers der ACS-Ressource mit aktiven Nummern und ihren Anruffunktionen

Telefonnummern der ACS-Ressource – die Spalte Calling zeigt die Richtung jeder Nummer

Zur Überprüfung über die CLI (erfordert az extension add --name communication) und zum Abrufen der Verbindungszeichenfolge, die die Bridge als ACS_CONNECTION_STRING verwendet:

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

Schritt 2 – Bridge bereitstellen

Die Bridge ist eine kleine Flask- + flask-sock-App mit azure-communication-callautomation. Der Kern des eingehenden Ablaufs:

bridge.py (excerpt)
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"})

Leiten Sie auf dem Socket /ws PCM16 in beide Richtungen weiter: Leiten Sie ACS-AudioData-Frames als {"user_audio_chunk": "<base64>"} an ElevenLabs weiter und senden Sie das Audio des Agenten als {"Kind":"AudioData","AudioData":{"Data":"<base64>"},"StopAudio":null} zurück. Der erste von ACS gesendete Frame ist AudioMetadata (das ausgehandelte Format) – protokollieren und ignorieren Sie ihn. Die ElevenLabs-Seite nutzt das standardmäßige Agent-WebSocket-Protokoll.

ACS verwendet je Richtung unterschiedliche JSON-Groß-/Kleinschreibung: Eingehende Frames, die ACS sendet, nutzen camelCase (kind, audioData.data), während ausgehende Frames, die ACS erwartet, PascalCase verwenden (Kind, AudioData.Data, StopAudio). Halten Sie die beiden Schreibweisen getrennt – das folgende Relay bildet dies ab.

bridge.py — media relay
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

Dieses Relay ist bewusst minimal gehalten. Ergänzen Sie für den Produktionseinsatz Logging, Wiederverbindung und einen kontrollierten Abbau. Die vollständige Nachrichtenreferenz finden Sie in der WebSocket- Dokumentation.

EL_WS verbindet sich mit einem öffentlichen Agenten. Lassen Sie die Bridge für einen privaten Agenten serverseitig eine kurzlebige signierte URL anfordern – GET /v1/convai/conversation/get-signed-url?agent_id=... mit Ihrem API- Schlüssel – und verbinden Sie sich stattdessen mit der zurückgegebenen URL. Legen Sie bei Datenresidenz ELEVENLABS_ORIGIN auf Ihren Residenzhost fest (wss://api.eu.residency.elevenlabs.io, .in. oder .sg.) – Anfragen nach signierten URLs verwenden den entsprechenden https://-Host.

Stellen Sie die Anwendung in Azure Container Apps bereit und erfassen Sie den öffentlichen FQDN:

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)

Setzen Sie anschließend BRIDGE_PUBLIC_HOST=$FQDN und die ACS-Verbindungszeichenfolge als Secret in der App.

Mit Contributor (nicht Owner) kann az containerapp up die ACR- Pull-Rolle der verwalteten Identität nicht erstellen. Aktivieren Sie den Admin-Nutzer der Registry (az acr update --admin-enabled true) und binden Sie ihn mit az containerapp registry set ein. Führen Sie dann az containerapp update --image ... aus.

Schritt 3 – IncomingCall zur Bridge weiterleiten

Erstellen Sie für die ACS-Ressource ein Event-Grid-Abonnement, das IncomingCall an die Bridge sendet. Der oben gezeigte Validierungs-Handshake der Bridge schließt das Abonnement automatisch ab.

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

Das Abonnement wird in der Ansicht Events der ACS-Ressource angezeigt:

Die Ansicht Events der ACS-Ressource mit dem nach Microsoft.Communication.IncomingCall gefilterten Webhook-Abonnement acs-incomingcall

ACS-Ressource → Events → Event Subscriptions

Wählen Sie die Nummer – der Agent nimmt den Anruf an.

Mit Teams verbinden

  • Direktwahl: Ein Teams-Nutzer mit Teams Phone + Calling Plan kann die ACS-Nummer wie jede externe Nummer wählen.
  • Teams-Ressourcenkonto (TPE): Binden Sie ein Teams-Ressourcenkonto mit Teams Phone Extensibility an die ACS-Ressource, damit Anrufe an das Ressourcenkonto denselben Ablauf IncomingCall → Bridge auslösen.

Anrufende

Wenn der Agent die Unterhaltung beendet (z. B. über sein Tool End Call), schließt ElevenLabs den WebSocket. Legen Sie die ACS-Verbindung auf, damit der Anrufer nicht in einer toten Leitung bleibt:

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

Warme Übergabe an einen Menschen

Die nativen Übergabe-Tools von ElevenLabs gelten nur, wenn ElevenLabs die Telefonie bereitstellt. Hier löst der Agent daher ein benutzerdefiniertes Client-Tool aus (z. B. transfer_to_human), das Ihre Bridge verarbeitet, indem sie den Menschen mit add_participant zum aktiven Anruf hinzufügt (warm), statt den Anruf blind weiterzuleiten:

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 sendet AddParticipantSucceeded- / AddParticipantFailed-Callbacks an /api/callbacks. Geben Sie dem Agenten ein client_tool_result zurück, damit er seine Übergabezeile sagen kann. Die Konfiguration auf Agentenseite finden Sie unter System-Tools.

Setzen Sie die Übergabe-Sperre, sobald das Tool ausgelöst wird (vor dem Aufruf von add_participant), da ein schnelles Schließen des EL- WebSockets mit dem Auflegen konkurrieren und den Anruf beenden kann, bevor der Mensch beitritt.

Fehlerbehebung

Prüfen Sie, ob das Event-Grid-Abonnement bereitgestellt wurde (provisioningState: Succeeded) und ob die Bridge über /api/incomingCall die Validierungsantwort zurückgegeben hat. Prüfen Sie, ob die Nummer eingehende Anrufe unterstützt und sich in derselben ACS-Ressource befindet, für die das Abonnement erstellt wurde. Im Tab Filters des Abonnements müssen die Ereignistypen Incoming Call enthalten:

Der Tab Filters des Event-Abonnements mit auf Incoming Call gefiltertem Ereignistyp

Event subscription → Filters → Incoming Call

ACS-Ausgänge zu einigen Zielen (z. B. Indien) sind eingeschränkt oder unzuverlässig. Verwenden Sie ein unterstütztes Ziel oder schalten Sie für die menschliche Verbindung eine SIP-/Operator-Nummer vor. Die Bridge-Logik bleibt unverändert – es handelt sich um einen Fehler auf Carrier-Ebene der ausgehenden Verbindung.

Beide Seiten müssen PCM 16 kHz Mono verwenden. Setzen Sie das Ein-/Ausgabeformat des Agenten auf pcm_16000. Die Bridge protokolliert das ausgehandelte Format aus conversation_initiation_metadata.

Der Nummernkauf erfordert einen kostenpflichtigen Abonnementtyp (MCA/EA/PAYG). Falls ACS in Ihrem Land keine Nummern anbietet, verwenden Sie stattdessen einen SIP-Anbieter.