Integrate with the Contact Center Intelligence universal connector (preview)

[This article is prerelease documentation and is subject to change.]

The Contact Center Intelligence universal connector is a Microsoft-hosted WebSocket server that bridges any non-Microsoft contact center platform with Microsoft AI services. This article is a developer reference for non-Microsoft contact center platform developers who are integrating using the universal connector.

Your platform connects to the connector as a WebSocket client and exchanges JSON messages using an envelope called the Activity Protocol. The protocol is language-agnostic: implement your WebSocket client in any language, with no SDK required. One WebSocket connection carries all messages for a session, and each channel has its own URL, so a single organization can have multiple channels.

Important

This article describes the AI agent experience. Your platform connects a call or chat to a Microsoft-hosted AI agent that converses with the customer. Build your integration against the AI agent described here.

The connector supports two session types, declared in the session.open message:

Session type Use case Status
audio Voice call. Streams real-time audio in both directions; the AI agent responds with audio. Live
text Chat. Exchanges text messages; the AI agent replies with text. Live

The same WebSocket URL, authentication, and session.open/session.close handshake are used for both session types. The sessionType field in session.open tells the connector about the modality to activate.

A screenshot of the universal connector workflow with explanation.

Your platform streams audio or sends text, and the AI agent's outcomes are delivered back over the same connection.

Onboard

Before your platform can connect, you must complete a one-time provisioning process with Microsoft. Onboarding is the same for both audio and text sessions.

Create a service principal in Power Platform tenant

The Contact Center Intelligence service derives the token issuer from the Microsoft Entra tenant that owns the Power Platform environment. Your token must therefore be issued by that same tenant, not your own corporate tenant.

In the Azure portal, in the Microsoft Entra tenant, create an app registration or grant consent to a multitenant app you own, which creates a guest service principal in the tenant. You need:

  • Application (client) ID: The app registration's client ID.
  • Tenant ID: The Power Platform environment's Microsoft Entra tenant ID.
  • Service principal object ID: The enterprise application object ID in the tenant (not the app registration object ID).
  • A credential. Choose one of the following:
    • Client secret: simplest option; many enterprise tenants disable this.
    • Certificate: stronger option; also subject to tenant policy.
    • Federated credential: no secret stored in Microsoft Entra ID; uses your workload identity (managed identity, GitHub OIDC, Kubernetes workload identity).

Important

The token must be issued by the Power Platform tenant. The Contact Center Intelligence service validates the token issuer against the Microsoft Entra tenant of the Power Platform environment. A token issued by a different tenant, including your own corporate tenant, is rejected with HTTP 401 even if the signature is valid and the audience matches. Use authority = https://login.microsoftonline.com/<power-platform-tenant-id>.

Submit your app registration to Microsoft

Provide these values in the Contact Center Intelligence admin page:

  • Application (client) ID: used as the expected JWT audience.
  • Service principal object ID: validated against the oid claim on every connection.

Microsoft validates the registration and provisions your organization and channel.

Receive your connection details

After provisioning, Microsoft provides:

  • Service endpoint: the WebSocket host (the format varies by environment).
  • Org ID: a GUID used in the WebSocket URL path.
  • Channel ID: one per configured channel.

Tip

Credential rotation. Rotate client secrets or certificates in your own Microsoft Entra tenant at any time. If your application (client) ID changes, submit your app registration again.

Authenticate

Authentication is the same for audio and text sessions. Tokens are acquired via the OAuth 2.0 client credentials flow and sent on every WebSocket upgrade.

Acquire a token

POST https://login.microsoftonline.com/<power-platform-tenant-id>/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=<your-app-client-id>
&client_secret=<your-client-secret>
&scope=<your-app-client-id>/.default

Note

Token scope. Use your own application (client) ID as the resource: scope=<your-app-client-id>/.default. The resulting token's aud claim is your raw application (client) ID GUID. The Contact Center Intelligence service validates this against msdyn_appid in the CSAC vendor registration. No admin consent for a Microsoft-owned app is required.

Get a token with a federated credential

Use this flow if secrets and certificates are disabled in your tenant.

POST https://login.microsoftonline.com/<power-platform-tenant-id>/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&client_id=<your-app-client-id>
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=<external-identity-token>
&scope=<your-app-client-id>/.default

Token requirements

Requirement Value
Flow Client credentials (no user involvement).
Audience (aud) Your own Microsoft Entra application (client) ID.
Required claims aud, appid, oid, iss.
Accepted issuers The Microsoft Entra tenant that owns the Power Platform environment. Both V1 (sts.windows.net/<tid>/) and V2 (login.microsoftonline.com/<tid>/v2.0) are accepted.
oid validation Validated against your registered service principal object ID (msdyn_objectid). A mismatch returns HTTP 401.
Credential placement Authorization: Bearer <token> header, or the bearer.<token> WebSocket subprotocol entry (see Present credentials on the upgrade). Never place the token in the URL.
Lifetime Validated at WebSocket upgrade only. A mid-session expiry doesn't disconnect an active session.
On reconnect A non-expired token is required on every upgrade. The same token can be reused as long as it hasn't expired (5-minute clock skew tolerance).

Connect over WebSocket

The connection setup is identical for audio and text sessions.

URL format

wss://<host>/connector/universal/api/v1/ws/<orgId>/<channelId>/<sessionId>
Segment Description
<host> The endpoint that Microsoft provides at provisioning. Always use the provisioned value; don't construct URLs manually.
<orgId> Your org GUID. The server looks up your registered credentials using this value.
<channelId> The channel ID for this integration.
<sessionId> A UUID v4 that your platform generates. It's stable across reconnects for the same call or chat. Non-UUID values are rejected with HTTP 400.

Present credentials on the upgrade

Credentials can be presented in one of two ways. Both are validated identically: token validation, replay protection, and the reconnect rules are the same regardless of the transport you use.

Option A: HTTP headers (server-to-server clients). Set these headers on the upgrade request.

Header Required Description
Authorization: Bearer <token> Must The Microsoft Entra bearer token described in Authenticate.
Request-ID: <uuid> Must A new UUID on every connection attempt. Together with the <sessionId> URL segment, this forms a replay-prevention nonce.

Option B: Sec-WebSocket-Protocol subprotocol (browser clients). Browsers can't set arbitrary headers (Authorization, Request-ID) on a WebSocket handshake, so the bearer token and request ID are carried as subprotocol entries instead.

Sec-WebSocket-Protocol: bearer.<token>, requestid.<uuid>, <connector-subprotocol>
Subprotocol entry Required Description
bearer.<token> Must The Microsoft Entra bearer token described in Authenticate, prefixed with bearer. (case-insensitive). Replaces the Authorization header.
requestid.<uuid> Must A new UUID on every connection attempt, prefixed with requestid.. Replaces the Request-ID header.
<connector-subprotocol> Must A protocol name (for example, cci.universal.v1) that the server selects and echoes back to complete subprotocol negotiation. A handshake with only bearer.*/requestid.* entries and no connector subprotocol is rejected with HTTP 400.

In both options, the session ID is taken from the <sessionId> URL segment. The server echoes only <connector-subprotocol> in the Sec-WebSocket-Protocol response; the bearer.* and requestid.* entries are credentials and are never echoed back.

Note

When to use which option. Use Option A for server-side integrations. Use Option B only when your client (such as an in-browser single-page application) can't set custom request headers on a WebSocket. A JWT contains only base64url characters and dots, all valid WebSocket subprotocol-token characters, so it can be carried as a subprotocol entry. The token is still sent only over the encrypted wss:// channel.

Upgrade examples

Option A: headers

GET wss://<host>/connector/universal/api/v1/ws/<orgId>/<channelId>/<sessionId>
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: <base64-nonce>
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
Request-ID: aaaaaaaa-0b0b-1c1c-2d2d-333333333333

Option B: subprotocol (browser)

GET wss://<host>/connector/universal/api/v1/ws/<orgId>/<channelId>/<sessionId>
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: <base64-nonce>
Sec-WebSocket-Protocol: bearer.eyJ0eXAiOiJKV1Qi..., requestid.aaaaaaaa-0b0b-1c1c-2d2d-333333333333, cci.universal.v1

From the browser WebSocket API, the subprotocols are the second argument:

const ws = new WebSocket(
  `wss://<host>/connector/universal/api/v1/ws/${orgId}/${channelId}/${sessionId}`,
  [`bearer.${token}`, `requestid.${requestId}`, 'cci.universal.v1']);

Success returns HTTP/1.1 101 Switching Protocols (for Option B the response carries Sec-WebSocket-Protocol: cci.universal.v1).

Upgrade failure codes

HTTP status Cause Action
400 Missing or malformed header, sessionId isn't a valid UUID, or (browser) the Sec-WebSocket-Protocol lacks the connector subprotocol. Fix the request.
401 Token expired, invalid signature, wrong audience, or the token's appid/oid doesn't match the registered service principal. Refresh the token; verify the app registration.
404 Org not provisioned, or org/channel not found. Verify the org ID and channel ID with Microsoft.
409 Replay: the (sessionId, Request-ID) pair was already used. Generate a new Request-ID and retry.
429 Reconnect rate limit exceeded. Apply exponential backoff before retrying.
500 Internal server error. Apply exponential backoff before retrying.

Message protocol

All messages are JSON text frames using the Activity Protocol envelope. One WebSocket connection carries all messages: audio frames, text messages, and signaling.

Envelope structure

{
  "type":     "event" | "command",
  "id":       "<uuid-v4>",
  "name":     "<message-name>",
  "from": {
    "id":   "<uuid-v4>",
    "role": "caller" | "agent" | "bot",
    "name": "<optional display label>"
  },
  "value":    { },
  "entities": [ ]
}
Field Required Notes
type Must "event" (notification) or "command" (request).
id Must UUID v4, unique per message. Used for log correlation.
name Must The message identifier. See Message reference for the full list.
from Must for media.chunk, dtmf, text.message Sender identity. from.id must be a stable UUID per participant. from.role identifies the speaker's part in the call: "caller" (customer) or "agent" (human agent). from.name is an optional free-form display label. May be omitted for session control messages (session.open, session.close).
value Must when carrying a payload The message-specific payload object.
entities May For media.chunk, carries streamSequence and sampleOffset. Omit if empty.

Tip

Forward compatibility. Both sides must silently ignore unknown JSON fields and unknown message name values. Build your receive path defensively.

Voice calls stream real-time audio in; the AI agent responds with audio. The following sections cover session setup, audio streaming, speaker identification, DTMF, AI agent events, reconnection, and full call flows for audio sessions.

Open an audio session

Send the session open request

session.open must be the first application message sent after every WebSocket upgrade, on both the initial connect and all reconnects. The server expects it within 5 seconds; otherwise the connection is closed.

{
  "type": "command",
  "id":   "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
  "name": "session.open",
  "value": {
    "sessionType": "audio",
    "audioFormat": "pcmu",
    "contextVariables": {
      "ani":                "+14255550100",
      "dnis":               "+18005550123",
      "language":           "en-US",
      "sipUui":             "<optional SIP UUI value>",
      "customKey":          "customValue"
    }
  }
}
Field Required Description
sessionType Yes, must be "audio" Declares this as a voice session. The server initializes the audio codec pipeline, paced sender, and liveness watchdog.
audioFormat Yes One of "pcm16k", "pcm24k", "pcmu", "pcma". Declares the encoding you send and that the server uses for AI agent audio. See Supported audio formats.
contextVariables No A string-to-string map of call metadata passed to the AI agent. Maximum 50 keys, 64-character key name, 256-character value. Set once at session start; incremental updates aren't supported.

Important

Validation errors close the session immediately. The following conditions close the WebSocket with code 1002 (protocol error):

  • sessionType absent or unrecognized.
  • sessionType: "audio" without audioFormat.
  • An unrecognized audioFormat value.

Use context variables

The following key in contextVariables has a well-known meaning:

Key Format Description
ani E.164, for example +14255550100 The caller ANI (originating number).
dnis E.164, for example +18005550123 The dialed DNIS (destination number).

Important

ani and dnis are personal data. Your platform is responsible for handling them in compliance with applicable data protection requirements.

Receive the session opened acknowledgment

{
  "type": "event",
  "name": "session.opened",
  "value": {
    "sessionId": "bbbbbbbb-1111-2222-3333-cccccccccccc",
    "requestId": "aaaaaaaa-0b0b-1c1c-2d2d-333333333333",
    "startPaused": false
  }
}
value field Notes
sessionId Echoes the session ID for this connection.
requestId Echoes the Request-ID presented on the upgrade.
startPaused Optional, default false. When true, the session opens paused: don't stream media.chunk frames yet. The connector accepted the session but deferred starting the AI stream until it's resumed out-of-band (see the following note). When false or absent, stream immediately per the normal first-frame rule.

When startPaused is false or absent, begin sending media.chunk frames within 5 seconds. The server closes the connection with code 1002 if no audio arrives within this window.

Note

Paused sessions (deferred streaming). Some session modes open paused: the connector acknowledges session.opened with startPaused: true but doesn't connect the AI back end until a separate, out-of-band trigger starts the stream. While paused, you must not stream audio; pause your recording. The 5-second first-frame timeout doesn't apply while paused. If your platform sends media.chunk frames anyway, the connector ignores them (and logs a warning); no audio is delivered to the AI until the session resumes. When the connector resumes the session, it begins accepting audio. When the connector is ready to start, or later pause or resume, the stream, it sends a session.resume or session.pause event on the same WebSocket: on session.resume, resume sending media.chunk; on session.pause, stop.

Handle session pause and resume

The connector sends session.pause and session.resume events to control whether you stream audio. They're the counterpart to a paused open (startPaused: true): a paused session streams nothing until the connector sends the first session.resume.

{ "type": "event", "name": "session.resume", "value": {} }
{ "type": "event", "name": "session.pause", "value": {} }
Event Your platform's action
session.resume Begin (or resume) sending media.chunk frames. After a paused open, the first session.resume is what starts the AI stream.
session.pause Stop sending media.chunk frames until the next session.resume. Audio sent while paused is ignored by the connector.

Close an audio session

{
  "type": "command",
  "id":   "<uuid>",
  "name": "session.close",
  "value": { "reason": "normal" }
}
reason When to use
"normal" The call ended cleanly.
"transfer" Your platform is executing a transfer.
"error" Your platform encountered an error.

The reason field is informational; teardown behavior is the same regardless of value. The server immediately evicts the session and closes the WebSocket with code 1000. The sessionId can't be reused for reconnection after session.close.

Stream audio

Supported audio formats

Format Sample rate Frame size Interval Notes
pcm16k 16 kHz 640 bytes 20 ms PCM 16-bit mono; no transcoding.
pcm24k 24 kHz 960 bytes 20 ms PCM 16-bit mono; no transcoding.
pcmu 8 kHz 160 bytes 20 ms G.711 mu-law; server transcodes internally.
pcma 8 kHz 160 bytes 20 ms G.711 A-law; server transcodes internally.

PCM specification

Property Value
Encoding Linear PCM, 16-bit signed integer.
Byte order Little-endian (LE).
Channels Mono (1 channel) only. Stereo and multichannel audio isn't supported. A channels parameter in value.contentType, if present, must be 1; omitting it also means mono.
Silence representation PCM zeros (all bytes 0x00).
  • Strict frame sizes. Frames must be exactly 640 bytes (pcm16k), 960 bytes (pcm24k), or 160 bytes (G.711). An oversized frame is rejected with an error event (code 4002); a frame whose size isn't aligned to the format's sample unit is rejected with an error event (code 4000). Either way, the connection is then closed (1002).

  • Mono audio only. All audio is single-channel (mono). Stereo and multichannel audio isn't supported: the connector doesn't de-interleave or mix channels, so a stereo payload would play back garbled. If you include a channels parameter in value.contentType, it must be 1; omitting it also means mono. A frame that declares channels not equal to 1 is rejected with an error event (code 4000) and the connection is closed (1002). Multiple speakers are sent as separate mono streams distinguished by from.id (see Identify speakers), never as interleaved stereo channels.

Keep the audio stream alive

The server enforces two timeouts. Both are reset by any received media.chunk frame:

  • First-frame timeout (5 s): The first media.chunk must arrive within 5 seconds of session.opened. If none arrives, the connection is closed with code 1002.
  • Audio liveness timeout (5 s): A 5-second watchdog is reset by each frame. If no frame arrives within this window, the connection is closed with code 1001.

Hold or mute: Send silence frames (PCM zeros); don't pause the stream. A pause that exceeds the liveness timeout closes the session.

  • Send at least every 100-200 ms per speaker. The 5-second timeout is a bare minimum to keep the connection alive, not a target rate. Large bursts (for example, one batch every 4 seconds) produce severely degraded speech recognition. For best quality, send at ~20 ms intervals. The minimum acceptable quality rate is every 100-200 ms.

Tip

No ping/pong needed. The audio stream is the liveness signal. WebSocket-level ping/pong isn't required or supported.

Send audio with the media chunk message

{
  "type": "event",
  "id":   "<uuid>",
  "name": "media.chunk",
  "from": { "id": "bbbbbbbb-1111-2222-3333-cccccccccccc", "role": "caller" },
  "value": {
    "contentType": "audio/x-mulaw;rate=8000",
    "content": "<base64-encoded audio - exactly one 20 ms frame>"
  },
  "entities": [
    { "streamSequence": 42, "sampleOffset": 672000 }
  ]
}
Field Description
from.id UUID, stable per speaker for the entire call. See Identify speakers.
from.role The speaker's part in the call: "caller" (customer) or "agent" (human agent). See Identify speakers.
from.name Optional free-form display label. See Identify speakers.
value.contentType Optional. The MIME type for the frame. If omitted, the audioFormat declared in session.open is authoritative. If sent, it must match that format (media type and rate), and any channels must be 1. See Match content type to the audio format.
value.content Base64-encoded audio bytes: exactly one frame at the declared format.
entities[0].streamSequence A per-connection monotonic counter starting at 0. Resets on reconnect. Debug use only.
entities[0].sampleOffset The absolute sample index since session.open. Monotonically increases across the full session, including reconnects. Computed from a monotonic clock.

Note

sampleOffset formula: sampleOffset = elapsed_seconds_since_session_open x sample_rate. Example at 16 kHz, 30 seconds into the session: 30 x 16000 = 480000. Advance by 320 samples per frame (pcm16k) or 480 (pcm24k). Do not reset on reconnect; continue from where you left off.

Match content type to the audio format

The contentType field is optional. The audio encoding is authoritatively set by audioFormat in session.open; if you omit contentType, that declared format is used. If you do send contentType, it must be consistent with your declared audioFormat: its media type must match (case-insensitive), its rate parameter (if present) must equal your format's sample rate, and its channels parameter (if present) must be 1 (the protocol is mono-only).

audioFormat Required contentType
pcm16k audio/L16;rate=16000;channels=1
pcm24k audio/L16;rate=24000;channels=1
pcmu audio/x-mulaw;rate=8000
pcma audio/x-alaw;rate=8000

Note

Why audio/L16 for pcm24k? In audio/L16 the number is the bit depth, not the sample rate (RFC 2586). pcm24k is 16-bit PCM sampled at 24 kHz, so the media type is audio/L16 and the rate parameter carries 24000. audio/L24 means 24-bit audio and is rejected.

A contentType that contradicts the declared audioFormat, such as the wrong media type (for example, audio/x-alaw on a pcmu session, or audio/L24 on a pcm24k session), the wrong rate, or channels not equal to 1, ends the call with an error event (code 4000) and WebSocket close 1002, the same handling as a non-mono channels value.

Identify speakers

Every media.chunk and dtmf message must include a from field. Use a stable UUID per participant for the entire call: the same value on every frame, including after reconnects. The connector uses from.id as a pipeline key to keep each speaker's audio isolated. Set from.role to identify the speaker's part in the call: "caller" for the customer, "agent" for a human agent. from.name is an optional free-form display label.

from.id from.role from.name Meaning
UUID v4, for example "bbbbbbbb-1111-2222-3333-cccccccccccc" "caller" (optional label) Caller (customer) audio.
UUID v1, for example "bbbbbbbb-1c1c-2d2d-3e3e-444444444444" "agent" (optional label) Human agent audio (if present on the call).

Note

Backward compatibility. Earlier versions of this guide put the "caller"/"agent" label in from.name with from.role: "user". That form is still supported: when from.role isn't "caller" or "agent", the connector falls back to the label in from.name. New integrations should send the label in from.role.

A valid UUID is required. The server rejects media.chunk and dtmf frames that have a non-UUID from.id; the connection is closed with code 1002 (protocol error). Generate one UUID per participant at call start and reuse it.

Send DTMF digits

DTMF must be sent out-of-band as a dtmf event. The connector doesn't perform in-band tone detection on the audio stream; the dtmf event is the only guaranteed delivery path for digits to the AI agent.

{
  "type": "event",
  "id":   "<uuid>",
  "name": "dtmf",
  "from": { "id": "bbbbbbbb-1111-2222-3333-cccccccccccc", "role": "caller" },
  "value": { "digit": "5" }
}

Valid digit values are "0"-"9", "*", and "#". Send one event per keypad press. from.id must be a valid UUID; set from.role to "caller" or "agent" (from.name is an optional display label).

Receive AI agent audio

While the AI agent is speaking, the server delivers audio as media.chunk events. When the AI agent is silent, no frames are sent; the absence of frames means silence.

  • Identify AI agent audio by from.role = "bot" (a protocol field value; don't change it). The from.id is server-generated; don't parse or validate its format.
  • Encoding matches the audioFormat you declared in session.open.
  • Frames arrive in 200 ms batches (10 x 20 ms). Buffer and play them continuously to the caller.
  • When call.transfer or call.end arrives, the server already drained all buffered AI agent audio.
{
  "type": "event",
  "id":   "<uuid>",
  "name": "media.chunk",
  "from": { "id": "<server-generated>", "role": "bot" },
  "value": {
    "contentType": "audio/x-mulaw;rate=8000",
    "content": "<base64-encoded AI agent audio - 200 ms (10 x 20 ms frames)>"
  }
}

Plan for bandwidth

Unlike a raw-binary transport (for example, the Genesys connector), the Universal Connector carries every audio frame as a JSON media.chunk envelope with the audio Base64-encoded in value.content. Two sources of overhead sit on top of the raw media rate:

  • Base64 encoding: inflates the audio payload by ~33% (4 wire bytes per 3 audio bytes).
  • Activity Protocol JSON envelope: roughly 230-280 bytes per frame (type, id, name, from, value, entities; the two UUIDs alone are ~72 bytes). value.contentType is optional; omitting it trims ~40 bytes per frame.
  • WebSocket text framing: ~2-14 bytes per frame (negligible).

The overhead is asymmetric because of how frames are batched:

  • Inbound (your platform to the connector): one 20 ms frame per media.chunk, so 50 frames/s, meaning the per-frame JSON overhead is paid 50 times a second.
  • Outbound (connector to your platform): AI agent audio arrives in 200 ms batches (10 x 20 ms), so 5 frames/s, meaning the same envelope overhead is amortized over 10 times more audio.

Effective per-call wire bitrate

Raw media rate compared to after Base64 encoding plus a ~250-byte-per-frame envelope:

audioFormat Raw media rate Inbound (50 fps) Outbound (5 fps) Per call (full duplex)
pcmu / pcma (8 kHz G.711) 64 kbps ~185 kbps ~95 kbps ~0.28 Mbps
pcm16k (16 kHz PCM, default) 256 kbps ~440 kbps ~350 kbps ~0.79 Mbps
pcm24k (24 kHz PCM) 384 kbps ~610 kbps ~520 kbps ~1.13 Mbps

Tip

Prefer G.711 when bandwidth is constrained. The raw media rate is the audio payload only; the wire rate is what actually crosses the network after Base64 encoding and the JSON envelope. Choosing pcmu/pcma (G.711) roughly quarters the wire bandwidth of pcm16k. Prefer it when network capacity is the constraint and 8 kHz telephony quality is acceptable.

At scale: 1,000 concurrent calls

audioFormat Inbound Outbound Total (full duplex) Minimum external bandwidth*
pcmu / pcma ~185 Mbps ~95 Mbps ~280 Mbps ~285 Mbps
pcm16k (default) ~440 Mbps ~350 Mbps ~790 Mbps ~805 Mbps
pcm24k ~610 Mbps ~520 Mbps ~1.13 Gbps ~1.15 Gbps

* Includes ~2% headroom for TCP/IP overhead, WebSocket control frames, signaling (session.open/session.opened, speech.transcript, and so on), and DTMF.

Summary. Plan for roughly ~0.3 Mbps per call at 8 kHz G.711 (pcmu/pcma) or ~0.8 Mbps per call at 16 kHz (pcm16k, the default): about 280 Mbps or 800 Mbps respectively for ~1,000 concurrent calls. Because of the Base64 encoding plus JSON envelope, this is materially higher than a raw-binary connector at the same codec. Select the lowest audioFormat that meets your quality bar to minimize it.

Deployment considerations:

  • Azure side: no changes or additional requirements are expected for this bandwidth profile.
  • Customer side: bandwidth and network capacity should scale proportionally with concurrent call volume and with the chosen audioFormat. Size for pcm16k (the default) unless you explicitly negotiated G.711.

Handle server events

These events are sent by the server and apply to both audio and text sessions unless noted. call.transfer and call.end are audio sessions only; text sessions receive the modality-equivalent chat.transfer and chat.end events with the same { "contextVariables": {} } payload and terminal semantics. dialog.failed and error apply to both session types.

Handle barge-in signals

request.update is informational; no action is required. The server manages AI agent audio interruption internally.

{
  "type": "event",
  "id":   "<uuid>",
  "name": "request.update",
  "value": { "state": "detected" }
}
state Meaning
"detected" The caller started speaking while the AI agent was speaking (barge-in).
"processing" The server is processing the caller's utterance.

Handle transfers

The AI agent can request a transfer with call.transfer. The server closes the WebSocket with code 1000 immediately after sending; don't send session.close.

Note

Text sessions. Text sessions receive chat.transfer instead of call.transfer, with the identical { "contextVariables": {} } payload and semantics. Route the chat to transferDestination using your platform's handoff mechanism; the server then closes the WebSocket with code 1000.

{
  "type": "event",
  "id":   "<uuid>",
  "name": "call.transfer",
  "value": {
    "contextVariables": {
      "transferDestination": "agent-queue-01",
      "customOutputVar":     "some-value"
    }
  }
}
  1. Read value.contextVariables.transferDestination. Route the call to that destination using your platform's transfer mechanism.
  2. If transferDestination is empty or unrecognized, apply a fallback (for example, a catch-all queue). Don't drop the call.

Warning

transferDestination might be empty. Always implement a fallback. Never drop the call when the destination value is empty or unrecognized.

Output variable constraints

These constraints apply to call.transfer/chat.transfer and call.end/chat.end.

Constraint Value
Structure A flat string-to-string map under contextVariables.
Max keys 50
Max key length 64 characters
Max value length 256 characters

Handle conversation end

The AI agent completed the conversation with call.end. The server closes the WebSocket with code 1000 immediately after sending.

Note

Text sessions. Text sessions receive chat.end instead of call.end, with the identical { "contextVariables": {} } payload and semantics. Read the output variables and apply your post-chat logic; the server then closes the WebSocket with code 1000.

{
  "type": "event",
  "id":   "<uuid>",
  "name": "call.end",
  "value": {
    "contextVariables": {
      "callOutcome":     "resolved",
      "customOutputVar": "some-value"
    }
  }
}

Read any output variables from value.contextVariables and apply your post-call logic.

Handle AI errors

The AI service encountered an unrecoverable error, reported as dialog.failed. The server closes the WebSocket with code 1001 immediately after sending.

{
  "type": "event",
  "id":   "<uuid>",
  "name": "dialog.failed",
  "value": {
    "reason":  "BotError",
    "message": "The bot returned an unrecoverable error."
  }
}

Apply your fallback (for example, route to a human agent). See Dialog failed reason codes for all reason codes.

Receive real-time transcripts

speech.transcript carries real-time transcription of caller speech (speech-to-text) and AI agent speech. The plumbing is complete, and this event is enabled in all environments.

{
  "type": "event",
  "id":   "<uuid>",
  "name": "speech.transcript",
  "value": {
    "origin": "user",
    "text":   "Hello, I need help with my account"
  }
}

Valid origin values: "user" (caller speech), "agent" (AI agent response).

Reconnect an audio session

Your platform is solely responsible for detecting disconnects and reconnecting. The server preserves session state for a 15-second grace window after a disconnect.

Follow the reconnect procedure

  1. Detect the WebSocket disconnection.
  2. Connect to the same URL with the same sessionId, a new Request-ID, and a valid (non-expired) bearer token.
  3. Send session.open with the exact same sessionType: "audio" and audioFormat. Changing either closes the connection with a protocol error.
  4. Resume sending media.chunk. Do not reset sampleOffset; continue from where you left off.

Note

sampleOffset continuity across reconnects. Continue the sample offset counter across a reconnect instead of resetting it. For example:

t=30.0s: sampleOffset = 480000   <- last frame before disconnect
[2.5s disconnect]
t=32.5s: sampleOffset = 520000   <- first frame after reconnect (gap = 2.5 s encoded)

Reconnect scenarios

Scenario Behavior
Reconnect within the 15 s grace window The session is reattached, and the AI agent resumes the conversation.
Reconnect after the grace window expires The server treats it as a new session; prior context is lost.
Same sessionId and same Request-ID HTTP 409, replay rejected. Use a new Request-ID.
Same sessionId and new Request-ID (old connection still open) The server closes the old WebSocket (1001) and accepts the new connection.
Reconnect rate limit exceeded HTTP 429; apply exponential backoff.

Full audio call flow

Normal call: AI agent requests transfer

sequenceDiagram
    autonumber
    participant P as 3P Platform
    participant C as Contact Center Intelligence Universal Connector

    P  ->> C: GET wss://.../ws/orgId/channelId/sessionId (Authorization, Request-ID)
    C -->> P: 101 Switching Protocols
    P  ->> C: session.open { sessionType: "audio", audioFormat: "pcmu", contextVariables: {...} }
    C -->> P: session.opened
    loop Every 20ms
        P  ->> C: media.chunk { caller audio, sampleOffset: N }
        C -->> P: media.chunk { AI agent audio } (while AI agent is speaking)
    end
    C -->> P: call.transfer { contextVariables: { transferDestination: "agent-queue-01" } }
    C -->> P: WebSocket close (1000)
    P  ->> P: Route call to "agent-queue-01"

Normal audio call: session open, then audio streaming, then the AI agent requests a transfer, and the third-party platform routes to the destination.

Normal call: AI agent ends the conversation

sequenceDiagram
    participant P as 3P Platform
    participant C as Contact Center Intelligence Universal Connector

    P  ->> C: 101 + session.open { sessionType: "audio", audioFormat: "pcmu" }
    C -->> P: session.opened
    loop Every 20ms
        P  ->> C: media.chunk { caller audio }
        C -->> P: media.chunk { bot speech audio } (while speaking)
    end
    C -->> P: call.end { contextVariables: { ... } }
    C -->> P: WebSocket close (1000)

The bot completes the conversation and signals call.end.

AI error: fall back to a human agent

sequenceDiagram
    participant P as 3P Platform
    participant C as Contact Center Intelligence Universal Connector

    P  ->> C: 101 + session.open { sessionType: "audio" }
    C -->> P: session.opened
    loop Every 20ms
        P  ->> C: media.chunk { caller audio }
    end
    C -->> P: dialog.failed { reason: "BotError" }
    C -->> P: WebSocket close (1001)
    P  ->> P: Apply fallback (route to human agent)

On an AI error, the platform receives dialog.failed and applies its fallback.

Reconnect within the grace window

sequenceDiagram
    participant P as 3P Platform
    participant C as Contact Center Intelligence Universal Connector

    Note over P,C: Network drop
    P  -x C: WebSocket drops
    Note over C: Grace window: 15 seconds
    P  ->> C: GET wss://... SAME sessionId, NEW Request-ID, valid token
    C -->> P: 101 Switching Protocols
    P  ->> C: session.open { SAME sessionType: "audio", SAME audioFormat }
    C -->> P: session.opened
    Note over P,C: Resume audio - sampleOffset continues from pre-disconnect value

On reconnect: same sessionId, new Request-ID, and sampleOffset never resets.

Text sessions use chat over WebSocket. Your platform sends text messages, and the AI agent replies with text; there's no audio in either direction. The WebSocket upgrade and token acquisition are identical to audio sessions; only the session.open value differs.

Open a text session

Tip

Bot requirements for text sessions. The channel must be configured with a Classic Copilot Studio chat bot, not a voice or GPT-Realtime (speech-to-speech) bot. Voice-only bots don't have a text/messaging interface and cause the session to fail.

Send the session open request for text

{
  "type": "command",
  "id":   "cccccccc-2222-3333-4444-dddddddddddd",
  "name": "session.open",
  "value": {
    "sessionType": "text",
    "contextVariables": {
      "ani":        "+14255550100",
      "custom_key": "custom_value"
    }
  }
}
Field Required Description
sessionType Yes, must be "text" Declares this as a text session. The connector skips all audio infrastructure. Including audioFormat with sessionType: "text" is a protocol error.
audioFormat Must be absent Including audioFormat in a text session closes the connection with code 1002.
contextVariables No Same as audio sessions: a string-to-string map, maximum 50 keys, 64-character key, 256-character value.

After session.open, the server responds with session.opened (see Receive the session opened acknowledgment). For text sessions, don't send media.chunk; the server closes the connection with code 1002 (protocol error).

Close a text session

Closing a text session is identical to audio sessions (see Close an audio session). The reason field accepts the same values.

Server events on text sessions

The following server events apply to text sessions:

Event Meaning for text sessions
session.opened The session is accepted; ready to send text.message.
text.message The AI agent's text response. See Receive text.
chat.transfer The AI agent requests a chat transfer. Same payload and semantics as call.transfer; route to transferDestination, then the server closes the WebSocket (1000). See Handle transfers.
chat.end The AI agent ends the chat. Same payload and semantics as call.end; apply post-chat logic, then the server closes the WebSocket (1000). See Handle conversation end.
dialog.failed An AI service error. The server closes the WebSocket (1001). Apply your fallback. See Handle AI errors.
error A protocol violation. See Error event codes.

Note

Text terminal events. call.transfer and call.end remain audio-only names. Text sessions receive chat.transfer/chat.end instead, with the same contextVariables payload shape and terminal close behavior (WebSocket close 1000).

Exchange text

Send text

Send a text.message event any time during an active text session. The from field is required and must carry a stable UUID identifying the sender.

{
  "type": "event",
  "id":   "<uuid>",
  "name": "text.message",
  "from": { "id": "bbbbbbbb-1111-2222-3333-cccccccccccc", "role": "caller" },
  "value": {
    "content": "I need to update my billing address"
  }
}
Field Required Description
from.id Yes A valid UUID, stable for the session lifetime. Non-UUID values are rejected; the connection is closed with code 1002 (protocol error).
from.role Yes The speaker's part in the call: "caller" (customer) or "agent" (human agent).
from.name No An optional free-form display label.
value.content Yes A non-empty text string. Blank or whitespace-only content is silently dropped.

Note

Audio sessions reject text.message. Sending text.message on a sessionType: "audio" session closes the connection with code 1002.

Receive text

When the AI agent produces a text response, the server delivers it as a text.message event.

{
  "type": "event",
  "id":   "<uuid>",
  "name": "text.message",
  "value": {
    "content": "Your appointment is confirmed for Monday at 2 PM."
  }
}
Field Description
value.content The AI agent's text response. Display this to the end user.

Note

No from field on server-sent text.message. Outbound text.message events (server to your platform) don't include a from field. They're always AI agent responses.

Full text session flow

sequenceDiagram
    autonumber
    participant P as 3P Platform
    participant C as Contact Center Intelligence Universal Connector

    P  ->> C: GET wss://.../ws/orgId/channelId/sessionId (Authorization, Request-ID)
    C -->> P: 101 Switching Protocols
    P  ->> C: session.open { sessionType: "text", contextVariables: {...} }
    C -->> P: session.opened
    P  ->> C: text.message { from: { id: "UUID", role: "caller" }, value: { content: "Hello" } }
    C -->> P: text.message { value: { content: "Hi! How can I help you?" } }
    P  ->> C: text.message { from: { id: "UUID", role: "caller" }, value: { content: "Update my address" } }
    C -->> P: text.message { value: { content: "Sure, what is your new address?" } }
    alt Bot transfers chat
        C -->> P: chat.transfer { contextVariables: { transferDestination: "agent-queue-01" } }
        C -->> P: WebSocket close (1000)
        P  ->> P: Route chat to "agent-queue-01"
    else Bot ends chat
        C -->> P: chat.end { contextVariables: { ... } }
        C -->> P: WebSocket close (1000)
        P  ->> P: Apply post-chat logic
    end

In a text session, the bot either transfers the chat (chat.transfer) or ends it (chat.end), and the server then closes the WebSocket (1000).

Handle text session inactivity and reconnects

Inactivity timeout

If no text.message is received for 30 minutes, the server closes the session with code 1001 (going away). This mirrors the audio liveness mechanism.

Session type Liveness signal Timeout Close code
Audio media.chunk frames 5 s gap 1001
Text text.message events 30 min inactivity 1001

Tip

Keep text sessions alive during idle periods. If your platform may have extended gaps between messages (for example, a user who opens a chat window and goes idle), send a periodic text.message to prevent the 30-minute timeout.

Follow the reconnect procedure for text sessions

Reconnection for text sessions follows the same 15-second grace window as audio sessions (see Reconnect an audio session).

  1. Detect the WebSocket disconnection.
  2. Connect to the same URL with the same sessionId, a new Request-ID, and a valid bearer token.
  3. Send session.open with the exact same sessionType: "text". Changing sessionType on reconnect closes the connection with a protocol error.
  4. Resume sending text.message. The 30-minute inactivity timer resets on reconnect.

Error codes, the complete message catalog, and integration checklists follow.

Error and close code catalog

WebSocket close codes

Code Meaning Action
1000 Normal closure. The AI agent ended (call.end/chat.end) or transferred (call.transfer/chat.transfer) the session, or you sent session.close. No action needed.
1001 Going away. Audio liveness timeout (5 s), text session inactivity timeout (30 min), dialog.failed, or the server force-closing the previous connection when a newer one is accepted for the same session. Reconnect if the session is still active.
1002 Protocol error. A first-frame timeout; a reconnect whose sessionType/audioFormat doesn't match the original; or any inbound validation failure (malformed JSON, invalid session.open, invalid media.chunk/dtmf/text.message, or a binary frame). Fix the implementation; don't retry the same payload.
1008 Policy violation. A second connection was opened while this session was already active. Keep a single connection per session.
1011 Internal server error. The server failed during teardown (for example, draining or sending the final audio). Reconnect or start a new session.
4000 Session not found. sessionId unknown, or the reconnect grace window (15 s) expired. Start a new session.

Error event codes

The connector emits only two error event codes. Many protocol violations are reported purely by closing the WebSocket with code 1002 and carry no error event (for example, a missing or unrecognized sessionType, a sessionType/audioFormat mismatch, a message type that doesn't match the session, a non-UUID from.id, malformed JSON, or a binary frame). Build your handling around the close code and treat any error event as extra detail.

code Meaning Action
4000 A protocol or validation error the server reports before closing: the first message wasn't session.open; the session.open payload couldn't be parsed; a reconnect's session.open didn't match the original; a media.chunk contentType contradicted the declared audioFormat (wrong media type, rate, or non-mono channels); invalid base64; or an empty or size-misaligned audio frame. The connection is then closed with 1002. Fix the payload and reconnect.
4002 An audio frame exceeded the maximum size for the declared format. The connection is then closed with 1002. Send exactly 640, 960, or 160 bytes per frame.

Dialog failed reason codes

reason Meaning
"BackendUnavailable" The AI back end was unreachable or failed to initialize.
"BotError" The bot returned an unrecoverable error.
"AuthExpired" AI service session credentials expired.
"Unknown" An unclassified failure. Check the message field for details.

Message reference

The following tables catalog every message name that the connector recognizes or emits. Status Live means production-ready; status In progress means not yet available in production.

Inbound messages

Messages your platform sends to the server.

Name type Session Status Value schema
session.open command Both Live { "sessionType": "audio"|"text", "audioFormat": "..." (audio only), "contextVariables": {} }. See Send the session open request and Send the session open request for text.
media.chunk event Audio only Live { "contentType": "audio/...", "content": "<base64>" } plus entities[0] with streamSequence, sampleOffset. See Send audio with the media chunk message.
dtmf event Audio only Live { "digit": "0"-"9" | "*" | "#" }. One per keypad press. See Send DTMF digits.
session.close command Both Live { "reason": "normal"|"transfer"|"error" }. Informational only. See Close an audio session.
text.message event Text only Live { "content": "<string>" }. from.id is a valid UUID; from.role is "caller"/"agent". Blank content is silently dropped. See Send text.

Outbound messages

Messages the server sends to your platform.

Name type Session Status Value schema and action
session.opened event Both Live { "sessionId": "...", "requestId": "..." }. Audio: begin sending media.chunk within 5 s. Text: ready to send text.message.
media.chunk event Audio only Live { "contentType": "...", "content": "<base64>" }. from.role = "bot" (identifies AI agent audio). 200 ms batches. Play to the caller. See Receive AI agent audio.
request.update event Audio only Live { "state": "detected"|"processing" }. A barge-in signal. Informational; no action. See Handle barge-in signals.
call.transfer event Audio only Live { "contextVariables": {} }. Route to transferDestination (may be empty). The server closes the WebSocket (1000). See Handle transfers.
chat.transfer event Text only Live { "contextVariables": {} }. Route to transferDestination (may be empty). The server closes the WebSocket (1000). See Handle transfers.
call.end event Audio only Live { "contextVariables": {} }. Apply post-call logic. The server closes the WebSocket (1000). See Handle conversation end.
chat.end event Text only Live { "contextVariables": {} }. Apply post-chat logic. The server closes the WebSocket (1000). See Handle conversation end.
dialog.failed event Both Live { "reason": "...", "message": "..." }. Apply a fallback. The server closes the WebSocket (1001). See Handle AI errors and Dialog failed reason codes.
error event Both Live { "code": <int>, "message": "..." }. A protocol violation. See Error event codes.
speech.transcript event Audio only Live { "origin": "user"|"agent", "text": "..." }. Real-time transcription of caller and AI agent speech. See Receive real-time transcripts.
text.message event Text only Live { "content": "..." }. The AI agent's text response. No from field. See Receive text.

Integration checklists

Common checklist

Applies to both session types.

  • [ ] Microsoft Entra app registration created in the customer's Power Platform tenant.
  • [ ] App submitted to Microsoft for provisioning; org ID and channel ID received.
  • [ ] Token acquired via the client credentials flow using scope=<your-app-client-id>/.default.
  • [ ] Token placed in the Authorization: Bearer header, never in the URL.
  • [ ] sessionId in the URL is a UUID v4 (the session ID is taken from the URL; there's no sessionId header).
  • [ ] Request-ID is a new UUID on every connect attempt (initial and reconnects).
  • [ ] (Browser clients only) Credentials sent via Sec-WebSocket-Protocol instead: bearer.<token>, requestid.<uuid>, <connector-subprotocol> (see Present credentials on the upgrade, Option B).
  • [ ] session.open is the first message sent after every WebSocket upgrade.
  • [ ] sessionType is present in session.open.
  • [ ] After dialog.failed: apply a fallback (for example, route to a human agent); the server closes the WebSocket (1001) automatically.
  • [ ] Unknown inbound message name values are silently ignored.
  • [ ] On reconnect: same sessionId, new Request-ID, same sessionType.
  • [ ] HTTP 409 handled: new Request-ID and retry.
  • [ ] HTTP 429 handled: exponential backoff.
  • [ ] WebSocket close 1001 handled: reconnect if the session is still active.
  • [ ] WebSocket close 4000 handled: start a new session.

Audio session checklist

Additional checks for audio sessions.

  • [ ] sessionType: "audio" with audioFormat present in session.open.
  • [ ] After call.transfer: read contextVariables.transferDestination and route; the server closes the WebSocket (1000) automatically.
  • [ ] After call.end: apply post-call logic; the server closes the WebSocket (1000) automatically.
  • [ ] First media.chunk sent within 5 s of session.opened.
  • [ ] Audio sent continuously: silence frames (PCM zeros) sent during hold/mute, and the stream is never paused.
  • [ ] PCM frames are little-endian; frame sizes are 640 bytes (pcm16k), 960 bytes (pcm24k), 160 bytes (G.711).
  • [ ] from.id is a stable UUID per participant for the entire call lifetime.
  • [ ] from.role identifies the speaker: "caller" (customer) or "agent" (human agent).
  • [ ] from.name is an optional free-form display label.
  • [ ] DTMF sent out-of-band as dtmf events, not relied on from in-band tone detection.
  • [ ] sampleOffset computed from a monotonic clock; doesn't reset on reconnect.
  • [ ] On reconnect: session.open sent with the exact same audioFormat.
  • [ ] No text.message sent on audio sessions.

Text session checklist

Additional checks for text sessions.

  • [ ] sessionType: "text" with no audioFormat in session.open.
  • [ ] Channel configured with a Classic Copilot Studio chat bot (not a voice or GPT-Realtime bot).
  • [ ] No media.chunk sent on text sessions.
  • [ ] text.message from.id is a valid UUID, stable for the session lifetime.
  • [ ] text.message from.role is "caller" or "agent".
  • [ ] Inbound text.message (server to platform) value.content displayed to the end user.
  • [ ] After chat.transfer: read contextVariables.transferDestination and route; the server closes the WebSocket (1000) automatically.
  • [ ] After chat.end: apply post-chat logic; the server closes the WebSocket (1000) automatically.
  • [ ] WebSocket close 1000 handled after chat.transfer/chat.end or your session.close.
  • [ ] Platform sends a periodic text.message if idle periods might exceed 30 minutes.
  • [ ] WebSocket close 1001 handled: reconnect (text inactivity timeout or disconnect).
  • [ ] On reconnect: session.open sent with the same sessionType: "text".

Overview of Contact Center Intelligence