Custom Agent Desk Developer Guide 2.0

Agent Desk Developer Guide

Purpose

This guide is for developers who need to build a custom Agent Desk against Expertflow CX.

It describes the contracts you must implement in your own application:

  1. Agent Manager — REST login and Socket.​IO events for presence, CHAT MRD, conversation subscription, and CIM.

  2. efSwitch SIP wrapper — WebRTC voice for CX VOICE MRD (login, ring, answer, call control).

Use this page as the implementation reference: endpoints, event names, payload fields, and the order of operations. UI layout and framework choice are yours.

What to implement first

  1. Agent Manager REST login → Keycloak user (includes agentExtension for voice).

  2. Socket.​IO connect to /agent-manager with the auth contract in Step 2.

  3. Parent agent state READY, then CHAT MRD READY (if you handle chat).

  4. For voice: set sipConfig, then SIP login through wrapper.js with domain + wssUrl, then CX VOICE MRD READY after agentInfo.state === "LOGIN".

  5. CHAT: taskRequesttopicSubscriptiononTopicData / CIM.

  6. CX VOICE: SIP ring → CCM intents → answerCall → topic subscription when the voice task is STARTED.

Technical Overview

The diagram below is the integration surface for a custom Agent Desk: Agent Manager (REST + Socket.​IO) and efSwitch (SIP/WebRTC media). Implement against these interfaces whether the desk runs in a browser, mobile app, or desktop client. PSTN and WebRTC voice both terminate on efSwitch.

  • Agent Manager REST — authenticate the agent and resolve tenant media settings (mediaServer.wssUrl).

  • Agent Manager Socket.​IO — agent presence, parent/MRD state, CHAT offers (taskRequest), topic subscription, and CIM.

  • SIP wrapper — establish the CX VOICE media path. Register the extension, receive ringing, answer/hold/mute/release. Do not invent a separate SIP stack for CX VOICE.

Assumptions and Constraints

The integration of frontend UI wrappers and visual presentation is left at the developer's discretion and is not constrained by this guide.

newimage-developer-guide.png


Prerequisites

Required endpoints and libraries before you initialize Socket.​IO or send the efSwitch JS login command:

1. General Agent Desk Prerequisites

Establish the connection endpoints used for REST authentication and Socket Events.

  • Agent Manager Server: Accessible endpoint providing REST authentication and Socket.​IO presence gateways.

  • Socket.​IO Client: Version 4.4.0 for connection handling and presence synchronization.

2. Voice-Specific Prerequisites

Complete these items if you want to make sip connection. Without them, SIP registration and WebRTC calls fail.

  • SIP Wrapper files: Include sip-0.21.2.min.js in the custom desk and load it before wrapper.js. The wrapper depends on sip-0.21.2.min.js. If that file is missing or loaded after the wrapper, SIP registration fails.

  • CryptoJS Library (optional): crypto-js.min.js — decrypt static extension credentials when encrypted static passwords (EXT_STATIC) are configured.

  • Media Server / PBX: efSwitch with WebSocket Secure (WSS) enabled (typical port 7443). Get the WSS URL from the CX Tenant API (GET https://{FQDN}/cx-tenant/tenant/{tenantId}) — the response mediaServer object includes wssUrl. Set that value on sipConfig.wss.

  • Browser Microphone Permission: navigator.mediaDevices.getUserMedia({ audio: true, video: false }).

3. SIP Wrapper Configuration (sipConfig)

Initialize the efSwitch JS / SIP wrapper with a sipConfig object before Agent Manager login and before running the efSwitch login command. wss and uri are required for any voice session.

Important: Declare var sipConfig = { ... } in a <script> tag before loading wrapper.js. The wrapper evaluates let sipconfig = sipConfig at load time; if sipConfig is missing, SIP commands throw Cannot access 'sipconfig' before initialization.

To get the WSS URL, call the CX Tenant API. The response contains a mediaServer object. Use mediaServer.wssUrl as sipConfig.wss.

GET https://{FQDN}/cx-tenant/tenant/{tenantId}

TenantId usually the subdomain name :

https://<subdomain>.expertflow.com

Example: https://{FQDN}/cx-tenant/tenant/{tenantId}

JSON
"mediaServer": {
        "wssUrl": "wss://10.192.9.92:7443",
        "domainManagerUrl": "http://10.192.9.92/add-domain/",
        "voiceConnectorUrl": "https://10.192.9.92"
      }

Set sipConfig.wss from mediaServer.wssUrl. sipConfig.uri should be the tenant subdomain (same value later sent as login parameter.domain, for example ux-controls-02). The login command also passes domain and wssUrl, which override sipConfig.uri / sipConfig.wss in the wrapper.

JavaScript
var sipConfig = {
  // WebSocket to efSwitch (SIP signaling)
  wss: "wss://10.192.9.92:7443",
  // FROM: CX Tenant API → mediaServer.wssUrl
  // GET https://{FQDN}/cx-tenant/tenant/{tenantId}
  // Agent Desk config.json key: SIP_SOCKET_URL (often filled from tenant)

  // SIP domain / host in sip:<extension>@<uri>
  uri: "10.192.9.92",
  // FROM: media-server host (often host part of wssUrl), or SIP_URI in config.json
  // Agent Desk may also set/override from tenant domain / parameter.domain on login

  // Shared static password for extensions
  agentStaticPassword: "1234",
  // FROM: decrypted EXT_STATIC (AES) in desk config — same password for CX extensions
  // Not the Agent Manager / Keycloak password

  // Console SIP.js logs
  enable_sip_log: true,
  // FROM: desk config ENABLE_SIP_LOGS

  // SIP.js log level when logging is on
  loglevel: "log",
  // FROM: desk/local choice (error | warn | log | debug) — optional

  // ICE gather wait (ms) before SDP completes
  iceGatheringTimeout: 500,
  // FROM: desk/local tuning — increase if NAT/audio fails

  // Prefix for queue blind transfer / queue consult
  staticQueueTransferDn: "99887766",
  // FROM: desk config STATIC_QUEUE_TRANSFER_DN
  // Preconfigured DN on efSwitch / dialplan; wrapper sends {DN}-{queue}

  // Auto-answer timer (seconds) for outbound/campaign on agent phone
  autoCallAnswer: 5,
  // FROM: desk config AUTO_CALL_ANSWER_TIMER (min 5)

  // Also post CTI events on window.postMessage for CRM gadgets
  voicePostMessageSending: false,
  // FROM: desk config Enable_Voice_Events_For_CRM

  // Supervisor silent monitor DN
  monitoringDn: "*44",
  // FROM: desk config SIP_MONITORING_DN
  // Preconfigured feature code / dialplan on media server (e.g. FusionPBX dialplan)

  // Prefix for external / PSTN transfer & consult
  staticExternalDn: "99887765"
  // FROM: desk config SIP_EXTERNAL_DN
  // Preconfigured DN on efSwitch / dialplan; wrapper sends {DN}-{number}
};

Parameter

Required

Type

Purpose

wss

Yes

string

WebSocket Secure URL of efSwitch (typical port 7443) Example: wss://10.192.9.92:7443. The wrapper opens SIP signaling over this connection.

uri

Yes

string

SIP domain used in the agent SIP URI (sip:<extension>@<uri>). Use the tenant subdomain (example: ux-controls-02). Login parameter.domain overrides this.

agentStaticPassword

Conditional

string

Static password for media-server extensions. In CX, the same password is shared across agents. In CX deployments, this is often the decrypted EXT_STATIC.

enable_sip_log

No

boolean

When true, the wrapper prints SIP.js / SIP signaling logs to the browser console. Use only for troubleshooting.

loglevel

No

string

SIP.js console log level when logs are enabled. Typical values: error, warn, log, debug.

iceGatheringTimeout

No

number (ms)

How long the browser waits to gather WebRTC ICE candidates before completing SDP. Sample: 500. Increase if agents are behind restrictive NAT and audio fails to connect.

staticQueueTransferDn

Conditional

string

Pre-configured efSwitch DN used as the prefix for queue blind transfer and queue consult. The wrapper sends {staticQueueTransferDn}-{queue} (example: 99887766-support-queue).

autoCallAnswer

No

number (seconds)

Timer to auto-accept an outbound / campaign call on the agent phone. Minimum 5 seconds. Set only if auto-answer is required.

voicePostMessageSending

No

boolean

When true, the wrapper also posts CTI voice events on window.postMessage so a CRM or third-party gadget can consume them. When false.

monitoringDn

Conditional

string

Pre-configured DN used to start supervisor silent monitor. Required for silentMonitor. Sample: *44.

staticExternalDn

Conditional

string

Pre-configured DN prefix for external / PSTN transfer and consult. The wrapper sends {staticExternalDn}-{number} (example: 99887765-03335598849).

How the wrapper consumes sipConfig

This object is global. On desk load the custom desk sets var sipConfig = { wss, uri, agentStaticPassword, ... } and loads the wrapper. The wrapper copies it with let sipconfig = sipConfig and uses those values for SIP. SIP login only needs the agent extension (and callback). The wrapper then opens sipconfig.wss and REGISTER’s sip:<extension>@<sipconfig.uri> using sipconfig.agentStaticPassword.

Use Cases

The sections below specify the events, payloads, and command sequences the custom desk must implement. (CIM Messages+Events+and+Activities)

1. Establish Connection with Agent Manager & Voice Initialization Flow

Agent Login (REST API)
        ↓
Keycloak Agent Information & Voice Attribute Available (Assigned via Unified Admin)
        ↓
Socket.​IO Connection Established (Presence Initialized)
        ↓
efSwitch JS API command: login  (registers SIP extension)
        ↓
Event: agentInfo (state = LOGIN)
        ↓
Agent Ready to Send & Receive Voice Calls and Digital Interactions
Step 1: Login to Agent Manager

To log the agent into Agent Manager:

  1. Initiate a POST request to /agent-manager/agent/login with { username, password }. A successful response provides the Keycloak user/agent details, including agent.attributes.agentExtension. User Login API in Postman.

  2. Determine the SIP extension password from Keycloak credentials or decrypt the static ciphertext (EXT_STATIC) using AES decryption (Only for Voice):

JavaScript
let sipPassword = userPassword;
if (appConfig.EXT_STATIC && typeof CryptoJS !== "undefined") {
  const decrypted = CryptoJS.AES.decrypt(appConfig.EXT_STATIC, "<AES_KEY>").toString(CryptoJS.enc.Utf8);
  if (decrypted) sipPassword = decrypted;
}
Step 2: Establish Socket.​IO Connection

Make the Socket.​IO handshake with the Agent Manager:

  • On successful connection, the socket receives a connect event.

  • On failure, a connect_error event is received with error details.

AGENT_MANAGER_FQDN is not the site root. Use the Agent Manager base URL:

https://{FQDN}/agent-manager

Example: https://ux-controls-02.expertflow.com/agent-manager

With that URI, pathname is /agent-manager, so the Socket.​IO path becomes /agent-manager/socket.io. Connecting to https://{FQDN}/ (path /socket.io) hits the frontend SPA HTML and the socket never establishes.

Auth contract (required):

  • auth.agent must be a JSON string of the Keycloak user object from login (JSON.stringify(keycloakUser)), not a raw object.

  • auth.fcm must be an object: { desktopFcmKey: null, mobileFcmKey: null } (not an empty string).

  • Pass query.username with the agent username.

JavaScript
import { io } from "socket.io-client";

// REQUIRED: include /agent-manager — not the bare FQDN root
let uri = "https://{FQDN}/agent-manager";
let origin = new URL(uri).origin;
let path = new URL(uri).pathname; // "/agent-manager"

let socket = io(origin, {
  path: path === "/" ? "/socket.io" : path + "/socket.io", // -> /agent-manager/socket.io
  auth: {
    agent: JSON.stringify(keycloakUser),
    fcm: { desktopFcmKey: null, mobileFcmKey: null }
  },
  query: {
    username: keycloakUser.username
  }
});

After connecting with the Agent Manager, the agent receives the initial agentPresence event.

agentPresence MRD shape (use this to locate chat / voice MRDs):

JavaScript
// Simplified shape
{
  agent: { id, username, ... },
  state: { name: "NOT_READY" | "READY" | "LOGOUT", ... },
  agentMrdStates: [
    {
      mrd: { id: "<mrd-uuid>", name: "CX VOICE" /* or "CHAT" */ },
      state: "READY" | "NOT_READY" | ...
    }
  ]
}

The socket agentPresence event is often an envelope: { action, tenantId, correlationId, agentPresence: { agent, state, agentMrdStates } }. Normalize with const presence = payload.agentPresence || payload.

Voice MRD id is agentMrdStates[i].mrd.id where mrd.name is typically CX VOICE (space). Do not require underscore CX_VOICE — normalize spaces/underscores, or match configured CX_VOICE_MRD id. Do not invent a top-level mrdId field on the presence row.

JavaScript
function isVoiceMrd(name) {
  const n = (name || "").toUpperCase().replace(/[_\s]+/g, " ").trim();
  return n === "CX VOICE" || n.includes("CX VOICE");
}
Step 3: Initialize Voice (efSwitch login)

Once the agent information is available:

  1. Extract the agent extension from agent.attributes.agentExtension[0]. If the voice attribute was not assigned in keycloak, this field is empty and SIP connection fails to establish with efSwitch.

  2. Send the efSwitch login command see here

2. Change Agent States & Voice MRD Management

a. Change Parent State of Agent

For changing the agent's parent state, the custom Agent Desk emits the event ‘changeAgentState’:

  • The action parameter must be "agentState".

  • The state object uses { name, reasonCode }.

  • name values: "READY", "NOT_READY", or "LOGOUT".

  • With "READY", reasonCode must be null. For "NOT_READY" / "LOGOUT", reasonCode may be null or a reason object/code.

JavaScript
// Parent READY (required before any MRD can be READY)
socket.emit("changeAgentState", {
  agentId: currentAgent.id,
  action: "agentState",
  state: { name: "READY", reasonCode: null }
});

// Parent NOT_READY
socket.emit("changeAgentState", {
  agentId: currentAgent.id,
  action: "agentState",
  state: { name: "NOT_READY", reasonCode: null }
});
b. Change MRD State of Agent

For changing the agent's Media Routing Domain (MRD) state:

  • The action parameter must be "agentMRDState".

  • The state parameter is the string "READY" or "NOT_READY" (not an object).

  • The mrdId must be agentMrdState.mrd.id from agentPresence.agentMrdStates.

JavaScript
socket.emit("changeAgentState", {
  agentId: currentAgent.id,
  action: "agentMRDState",
  state: "READY", // or "NOT_READY"
  mrdId: agentMrdState.mrd.id
});
c. Chat + Voice MRD ready sequence

Recommended sequence so the agent can receive chat and voice:

  1. Socket connects → wait for agentPresence.

  2. Emit parent agentState READY.

  3. Locate the CHAT MRD (mrd.name === "CHAT") and emit agentMRDState READY using mrdId: row.mrd.id. Do not set EMAIL / CISCO CC / other MRDs unless your product needs them.

  4. Complete efSwitch SIP login. When callback agentInfo.state === "LOGIN", locate Voice MRD:

JavaScript
const voiceMrd = agentPresence.agentMrdStates.find(
  (row) => isVoiceMrd(row.mrd?.name)
);
  1. Emit Voice MRD READY using mrdId: voiceMrd.mrd.id.

NOTE: Parent must be READY before any MRD can be READY. Setting parent to NOT_READY forces all MRDs to NOT_READY. Voice MRD should only go READY after SIP registration succeeds (agentInfo / LOGIN).

3. Agent Presence Event

On every state change request, the Agent Manager emits the event agentPresence:

  • AGENT_STATE_CHANGED: When the state change is successfully applied.

  • AGENT_STATE_UNCHANGED: When the state cannot be changed at that time.

NOTE: In order to set an MRD state to READY, the agent's parent state must be READY first. When the agent's parent state is set to NOT_READY, all MRD states automatically transition to NOT_READY.

4. Push Mode Tasks (Digital / Chat)

a. Request Received by Custom Agent Desk

Whenever a task request is initiated from the routing engine, custom Agent Desk receives a taskRequest: event:

  • The task state is set to RESERVED and a RONA timer is initiated.

  • If accepted within the RONA duration, the task transitions to ACTIVE. Otherwise, it transitions to CLOSED (reason: RONA) and Agent Manager emits ‘revokeTask.

b. To Accept a Task

When taskRequest.taskState.name is RESERVED, accept by emitting Socket.IO topicSubscription. Agent Manager then moves the task to active and replies with onTopicData. Until onTopicData arrives, the agent is not joined to the conversation topic.

Required payload (this shape is easy to get wrong):

JavaScript
socket.emit("topicSubscription", {
  topicParticipant: {
    id: crypto.randomUUID(),           // new UUID for this subscription
    type: "AGENT",
    participant: {
      id: currentAgent.id,
      participantType: "CCUser",       // required
      keycloakUser: currentAgent,      // full Keycloak user from agent login — NOT the raw user at this level
      associatedRoutingAttributes: []
    },
    token: null,
    conversationId: taskRequest.conversationId,
    role: taskRequest.taskDirection === "CONSULT" ? "ASSISTANT" : "PRIMARY",
    userCredentials: null,
    state: "SUBSCRIBED"
  },
  agentId: currentAgent.id,
  conversationId: taskRequest.conversationId,
  roomInfo: taskRequest.roomInfo,      // pass through from taskRequest
  taskId: taskRequest.taskId
});

Common mistake: putting the Keycloak user directly in topicParticipant.participant. It must be wrapped as a CCUser with participantType: "CCUser" and keycloakUser: <login user>. If the wrap is missing, the task may still flip to active while the agent never becomes a conversation participant / never receives usable onTopicData.

  • Upon acceptance, Agent Manager emits onTopicData containing conversation details. Use that event to open the conversation panel.

  • Ack required: Agent Manager sends onTopicData with a Socket.IO acknowledgement callback. Reply with { status: "ok" } (Agent Desk does this). Without the ack, subscription / message delivery can stall.

Voice does not use taskRequest: to ring the agent. Inbound voice is offered as a CTI event newInboundCall from the efSwitch JS API after the extension is registered.

c. Inbound Voice — SIP states, CCM events, and Topic Subscription

Inbound ringing does not arrive as Socket.​IO taskRequest:. The SIP wrapper sends the dialog. The custom desk copies that dialog onto a CCM message and POSTs {CCM_URL}/message/receive. The wrapper constructs the full dialog (fields below). You extract values from it and from the logged-in agent.

How the wrapper constructs the dialog — the desk receives this on the callback. Do not invent it. Copy response.dialog onto CCM body.dialog.

Dialog field

Wrapper sets from

Example

id

SIP X-Call-Id (fallback Call-ID)

hbiq7ptr2ljpm2fjf0oc

ani / fromAddress / customerNumber

SIP From user

1002

dnis / dialedNumber / serviceIdentifier

SIP X-Destination-Number (DID)

2555

state

ALERTING on ring, ACTIVE after answer, DROPPED on hangup

ALERTING

participants[0].mediaAddress

Logged-in SIP extension

2001

participants[0].alertingTime / startTime

Wrapper clock at ring / answer

ISO timestamp

callEndReason / isCallEnded

Set when the call drops

AGENT-HANDLED / 1

Step 1 — Call arrives: SIP alerting dialog, then POST CCM

When a call comes to the agent desk, efSwitch sends a ringing event (newInboundCall, dialog state ALERTING). Show the ringing screen from that dialog (caller number, Accept). Then, on the same event, build the CCM CALL_ALERTING message from this dialog and POST it to CCM.

JSON
{
  "event": "newInboundCall",
  "response": {
    "loginId": "2001",
    "dialog": {
      "id": "hbiq7ptr2ljpm2fjf0oc",
      "ani": "1002",
      "customerNumber": "1002",
      "associatedDialogUri": null,
      "callbackNumber": null,
      "outboundClassification": null,
      "scheduledCallbackInfo": null,
      "isCallEnded": 0,
      "eventType": "PUT",
      "callType": "OTHER_IN",
      "queueName": "voice regression",
      "queueType": "NAME",
      "dialedNumber": "2555",
      "dnis": "2555",
      "serviceIdentifier": "2555",
      "secondaryId": null,
      "state": "ALERTING",
      "isCallAlreadyActive": false,
      "wrapUpReason": null,
      "wrapUpItems": null,
      "callEndReason": null,
      "fromAddress": "1002",
      "callVariables": {
        "CallVariable": []
      },
      "participants": [
        {
          "actions": {
            "action": [
              "ANSWER"
            ]
          },
          "mediaAddress": "2001",
          "mediaAddressType": "SIP.js/0.21.2-CTI/Expertflow",
          "startTime": null,
          "alertingTime": "2026-08-20T06:50:34.985Z",
          "state": "ALERTING",
          "stateCause": null,
          "stateChangeTime": "2026-08-20T06:50:39.998Z",
          "mute": false
        }
      ],
      "mediaType": "audio",
      "channelType": "VOICE",
      "primaryDN": null
    }
  }
}

On that same event, construct CCM CALL_ALERTING from this dialog and POST it. body.dialog is a copy of the SIP dialog above.

How to construct CALL_ALERTING

CCM field

Extract / set from

Example

body.dialog

Copy eventPayload.response.dialog. Do not build it.

SIP dialog object

body.callId

dialog.id

hbiq7ptr2ljpm2fjf0oc

header.intent

Constant

CALL_ALERTING

id

New UUID

crypto.randomUUID()

header.channelData.channelCustomerIdentifier

dialog.customerNumber

1002

header.channelData.serviceIdentifier

dialog.serviceIdentifier

2555

header.customer

Customer API lookup by dialog.customerNumber

customer record

header.timestamp

Date.parse(agentParticipant.alertingTime). Agent participant mediaAddress = agent.attributes.agentExtension[0].

alertingTime ms

header.sender

Agent Manager login: agent.id, agent.username, type: AGENT

logged-in agent

body.type / reasonCode

Constants for inbound ringing

VOICE / INBOUND

body.leg

{dialog.id}:{agentExtension}:{customerNumber}:{alertingTimeMs}

id:ext:ani:ms

CCM JSON is a template. Replace every <source> with the live value. Keep constants as written (VOICE, INBOUND, AGENT, intent names). header.customer comes from the Customer API lookup by dialog.customerNumber (use customer.firstName, not the agent name).

JSON
{
  "id": "<crypto.randomUUID()>",
  "header": {
    "channelData": {
      "channelCustomerIdentifier": "<dialog.customerNumber>",
      "serviceIdentifier": "<dialog.serviceIdentifier>",
      "additionalAttributes": []
    },
    "customer": {
      "_id": "<customer._id>",
      "firstName": "<customer.firstName>",
      "voice": [
        "<dialog.customerNumber>"
      ],
      "isAnonymous": "<customer.isAnonymous>"
    },
    "language": {},
    "timestamp": "<Date.parse(participant.alertingTime)>",
    "securityInfo": {},
    "stamps": [],
    "intent": "CALL_ALERTING",
    "entities": {},
    "sender": {
      "id": "<agent.id>",
      "senderName": "<agent.username>",
      "type": "AGENT",
      "additionalDetail": {}
    }
  },
  "body": {
    "type": "VOICE",
    "markdownText": null,
    "reasonCode": "INBOUND",
    "leg": "<dialog.id>:<agent.extension>:<dialog.customerNumber>:<alertingTimeMs>",
    "callId": "<dialog.id>",
    "dialog": "<copy SIP dialog ALERTING above>"
  }
}
Step 2 — Agent clicks Accept

When the agent clicks Accept, send the answering command to efSwitch. That tells efSwitch to connect the ringing call and move it to the active state. Use the dialog id from the ringing event. For a voice call set answerCalltype to audio.

JavaScript
{
  "action": "answerCall",
  "parameter": {
    "dialogId": "hbiq7ptr2ljpm2fjf0oc",
    "answerCalltype": "audio",
    "clientCallbackFunction": eventCallback
  }
}

This is the same command Agent Desk sends to the wrapper. After efSwitch answers, it sends back the active dialog (step 3).

Step 3 — SIP ACTIVE dialog, then POST CCM

efSwitch then sends an active event (dialogState, state ACTIVE). The call is now connected. Build CCM CALL_LEG_STARTED from this dialog (same id as ringing) and POST it.

JSON
{
  "event": "dialogState",
  "response": {
    "loginId": "2001",
    "dialog": {
      "id": "hbiq7ptr2ljpm2fjf0oc",
      "ani": "1002",
      "customerNumber": "1002",
      "associatedDialogUri": null,
      "callbackNumber": null,
      "outboundClassification": null,
      "scheduledCallbackInfo": null,
      "isCallEnded": 0,
      "eventType": "PUT",
      "callType": "OTHER_IN",
      "queueName": "voice regression",
      "queueType": "NAME",
      "dialedNumber": "2555",
      "dnis": "2555",
      "serviceIdentifier": "2555",
      "secondaryId": null,
      "state": "ACTIVE",
      "isCallAlreadyActive": false,
      "wrapUpReason": null,
      "wrapUpItems": null,
      "callEndReason": null,
      "fromAddress": "1002",
      "callVariables": {
        "CallVariable": []
      },
      "participants": [
        {
          "actions": {
            "action": [
              "ANSWER"
            ]
          },
          "mediaAddress": "2001",
          "mediaAddressType": "SIP.js/0.21.2-CTI/Expertflow",
          "startTime": "2026-08-20T06:50:39.998Z",
          "alertingTime": "2026-08-20T06:50:34.985Z",
          "state": "ACTIVE",
          "stateCause": null,
          "stateChangeTime": "2026-08-20T06:50:39.998Z",
          "mute": false
        }
      ],
      "mediaType": "audio",
      "channelType": "VOICE",
      "primaryDN": null
    }
  }
}

How to construct CALL_LEG_STARTED — same envelope as alerting; change the fields below.

CCM field

Extract / set from

Example

body.dialog

Copy the ACTIVE SIP dialog (same id)

state = ACTIVE

header.intent

Constant

CALL_LEG_STARTED

header.timestamp

Date.parse(agentParticipant.startTime)

startTime

body.leg

Reuse the alerting leg

same as step 1

everything else

Same extraction as CALL_ALERTING from this ACTIVE dialog

customerNumber, sender

JSON
{
  "id": "<crypto.randomUUID()>",
  "header": {
    "channelData": {
      "channelCustomerIdentifier": "<dialog.customerNumber>",
      "serviceIdentifier": "<dialog.serviceIdentifier>",
      "additionalAttributes": []
    },
    "customer": {
      "_id": "<customer._id>",
      "firstName": "<customer.firstName>",
      "voice": [
        "<dialog.customerNumber>"
      ],
      "isAnonymous": "<customer.isAnonymous>"
    },
    "language": {},
    "timestamp": "<Date.parse(participant.startTime)>",
    "securityInfo": {},
    "stamps": [],
    "intent": "CALL_LEG_STARTED",
    "entities": {},
    "sender": {
      "id": "<agent.id>",
      "senderName": "<agent.username>",
      "type": "AGENT",
      "additionalDetail": {}
    }
  },
  "body": {
    "type": "VOICE",
    "markdownText": null,
    "reasonCode": "INBOUND",
    "leg": "<same leg as CALL_ALERTING>",
    "callId": "<dialog.id>",
    "dialog": "<copy SIP dialog ACTIVE above>"
  }
}
Step 4 — Conversation UI

CCM sets the voice task to STARTED. Agent Manager emits taskRequest. The desk emits topicSubscription and uses onTopicData to open the conversation. Call control stays on SIP dialogState.

Step 5 — SIP DROPPED dialog, then POST CCM

When the call ends, efSwitch sends a dropped event (dialogState, state DROPPED). Build CCM CALL_LEG_ENDED from that dialog and POST it.

JSON
{
  "event": "dialogState",
  "response": {
    "loginId": "2001",
    "dialog": {
      "id": "hbiq7ptr2ljpm2fjf0oc",
      "ani": "1002",
      "customerNumber": "1002",
      "associatedDialogUri": null,
      "callbackNumber": null,
      "outboundClassification": null,
      "scheduledCallbackInfo": null,
      "isCallEnded": 1,
      "eventType": "PUT",
      "callType": "OTHER_IN",
      "queueName": "voice regression",
      "queueType": "NAME",
      "dialedNumber": "2555",
      "dnis": "2555",
      "serviceIdentifier": "2555",
      "secondaryId": null,
      "state": "DROPPED",
      "isCallAlreadyActive": false,
      "wrapUpReason": null,
      "wrapUpItems": null,
      "callEndReason": "AGENT-HANDLED",
      "fromAddress": "1002",
      "callVariables": {
        "CallVariable": []
      },
      "participants": [
        {
          "actions": {
            "action": [
              "ANSWER"
            ]
          },
          "mediaAddress": "2001",
          "mediaAddressType": "SIP.js/0.21.2-CTI/Expertflow",
          "startTime": "2026-08-20T06:50:39.998Z",
          "alertingTime": "2026-08-20T06:50:34.985Z",
          "state": "DROPPED",
          "stateCause": null,
          "stateChangeTime": "2026-08-20T06:50:45.022Z",
          "mute": false
        }
      ],
      "mediaType": "audio",
      "channelType": "VOICE",
      "primaryDN": null
    }
  }
}

How to construct CALL_LEG_ENDED

CCM field

Extract / set from

Example

body.dialog

Copy the DROPPED SIP dialog

state = DROPPED

header.intent

Constant

CALL_LEG_ENDED

header.timestamp

Date.parse(agentParticipant.stateChangeTime)

stateChangeTime

header.conversationId

From taskRequest.conversationId after accept

conversation id

body.reasonCode

Normal inbound hangup

DIALOG_ENDED

body.endingReason

(dialog.callEndReason || "").toUpperCase()

AGENT-HANDLED

JSON
{
  "id": "<crypto.randomUUID()>",
  "header": {
    "channelData": {
      "channelCustomerIdentifier": "<dialog.customerNumber>",
      "serviceIdentifier": "<dialog.serviceIdentifier>",
      "additionalAttributes": [
        {
          "key": "conversationId",
          "type": "String2000",
          "value": "<task.conversationId>"
        }
      ]
    },
    "customer": {
      "_id": "<customer._id>",
      "firstName": "<customer.firstName>",
      "voice": [
        "<dialog.customerNumber>"
      ],
      "isAnonymous": "<customer.isAnonymous>"
    },
    "language": {},
    "timestamp": "<Date.parse(participant.stateChangeTime)>",
    "securityInfo": {},
    "stamps": [],
    "intent": "CALL_LEG_ENDED",
    "entities": {},
    "sender": {
      "id": "<agent.id>",
      "senderName": "<agent.username>",
      "type": "AGENT",
      "additionalDetail": {}
    },
    "conversationId": "<task.conversationId>"
  },
  "body": {
    "type": "VOICE",
    "markdownText": null,
    "reasonCode": "DIALOG_ENDED",
    "leg": "<same leg as CALL_ALERTING>",
    "callId": "<dialog.id>",
    "dialog": "<copy SIP dialog DROPPED above>",
    "endingReason": "<dialog.callEndReason.toUpperCase()>"
  }
}

Topic subscription: After accept, when taskRequest.taskState.name is STARTED,agent desk emit topicSubscription

5. Voice (SIP) — Commands, Parameters, Payload, Events

Voice call control is done only through the SIP. The custom desk sends a command; the library returns one or more events on the callback.

How to send a command
JavaScript
function eventCallback(eventPayload) {
  // eventPayload.event  → agentInfo | newInboundCall | dialogState | ...
  // eventPayload.response → loginId, dialog, ...
}

postMessages({
  action: "COMMAND_NAME",
  parameter: { }
});

Every command that needs a callback includes clientCallbackFunction: eventCallback.


5.1 login

Register the agent phone on efSwitch so the desk can receive ringing events. Do this after Agent Manager login and after resolving tenant mediaServer.wssUrl.

The wrapper reads parameter.domain into sipconfig.uri and parameter.wssUrl into sipconfig.wss, then REGISTERs with sipconfig.agentStaticPassword and parameter.extension.

Parameters

Parameter

Type

Required

Description

loginId

string

yes

Agent username (Agent Desk sends this). Wrapper success events use the extension as loginId.

password

string

yes

Agent / SIP password as known to the desk (Agent Desk still sends it). Registration password used by the wrapper is sipConfig.agentStaticPassword.

extension

string

yes

SIP extension

domain

string

yes

Tenant subdomain / SIP domain (example: ux-controls-02). Overrides sipConfig.uri.

wssUrl

string

yes

efSwitch WSS URL from tenant mediaServer.wssUrl (example: wss://192.168.1.17:7443). Overrides sipConfig.wss.

clientCallbackFunction

function

yes

Receives CTI events (required by the wrapper; not shown in JSON captures)

Payload (business fields Agent Desk posts; add clientCallbackFunction in code)

JavaScript
{
  "action": "login",
  "parameter": {
    "loginId": "raza",
    "password": "raza",
    "extension": "10001",
    "domain": "ux-controls-02",
    "wssUrl": "wss://192.168.1.17:7443",
    "clientCallbackFunction": eventCallback
  }
}

Response events

  • Success: agentInfo with state: "LOGIN" (loginId and extension are the SIP extension), then dialogState with dialog: null

  • Failure: Error with type subscriptionFailed or invalidState


5.2 makeCall

Places an internal / extension-to-extension call. callType can be audio, video, or screenshare.

Parameters

Parameter

Type

Required

Description

callType

string

yes

audio / video / screenshare

calledNumber

string

yes

Destination extension

Destination_Number

string

yes

Destination / service identifier

clientCallbackFunction

function

yes

Callback

Payload

JavaScript
{
  "action": "makeCall",
  "parameter": {
    "callType": "audio",
    "Destination_Number": "1777",
    "calledNumber": "1777",
    "clientCallbackFunction": eventCallback
  }
}

Response events: outboundDialing (INITIATING, INITIATED) → dialogState (ACTIVE)


5.3 makeOBCall

Places a manual outbound (PSTN) call. callType is audio.

Set Voice MRD to NOT_READY before this command.

calledNumber is the number the agent dialed. Destination_Number is the tenant default outbound channel service identifier (DID), not the customer number.

Parameters

Parameter

Type

Required

Description

callType

string

yes

audio

calledNumber

string

yes

Customer / destination number

Destination_Number

string

yes

Default outbound channel DID / service identifier

clientCallbackFunction

function

yes

Callback

Payload

JavaScript
{
  "action": "makeOBCall",
  "parameter": {
    "callType": "audio",
    "Destination_Number": "1777",
    "calledNumber": "1777",
    "clientCallbackFunction": eventCallback
  }
}

Response events: outboundDialing (INITIATING, INITIATED) → dialogState (ACTIVE when answered)


5.4 answerCall

Send the answering command to efSwitch so it connects the ringing call and puts it in the active state. This matches the Agent Desk wrapper: action: "answerCall" with the ringing dialog id and answerCalltype: "audio" (or video). efSwitch then sends dialogState with state: "ACTIVE".

Parameters

Parameter

Type

Required

Description

dialogId

string

yes

Dialog id from newInboundCall

answerCalltype

string

yes

audio / video / screenshare / onlyviewscreenshare

clientCallbackFunction

function

yes

Callback

Payload

JavaScript
{
  "action": "answerCall",
  "parameter": {
    "dialogId": "c1cc4fb6-3676-123b-ea94-005056bc90cf",
    "answerCalltype": "audio",
    "clientCallbackFunction": eventCallback
  }
}

Response events: dialogState (ACTIVE)


5.5 releaseCall

Drops / hangs up the call.

Parameters: dialogId (required)

Payload

JavaScript
{
  "action": "releaseCall",
  "parameter": {
    "dialogId": "c1cc4fb6-3676-123b-ea94-005056bc90cf"
  }
}

Response events: dialogState (DROPPED)


5.6 holdCall

Puts the active call on hold.

Parameters: dialogId, clientCallbackFunction

Payload

JavaScript
{
  "action": "holdCall",
  "parameter": {
    "dialogId": "c1cc4fb6-3676-123b-ea94-005056bc90cf",
    "clientCallbackFunction": eventCallback
  }
}

Response events: dialogState (HELD)


5.7 retrieveCall

Resumes a held call.

Parameters: dialogId, clientCallbackFunction

Payload

JavaScript
{
  "action": "retrieveCall",
  "parameter": {
    "dialogId": "c1cc4fb6-3676-123b-ea94-005056bc90cf",
    "clientCallbackFunction": eventCallback
  }
}

Response events: dialogState (ACTIVE)


5.8 logout

Unregisters the SIP extension.

Parameters

Parameter

Type

Required

Description

reasonCode

string

yes

Logout reason

userId

string

yes

Extension / user id

clientCallbackFunction

function

yes

Callback

Payload

JavaScript
{
  "action": "logout",
  "parameter": {
    "reasonCode": "Logged Out",
    "userId": "448899",
    "clientCallbackFunction": eventCallback
  }
}

Response events: agentInfo (state: "LOGOUT")


5.9 mute_call / unmute_call

Mutes or unmutes the agent microphone only. Remote (customer) audio is not muted.

Parameters: dialogId, clientCallbackFunction

Payload

JavaScript
{
  "action": "mute_call",
  "parameter": {
    "dialogId": "c1cc4fb6-3676-123b-ea94-005056bc90cf",
    "clientCallbackFunction": eventCallback
  }
}

Use "action": "unmute_call" with the same parameter object.

Response events: dialogState with participants[].mute = true or false


5.10 SST — blind transfer to an agent

Transfers the customer to another agent extension without waiting for that agent to answer.

Parameters

Parameter

Type

Required

Description

dialogId

string

yes

Active customer dialog

numberToTransfer

string

yes

Target agent extension

clientCallbackFunction

function

yes

Callback

Payload

JavaScript
{
  "action": "SST",
  "parameter": {
    "dialogId": "0334343",
    "numberToTransfer": "4488992",
    "clientCallbackFunction": eventCallback
  }
}

Response events

  • Agent A: dialogState (DROPPED)

  • Agent B: newInboundCall


5.11 SST_Queue — blind transfer to a queue

Parameters

Parameter

Type

Required

Description

dialogId

string

yes

Active customer dialog

queue

string

yes

Queue name or id

queueType

string

yes

e.g. ID

numberToTransfer

string

yes

Queue transfer DN / prefix

clientCallbackFunction

function

yes

Callback

Payload

JavaScript
{
  "action": "SST_Queue",
  "parameter": {
    "dialogId": "0334343",
    "queue": "queue_name",
    "queueType": "ID",
    "numberToTransfer": "00000000",
    "clientCallbackFunction": eventCallback
  }
}

Response events: Agent A dialogState (DROPPED); next reserved agent newInboundCall


5.12 makeConsult

Starts a consult call to another agent. The customer call is automatically held. After the consult is active, the agent can consultTransfer, conference_consult, or releaseCall on the consult dialog.

Parameters

Parameter

Type

Required

Description

numberToConsult

string

yes

Target agent extension

clientCallbackFunction

function

yes

Callback

Payload

JavaScript
{
  "action": "makeConsult",
  "parameter": {
    "numberToConsult": "4488992",
    "clientCallbackFunction": eventCallback
  }
}

Response events

  • Agent A: dialogState (HELD), consultCall (INITIATINGINITIATEDACTIVE)

  • Agent B: consultCall (ALERTING)


5.13 makeConsultQueue

Consult via queue instead of a named extension.

Parameters: numberToTransfer, queue, queueType, clientCallbackFunction

Payload

JavaScript
{
  "action": "makeConsultQueue",
  "parameter": {
    "numberToTransfer": "99887766",
    "queue": "65bb2dcbba1aab2d0a4742d6",
    "queueType": "ID",
    "clientCallbackFunction": eventCallback
  }
}

Response events: Agent A dialogState (HELD) and consultCall (INITIATINGINITIATEDACTIVE); Agent B consultCall (ALERTING)


5.14 consultTransfer

Completes attended transfer: bridges the customer to the consulted agent and drops Agent A. The other agent must already be on the consult call.

Parameters: clientCallbackFunction

Payload

JavaScript
{
  "action": "consultTransfer",
  "parameter": {
    "clientCallbackFunction": eventCallback
  }
}

Response events

  • Agent A: consultCall (DROPPED), dialogState (DROPPED)

  • Agent B: consultCall (DROPPED), customer dialogState (ACTIVE)


5.15 conference_consult

Creates a 3-way conference (customer + both agents). dialogId must be the consult dialog id, not the customer dialog.

Parameters: dialogId (consult), clientCallbackFunction

Payload

JavaScript
{
  "action": "conference_consult",
  "parameter": {
    "dialogId": "mnsghhjec8ncflpgfcv2",
    "clientCallbackFunction": eventCallback
  }
}

Response events: Both agents: consult DROPPED, customer dialogState (ACTIVE), callType CONSULT_CONFERENCE


5.16 SendDtmf

Sends DTMF digits (for example during IVR).

Parameters: dialogId, message (digit string), clientCallbackFunction

Payload

JavaScript
postMessages({
  "action": "SendDtmf",
  "parameter": {
    "dialogId": dialogId,
    "message": "1",
    "clientCallbackFunction": eventCallback
  }
});

Response events: DTMF with type: 1 (success) or type: 0 (failure)


5.17 silentMonitor (supervisor)

Supervisor listens to an agent–customer call without speaking. Set Voice MRD NOT_READY before this command.

Parameters: calledNumber, callType, Destination_Number, service_Identifier, clientCallbackFunction

Payload

JavaScript
{
  "action": "silentMonitor",
  "parameter": {
    "calledNumber": "1777",
    "callType": "audio",
    "Destination_Number": "1777",
    "service_Identifier": serviceIdentifier,
    "clientCallbackFunction": eventCallback
  }
}

Response events: outboundDialing (INITIATING, INITIATED) → dialogState (ACTIVE)


5.18 bargeIn (supervisor)

Joins a silent-monitor session as a speaking party. Requires an active silentMonitor dialog.

Parameters: dialogId, clientCallbackFunction

Payload

JavaScript
{
  "action": "bargeIn",
  "parameter": {
    "dialogId": dialogId,
    "clientCallbackFunction": eventCallback
  }
}

Response events: dialogState (DROPPED) then dialogState (ACTIVE) with callType BARGE_CONFERENCE


5.19 Voice events (callback response)

All events are delivered to clientCallbackFunction in this shape:

JSON
{
  "event": "EVENT_NAME",
  "response": {
    "loginId": "448899",
    "dialog": { }
  }
}

Use both dialog.state and participants[].mute to decide UI.

Event

When it is sent

What to read

Desk action

agentInfo

SIP login / logout

response.state = LOGIN or LOGOUT

On LOGIN, set Voice MRD READY

newInboundCall

Inbound ringing

dialog.id, fromAddress, customerNumber, state: ALERTING

Show Accept / Reject; call answerCall to accept

outboundDialing

Agent started a call

dialog.state INITIATING / INITIATED

Show dialing UI

dialogState

Any dialog change

dialog.state, participants[].mute, callEndReason

Update toolbar (hold, drop, mute)

consultCall

Consult leg change

INITIATING / INITIATED / ALERTING / ACTIVE / DROPPED

Consult UI

DTMF

Digit send result

type 1 success, 0 failure

Toast

xmppEvent

Transport up / down

IN_SERVICE / OUT_OF_SERVICE

Block controls if out of service

Error

Command failed

type: subscriptionFailed, generalError, invalidState

Show error

Dialog states: INITIATINGINITIATEDALERTINGACTIVEHELDACTIVEDROPPED

RONA / customer abandon while ringing: there is no separate RONA event name. The desk receives dialogState with state: "Canceled" and callEndReason: "Canceled". Clear the ringing UI. Do not call answerCall.

callType values: OTHER_IN, OUT, CONSULT, EXTERNAL-CONSULT, CONSULT_TRANSFER, CONSULT_CONFERENCE, BARGE_CONFERENCE

agentInfo sample

JSON
{
  "event": "agentInfo",
  "response": {
    "loginId": "448899",
    "extension": "448899",
    "state": "LOGIN",
    "cause": null
  }
}

Full newInboundCall (ALERTING) and dialogState (ACTIVE / DROPPED) dialogs are in Inbound Voice steps 1, 3, and 5.

Error sample

JSON
{
  "event": "Error",
  "response": {
    "type": "subscriptionFailed",
    "loginId": "Malik 3",
    "description": "Invalid Username or Password"
  }
}

Possible Error.type values: subscriptionFailed, generalError, invalidState.

6. Emit CIM Events

There are two types of CIM Events that the Agent Desk must handle - Messages and System Events.

Exchange Messages in a Conversation

Whenever a message is sent by the agent or system activity is generated, the custom Agent Desk emits the event publishCimEvent:

  • On accepting the request, the Agent Manager emits the event onCimEvent.

  • Multiple types of messages should be supported by Agent Desk. The types of messages are described in CIM Messages.

Socket emit envelope (required — do not emit the CIM object alone):

JavaScript
socket.emit("publishCimEvent", {
  cimEvent,          // full CimEvent object
  agentId: currentAgent.id,
  conversationId,
  roomInfo           // from taskRequest / onTopicData — required
});

For a plain agent reply, set message body type PLAIN, Base64-encode the text in body.markdownText, and set header.additionalData.isEncoded: true. Attach the active channelSession from onTopicData onto the message header (Agent Desk moves it to cimEvent.channelSession before publish).

Receiving: on onCimEvent, Agent Manager often sends cimEvent as a JSON string. Parse with typeof payload.cimEvent === "string" ? JSON.parse(payload.cimEvent) : payload.cimEvent before reading message fields.

Reply to a Specific Message

In case of a reply to any message in the conversation, the object data contains the ID of the previous message you are replying to:

// publishCimEvent - Reply
"data": {
  "id": "142ad3a6-6ab8-4cf7-9553-959e2c3605c2",
  "header": { ... },
  "body": { ... }
}
Edit a Specific Message

In case of editing any message in the conversation, the object header in data contains the intent: "UPDATE", and the ID of the original message being edited:

// publishCimEvent - Edit
"data": {
  "id": "142ad3a6-6ab8-4cf7-9553-959e2c3605c2",
  "header": {
    "intent": "UPDATE",
    "originalMessageId": "ORIGINAL_MESSAGE_ID"
  },
  "body": { ... }
}
Handle System Events

Whenever a system activity takes place, the custom Agent Desk emits publishCimEvent:

7. Reroute Chat Requests to Queues or a Specific Agent

An agent can request to reroute chats in three scenarios:

  1. Conference Request: Whenever an agent wants to add another agent to the active conversation.

  2. Transfer Request: Whenever an agent wants to transfer the conversation to another agent.

  3. Consult Request: Whenever an agent wants consultation from another agent without notifying the customer.

Voice transfer / consult / conference use efSwitch JS commands in section 5 (SST, makeConsult, consultTransfer, conference_consult), not these chat Socket.​IO events.

Conference Request

When an agent selects a queue or agent to make a conference request, custom Agent Desk emits 'directConferenceRequest':

  • The Routing Engine offers the request to an available agent. Once accepted, the new agent becomes a participant.

  • If not accepted and RONA occurs, Routing Engine continues looking for available agents until TTL expires.

Transfer Request

When an agent selects a queue or agent to transfer the conversation, custom Agent Desk emits 'directTransferRequest':

  • After placing the request, the current agent leaves the conversation upon acceptance by the target agent.

  • If RONA occurs, Routing Engine continues routing until TTL expires.

Consult Request

When an agent requests consultation without notifying the customer, custom Agent Desk emits consultRequest:

  • Once the consulted agent joins, both agents communicate in Whisper Message, invisible to the customer.

  • The agent can transfer or conference the conversation to the consulted agent using the offerToAgent: false flag.

8. Silent Monitoring & Barge In (Chat)

Silent monitoring and barge-in for push-mode chats are supervisor Socket.​IO events:

  • Prerequisites: Supervisors must have one or more assigned teams, and conversations must be active push-mode chats.

  • Silent Monitor: Emits ‘JoinAsSilentMonitor'. Agent Manager subscribes the supervisor via ‘onTopicData.. The supervisor can send Whisper Message.

  • Barge In: Emits ‘JoinAsBargin’. Agent Manager emits onCimEvent updating the supervisor role to BargIn, enabling them to send all types of CIM messages directly to the customer.

Voice silent monitor and barge-in use efSwitch JS silentMonitor and bargeIn in section 5.

9. Hand Raise

When an agent needs assistance during an ongoing conversation, clicking the "Hand Raise" icon causes Agent Desk to emit publishCimEvent with the HAND_RAISED notification payload:

JSON
{
  "id": "",
  "name": "HAND_RAISED",
  "type": "NOTIFICATION",
  "timestamp": 1709709884545,
  "conversationId": "CONVERSATION_ID",
  "eventEmitter": {},
  "channelSession": {},
  "data": {
    "agentId": "AGENT_ID",
    "userName": "USERNAME"
  },
  "roomInfo": {}
}

A supervisor can click the hand raise notification in the dashboard to join the conversation in whisper mode via onTopicData.

10. Leave Conversation

To leave any joined conversation, emit Socket.IO topicUnsubscription (event name has no trailing underscore).

Required payload:

JavaScript
socket.emit("topicUnsubscription", {
  roomInfo,            // from the joined conversation — required by Agent Manager
  conversationId,
  agentId: currentAgent.id
});
  • In response, Agent Manager closes the agent’s task for this conversation and emits topicUnsubscription back to Agent Desk (typically with statusCode: 200). Keep roomInfo from accept / onTopicData; without it the leave is ignored and the agent stays on the conversation.

For an active voice call, also send efSwitch JS releaseCall (or logout on agent logout) so the SIP dialog is cleared.