CX Voice Recording Solution — Complete Architecture Guide
Document Type: Comprehensive technical reference for the CX Voice system integrated with Cisco CUCM/UCCE/CCX for call recording.
Scope: End-to-end flows, all components, all code paths.
Based on: Confluence CX Voice — System Overview Guide + actual source code from production repositories.
---
Table of Contents
-
17.15 Port Utilisation
---
1. Executive Summary
The CX Voice Recording Solution is a comprehensive contact-center voice platform that integrates with Cisco Unified Communications Manager (CUCM) via JTAPI to capture call events, correlate them with ExpertFlow's conversation data, and produce retrievable call recordings. The system supports both Cisco UCCE and Cisco CCX deployments.
Two Recording Backends
|
Backend |
Technology |
Use Case |
|---|---|---|
|
EFSwitch |
FreeSWITCH-based media server with FusionPBX |
On-premise, file-based WAV recordings |
|
Eleveo |
Third-party recording platform (Cisco-BiB) |
Enterprise deployments with advanced QM |
High-Level Flow
Customer Phone → CUCM → FreeSWITCH → Voice Connector → CCM → Routing Engine → Agent Desktop
↓
SIPREC / BiB Recording
↓
EFSwitch / Eleveo Storage
↓
Cisco Connector (JTAPI Events)
↓
Recording Middleware → Recording Link Activities → CCM Activities
---
2. System Architecture Overview
┌─────────────────┐ SIP/RTP ┌──────────────────┐ SIP/RTP ┌─────────────────┐
│ CUSTOMER │◄────────────────►│ FREE SWITCH │◄────────────────►│ AGENT DESKTOP │
│ (Phone/App) │ (A-leg) │ (Media Server) │ (B-leg) │ (Browser/Angular│
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
│ ESL (Event Socket Library)
▼
┌──────────────────┐
│ Voice Connector │
│ (Java/Spring) │
└────────┬─────────┘
│ HTTPS REST
▼
┌──────────────────┐
│ CCM │
│ (Conversation │
│ Manager) │
└────────┬─────────┘
│ REST API
▼
┌──────────────────┐
│ Routing Engine │
│ (Task/Queue/ │
│ Agent Mgmt) │
└────────┬─────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│Cisco Connector│ │ Recording │ │ JMS/XMPP │
│ (JTAPI Events)│ │ Middleware │ │ Topics │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────────────┐
│ CUCM/ │ │ Recording Link │
│ UCCE/CCX │ │ Activities │
│ (TCD/ │ │ (Pushes links to │
│ CCD DB) │ │ CCM Activities) │
└─────────────┘ └──────────────────────┘
---
3. Component Definitions
|
Component |
Technology |
Role |
|---|---|---|
|
FreeSWITCH |
C + Lua scripts |
Media server. Handles SIP/RTP, queues calls, plays hold music, transfers, records calls |
|
Voice Connector |
Java 17 + Spring Boot |
Bridge between FreeSWITCH and CCM. Receives HTTP from FreeSWITCH Lua, sends CIM to CCM |
|
CCM |
Java + Spring Boot |
Conversation Manager. Routes CIM events, manages conversations and channel sessions |
|
Routing Engine |
Java + Spring Boot |
Task/queue/agent management. Assigns agents, handles state changes |
|
Agent Desktop |
Angular + TypeScript + SIP.js |
Browser app. Handles SIP signaling, UI, CRM integration |
|
Cisco Connector |
Java 17 + Spring Boot |
Receives JTAPI events from CUCM, correlates with Cisco TCD/CCD records, sends CIM to CCM |
|
JTAPI Connector |
Java + Spring Boot |
Legacy JTAPI event listener (used by some recording flows) |
|
Recording Middleware |
Java + Spring Boot |
Serves recording files via REST API (EFSwitch or Eleveo backend) |
|
Recording Link Activities |
Java + Spring Boot |
Pushes recording URLs to CCM as third-party activities |
|
FreeSWITCH Lua Scripts |
Lua |
|
---
4. Cisco Integration Layer
4.1 JTAPI (Java Telephony API)
The Cisco Connector and JTAPI Connector use Cisco's JTAPI to listen to call events from CUCM (Cisco Unified Communications Manager).
Connection String:
```
CUCM_IP;login=CUCM_APPLICATION_USER;passwd=CUCM_APPLICATION_USER_PASSWORD
```
Key Classes:
- JtapiConnector — Singleton that establishes the JTAPI provider connection to CUCM
- JtapiListenerService / JtapiCallObserver — Implements CallObserver and CallControlCallObserver to receive call events
4.2 JTAPI Events Captured
|
Event |
ID |
Handler |
Description |
|---|---|---|---|
|
|
Connected |
|
A connection is established |
|
|
Held |
|
Agent puts call on hold |
|
|
Talking |
|
Agent resumes call |
|
|
Transfer Start |
|
Call transfer initiated |
|
|
Consult Active |
|
Consult call created |
|
|
Conference Start |
|
Conference begins |
|
|
Conference End |
|
Conference ends |
|
|
Disconnected |
|
Call disconnected |
|
|
Observation Ended |
|
JTAPI stops observing call |
4.3 Event Persistence
JTAPI events are immediately persisted to a local database table (jtapi_event) with the following fields:
JtapiEvent event = JtapiEvent.builder()
.eventType(type) // CALL_CONNECTED, CALL_HELD, etc.
.globalCallId(globalCallId) // Cisco Global Call ID (GCID)
.jtapiId(jtapiId) // Cisco JTAPI Call ID
.connectionId(connectionId) // Connection ID (xrefciId)
.previousGlobalCallId(...) // For consult calls
.callingTerminal(callingTerm)
.calledTerminal(calledTerm)
.callingExtension(callingExt)
.calledExtension(calledExt)
.callDirection(INBOUND/OUTBOUND/INTERNAL)
.build();
4.4 HA (High Availability) with Consul
When JTAPI_HA_MODE=true, the JTAPI listener uses Consul distributed locks to ensure only one instance processes events for a given call:
boolean acquireLock(Consul consul, String callId) {
// Creates Consul session and acquires KV lock per callId
}
---
5. Complete Normal Inbound Call Flow
Phase 1: Customer Dials Service Number
Customer dials 8001
│
▼
FreeSWITCH receives SIP INVITE on A-leg
│
▼
FreeSWITCH dialplan matches destination_number 8001
│
▼
FreeSWITCH executes: lua vcApi.lua
│ (no argv[1] → inbound IVR branch)
▼
vcApi.lua:
1. Answers the call (session:answer())
2. Gets serviceIdentifier = destination_number
3. Gets queue info from config (cx_env.lua)
4. Sets sip_h_X-queue, sip_h_X-queueType
5. Sets sip_h_X-Destination-Number = serviceIdentifier
6. Sets session_in_hangup_hook = true
7. Sets api_hangup_hook = "lua cx_hangup.lua"
8. Calls request_agent() → POST to Voice Connector /request-agent
Request body sent to Voice Connector:
```json
{
"callingNumber": "+18005550199",
"queue": "Sales",
"queueType": "NAME",
"callSipId": "abc-123-call-id",
"callUid": "uuid-of-freeswitch-call",
"eslHost": "192.168.1.161",
"serviceIdentifier": "8001",
"direction": "INBOUND",
"priority": ""
}
```
Phase 2: Voice Connector Requests Agent from CCM
Voice Connector receives POST /request-agent
│
▼
InboundController.sendAgentRequest() → VcService.requestAgent()
│
▼
VcService.createAgentRequestPayload():
• intent = "ASSIGN_RESOURCE_REQUESTED"
• direction = "INBOUND"
• mode = "QUEUE"
• resource = { type: "NAME", value: "Sales" }
• metadata = { uuid, eslHost }
│
▼
VcService.sendPostRequestCcm() → POST CCM /ccm/message/receive
CIM Message sent to CCM:
```json
{
"header": {
"intent": "ASSIGN_RESOURCE_REQUESTED",
"channelData": {
"channelCustomerIdentifier": "+18005550199",
"serviceIdentifier": "8001"
},
"sender": { "type": "CONNECTOR", "senderName": "CX-Voice-Connector" }
},
"body": {
"type": "ASSIGN_RESOURCE_REQUESTED",
"additionalDetail": {
"callId": "abc-123-call-id",
"direction": "INBOUND",
"mode": "QUEUE",
"resource": { "type": "NAME", "value": "Sales" },
"metadata": { "uuid": "call-uuid", "eslHost": "192.168.1.161" }
}
}
}
```
Phase 3: CCM + Routing Engine Queue and Reserve Agent
CCM receives ASSIGN_RESOURCE_REQUESTED
│
▼
CCM creates conversation + channel session
│
▼
CCM → Routing Engine: AssignResourceRequest
│
▼
Routing Engine:
1. TaskManager.enqueueTask()
• Creates TaskMedia (state = QUEUED)
• Creates Task (conversationId, media)
• Inserts into TasksRepository
• Publishes TaskStateChanged event
2. PrecisionQueuesPool.publishNewRequest()
• Adds task to queue
3. AssignAgentService.assignAgent()
• Finds best available agent
• agent.reserveTask(task, media)
• TaskMedia state = RESERVED
• Agent MRD state = RESERVED
• Publishes AGENT_RESERVED event
Phase 4: CCM Notifies Voice Connector → FreeSWITCH Calls Agent
CCM receives AGENT_RESERVED from Routing Engine
│
▼
CCM sends notification to Voice Connector
• NotificationType = AGENT_RESERVED
• Contains: agent extension, call UUID, queue name
│
▼
Voice Connector receives POST /ccm-msg/receive/cim-messages
│
▼
OutboundController.receiveCimMessage():
• Extracts agentExtension from AgentReservedDto
• Extracts call UUID from task metadata
│
▼
VcService.routeAgent(direction="INBOUND"):
• Calls handleAgentForInbound()
│
▼
handleAgentForInbound():
1. Connects to FreeSWITCH via ESL (ESL inbound client)
2. ESL: uuid_setvar_multi <uuid> sip_h_X-queueType=NAME;sip_h_X-queue='Sales'
3. ESL: uuid_transfer <uuid> <agent_extension> XML <domain>
• This bridges the customer call (A-leg) to agent's SIP extension
│
▼
FreeSWITCH sends SIP INVITE to Agent Desktop (B-leg)
Phase 5: Agent Desktop Receives and Displays Call
SIP INVITE arrives at Agent Desktop via WebSocket
│
▼
sip-wrapper.js onInvite handler:
1. Creates dialog from dialogStatedata template
2. Extracts headers from INVITE:
• X-Call-Id → dialog.id
• X-Customernumber → dialog.customerNumber
• X-Queue → dialog.queueName
• X-Calltype → dialog.callType ("INBOUND")
• X-Destination-Number → dialog.serviceIdentifier
• X-Call-Variable0..9 → dialog.callVariables
3. Sets dialog.state = "ALERTING"
4. Sets participants[0].state = "ALERTING"
5. Sets participants[0].actions.action = ["ANSWER"]
6. Sets event = "newInboundCall"
7. callback(invitedata) → Angular sip.service.ts
8. SendPostMessage(invitedata) → parent window (if iframe)
9. Stores session reference: invitedata.session = invitation
10. calls.push(invitedata)
11. addsipcallback(invitation, 'inbound', callback)
• Attaches onCancel, onBye, stateChange listeners
Dialog object after onInvite:
```json
{
"event": "newInboundCall",
"response": {
"loginId": "1005",
"dialog": {
"id": "abc-123-call-id",
"state": "ALERTING",
"callType": "INBOUND",
"customerNumber": "+18005550199",
"serviceIdentifier": "8001",
"queueName": "Sales",
"participants": [{
"mediaAddress": "1005",
"state": "ALERTING",
"actions": { "action": ["ANSWER"] },
"startTime": null
}],
"callVariables": { "CallVariable": [...] }
}
},
"session": <SIP.js Invitation object>
}
```
Angular UI receives callback → sip.service.ts handleNewInboundCallEvent():
1. Sets callChannelType = "cx_voice"
2. Calls handleInboundAndCampaignCallEvent(event, "INBOUND")
• Creates customer object
• Identifies customer by phone number
• Shows incoming call notification
3. Sends CALL_ALERTING to CCM via ccmChannelSessionApi()
{
"header": { "intent": "CALL_ALERTING" },
"body": {
"type": "VOICE",
"callId": "abc-123-call-id",
"leg": "abc-123:1005:+18005550199:2026-06-01T10:00:00.000Z",
"reasonCode": "INBOUND"
}
}
Phase 6: Agent Answers
Agent clicks ANSWER button in UI
│
▼
sip.service.ts → postMessages({action: "answerCall", parameter: {dialogId, answerCalltype: "audio"}})
│
▼
sip-wrapper.js respond_call():
1. Finds session in calls[] by dialogId
2. Sets session delegate (inviteDelegate)
3. Configures sessionDescriptionHandlerOptions (audio constraints)
4. Calls session.accept(inviteOptions)
│
▼
SIP.js sends 200 OK response to FreeSWITCH
│
▼
Media established (RTP audio stream)
│
▼
addsipcallback stateChange listener fires:
• newState === SIP.SessionState.Established
• Sets participants[0].state = "ACTIVE"
• Sets dialog.state = "ACTIVE"
• Sets isCallEnded = 0
• Sets startTime = new Date().toISOString()
• Sets actions = ["TRANSFER_SST", "HOLD", "SEND_DTMF", "DROP"]
• callback(sessionall) → Angular
│
▼
Angular UI receives ACTIVE state
sip.service.ts handleDialogActiveEvent():
1. Checks if dialogCache exists (was ALERTING)
2. Sets dialogCache to "active"
3. Sets isCallActive = true
4. Starts local/remote media streams
5. Creates CALL_LEG_STARTED CIM message
6. Sends to CCM via ccmChannelSessionApi()
{
"header": { "intent": "CALL_LEG_STARTED" },
"body": {
"type": "VOICE",
"callId": "abc-123-call-id",
"leg": "abc-123:1005:+18005550199:2026-06-01T10:00:00.000Z",
"reasonCode": "INBOUND"
}
}
At this point:
- Customer and Agent are talking (RTP audio flowing)
- UI shows "On Call" with timer, Hold, Transfer, Hangup buttons
- CCM has received: CALL_ALERTING, CALL_LEG_STARTED
- Routing Engine knows agent is BUSY
Phase 7: Call Ends (Normal Hangup)
Agent clicks Hangup OR Customer hangs up
│
▼
If Agent clicks Hangup:
sip.service.ts → postMessages({action: "releaseCall", parameter: {dialogId}})
sip-wrapper.js terminate_call(dialogId):
• Finds session in calls[]
• Calls session.bye()
• SIP BYE sent to FreeSWITCH
If Customer hangs up first:
FreeSWITCH sends SIP BYE to Agent Desktop
│
▼
sip-wrapper.js onBye handler:
• Extracts X-Call-Dropped-Custom-Reason header
• Or extracts text="NORMAL_CLEARING" from BYE body
• Sets callEndReason = "NORMAL_CLEARING"
│
▼
addsipcallback stateChange → Terminated:
• state = DROPPED
• participants[0].state = DROPPED
• If callEndReason is null → "AGENT-HANDLED"
• callback(sessionall) → Angular
• calls.splice(index, 1) → removes from array
│
▼
Angular UI receives DROPPED state
```
sip.service.ts handleDialogDroppedEvent():
1. Removes notification
2. Determines callType = "DIALOG_ENDED"
3. handleWhenEndReasonIsNotCustomerConferenceLeft()
→ handleReasonOtherThanNoAnswerOrOriginatorCancel()
→ handleCallDroppedEvent()
4. Creates CALL_LEG_ENDED CIM message
5. Sends to CCM via ccmChannelSessionApi()
6. Clears local dialog cache
{
"header": { "intent": "CALL_LEG_ENDED" },
"body": {
"type": "VOICE",
"callId": "abc-123-call-id",
"leg": "abc-123:1005:+18005550199:2026-06-01T10:00:00.000Z",
"reasonCode": "NORMAL_CLEARING"
}
}
Phase 8: CCM Closes Conversation
CCM receives CALL_LEG_ENDED
│
▼
CallLegEndedEvent.handle():
• Returns RoomEventResponse.POST_CONTROLLER_INTENT
│
▼
CCM ActivityService:
• Updates channel session state
• If all channel sessions ended:
• Closes conversation
• Publishes CHANNEL_SESSION_ENDED
│
▼
Routing Engine:
• TaskManager.changeStateOnMediaClose()
• Agent MRD state: BUSY → READY (if no other tasks)
• Publishes AGENT_STATE_CHANGED
---
6. Complete Normal Outbound Call Flow
Phase 1: Agent Initiates Outbound Call
Agent opens dialpad in Agent Desktop UI
│
▼
Agent enters customer phone number (e.g., +18005550199)
│
▼
Agent clicks Call button
│
▼
Angular component → sip.service.ts makeCallOnSip(customer, number)
│
▼
sip.service.ts:
1. Sets isOBCallRequested = true
2. Calls makeCXVoiceMrdNotReady(customer)
• Emits changeAgentState → MRD = NOT_READY
3. Waits 700ms (setTimeout)
4. Checks if CX Voice MRD is now NOT_READY
• If yes: sends command to sip-wrapper.js
• If no: shows error, resets state
Why NOT_READY first? The agent's CX Voice MRD must be NOT_READY before making an outbound call. This prevents the Routing Engine from offering an inbound call while the agent is dialing out.
Phase 2: sip-wrapper.js Initiates SIP Call
sip.service.ts sends postMessages({action: "makeOBCall", parameter: {...}})
│
▼
sip-wrapper.js receives makeOBCall command
│
▼
sip-wrapper.js initiate_call(calledNumber, DN, mediaType, callback, "MANUAL_OUT", serviceIdentifier):
1. Validates parameters
2. Creates SIP URI: sip:+18005550199@expertflow
3. Creates new SIP.Inviter(userAgent, sip_uri)
4. Sets SIP headers on INVITE request:
• X-Destination-Number: <serviceIdentifier> (e.g., 8001)
• X-Calltype: OUT
5. Configures inviteOptions with requestDelegate
6. Calls sessionall.invite(inviteOptions)
│
▼
SIP.js sends SIP INVITE to FreeSWITCH
SIP INVITE sent by Agent Desktop:
```
INVITE sip:+18005550199@expertflow SIP/2.0
Via: SIP/2.0/WSS agent-desktop
From: <sip:1005@expertflow>
To: <sip:+18005550199@expertflow>
X-Destination-Number: 8001
X-Calltype: OUT
```
Phase 3: FreeSWITCH Receives and Processes Outbound INVITE
FreeSWITCH receives SIP INVITE from Agent Desktop
│
▼
FreeSWITCH dialplan processes the INVITE
│
▼
FreeSWITCH originates call to customer's phone number
│ (via SIP trunk / PSTN gateway)
▼
Customer's phone starts ringing
Phase 4: Call State Progression in sip-wrapper.js
|
SIP Response |
dialog.state |
Meaning |
|---|---|---|
|
onTrying |
INITIATING |
FreeSWITCH is processing the call |
|
onProgress |
INITIATED |
Customer's phone is ringing |
|
onAccept |
ACTIVE |
Customer answered, talking |
Phase 5: Agent Desktop Handles Call States
Event "outboundDialing" with state = "INITIATING":
- sip.service.ts handleOutboundDialingEvent()
- Calls identifyCustomer()
- Shows "Calling..." UI
Event "outboundDialing" with state = "INITIATED":
- Shows "Ringing..." UI
Event "dialogState" with state = "ACTIVE":
- sip.service.ts handleCallActiveEvent()
- For OUTBOUND calls, calls getDefaultOutBoundChannel()
- Fetches default outbound channel from backend API
- Gets serviceIdentifier for outbound channel
- Creates CALL_LEG_STARTED CIM message
- Sends to CCM via ccmChannelSessionApi()
- Sets isCallActive = true
- Shows "On Call" UI with timer
{
"header": { "intent": "CALL_LEG_STARTED" },
"body": {
"type": "VOICE",
"callId": "outbound-call-id",
"leg": "outbound-id:1005:+18005550199:2026-06-01T10:00:00.000Z",
"reasonCode": "OUTBOUND"
}
}
Phase 6: Call Active
Agent and Customer are talking (RTP audio)
- UI shows: Call timer counting, Buttons: Hold, Transfer, Hangup, Customer info (if identified)
Phase 7: Call Ends
Same as inbound — agent or customer hangs up → CALL_LEG_ENDED sent to CCM with reasonCode "OUTBOUND"
Phase 8: CCM + Routing Engine Cleanup
CCM receives CALL_LEG_ENDED → closes conversation → Routing Engine updates MRD: BUSY → READY
---
7. Complete RONA Flow
RONA = Ring No Answer. The FreeSWITCH originate command times out because the agent did not answer the SIP INVITE within the configured timeout (default ~30 seconds).
The Two Parallel Paths
When RONA occurs, two independent things happen simultaneously:
Path A: Agent Desktop (SIP Path)
Path B: FreeSWITCH Backend (HTTP Path)
These paths do NOT depend on each other. They are triggered by the same FreeSWITCH originate failure.
Path A: Agent Desktop
FreeSWITCH originate fails with cause = "NO_ANSWER"
│
▼
FreeSWITCH sends SIP CANCEL to Agent Desktop
│
▼
sip-wrapper.js onCancel:
• Extracts text="NO_ANSWER" from Reason header
• Sets callEndReason = "NO_ANSWER"
│
▼
addsipcallback stateChange → Terminated
• state = DROPPED
│
▼
sip.service.ts handleDialogDroppedEvent()
→ handleWhenEndReasonIsNotCustomerConferenceLeft()
→ handleNoAnswerOROriginatorCancelEndReason()
→ checkActiveTasks(agentId, dialog, "NO_ANSWER", ...)
→ handleNoAnswerEvent()
→ endingReason == "NO_ANSWER"
→ clearCacheAndCloseControlsDialog(cacheId)
• Clears localStorage dialog cache
• Closes call popup
CRITICAL: The Agent Desktop does NOT send CALL_LEG_ENDED to CCM during RONA. It only clears its local state. The call "disappears" from the UI.
Path B: FreeSWITCH Backend
FreeSWITCH originate fails with cause = "NO_ANSWER"
│
▼
FreeSWITCH executes: lua vcApi.lua rona
│
▼
vcApi.lua (rona branch):
1. Gets originate_failed_cause = "NO_ANSWER"
2. Converts to: hup_cause = "RONA"
3. Checks voicemail_enabled flag
• If true: transfers to voicemail DN, cancels agent, returns
4. Checks transferType:
• If NAMED (not CONSULT): plays error, ends call, returns
5. Calls cancel_agent() → POST to Voice Connector /cancel-agent
with errorCode = "RONA"
6. Calls request_agent() → requeues the call
vcApi.lua rona branch:
```lua
-- CONVERT NO_ANSWER CAUSE TO RONA AS THE CX NEEDS IT
if (hup_cause == "NO_ANSWER") then
hup_cause = "RONA"
end
-- CANCEL AGENT REQUEST VIA THE VOICE CONNECTOR
cancel_agent(serviceIdentifier, direction, cx_env, customerNumber, hup_cause)
-- REQUEUEING CALL (for queue-based transfers)
request_agent(serviceIdentifier, direction, queue, queueType, priority, cx_env, customerNumber)
```
Voice Connector → CCM
Voice Connector receives POST /cancel-agent
│
▼
VcService.cancelAgent():
• Creates CIM: intent = CANCEL_RESOURCE_REQUESTED
• body.additionalDetail.reasonCode = "RONA"
• body.additionalDetail.requestType = "VOICE"
• POSTs to CCM /ccm/message/receive
CCM → Routing Engine
CCM receives CANCEL_RESOURCE_REQUESTED
│
▼
CancelResourceRequestedEvent.handle():
• Extracts direction = "INBOUND"
• Extracts reasonCode = "RONA"
• Checks requestType == "VOICE"
• Calls: restRequest.revokeVoiceRequestByDirection(convId, INBOUND, "RONA")
│
▼
Routing Engine TasksController:
• DELETE /tasks/conversation/{convId}/direction/{dir}/voice/{reasonCode}
│
▼
TasksService.revokeVoiceResourceByDirection(convId, INBOUND, "RONA")
Routing Engine Revokes Task
public void revokeVoiceResourceByDirection(String conversationId, TaskTypeDirection direction, String reasonCode) {
List<Task> tasks = find voice tasks by conversationId with direction
tasks.forEach(t -> {
TaskMedia media = t.findInProcessCxVoiceMedia(CX_VOICE_MRD_ID);
boolean agentStateToBeUpdatedToNotReady = false;
if (media != null) {
// ONLY for RONA on RESERVED media → set agent to NOT_READY
if (media.getState().equals(TaskMediaState.RESERVED)
&& reasonCode.equalsIgnoreCase("RONA")) {
agentStateToBeUpdatedToNotReady = true;
}
// Revoke the task
this.taskManager.revokeInProcessVoiceTask(t, false, media);
// After revoke, set agent state to NOT_READY (RONA only)
if (agentStateToBeUpdatedToNotReady) {
AgentStateChangeRequest request = new AgentStateChangeRequest(
t.getAssignedTo().getId(),
new AgentState(Enums.AgentStateName.NOT_READY, null)
);
agentStateService.agentState(request);
}
}
});
}
FreeSWITCH Requeues the Customer
CRITICAL: The original task is CLOSED (deleted), NOT requeued. A NEW task is created.
freeswitch.consoleLog("INFO", "REQUEUEING CALL")
session:sleep(3000);
session:setVariable("session_in_hangup_hook", "true")
session:setVariable("api_hangup_hook", "lua cx_hangup.lua")
local statusCode = tostring(request_agent(serviceIdentifier, direction, queue, queueType, priority, cx_env, customerNumber))
if (statusCode ~= "200") then
session:streamFile(cx_env.ivr_prompts_folder .. "error_try_later.wav")
session:hangup("SERVICE_UNAVAILABLE")
return
end
hold_music(cx_env) -- Customer hears hold music while waiting
---
8. Transfer, Consult & Conference Flows
8.1 Direct Transfer
Agent A (1005) is on call with Customer
│
▼
Agent A initiates transfer to Agent B (1006)
│
▼
Agent A clicks Transfer button → sip.service.ts
│
▼
Transfer flow in Angular:
• If blind transfer: sends TRANSFER_SST CIM to CCM
• If consult transfer: first creates consult call, then completes transfer
│
▼
CCM processes transfer → sends new AGENT_RESERVED to Voice Connector
│
▼
Voice Connector transfers customer call to Agent B via ESL
8.2 Consult Transfer
Agent A is on call with Customer
│
▼
Agent A clicks Consult → dials Agent B
│
▼
sip-wrapper.js initiates new INVITE to Agent B (CONSULT call)
│
▼
Agent B answers → Agent A and Agent B are talking (customer on hold)
│
▼
Agent A clicks Transfer → completes consult transfer
│
▼
Customer is now connected to Agent B
│
▼
JTAPI events captured:
• CiscoConsultCallActiveEv — consult call started
• CiscoTransferStartEv — transfer initiated
• ConnDisconnectedEv on original leg
• ConnConnectedEv on new leg
8.3 Conference
Agent A is on call with Customer
│
▼
Agent A clicks Conference → dials Agent B
│
▼
Agent B answers
│
▼
Agent A clicks Merge → Conference starts
│
▼
JTAPI events:
• CiscoConferenceStartEv
• CiscoConferenceEndEv (when conference ends)
│
▼
Cisco Connector processes conference events:
• Links all conference legs under same callId
• Creates CALL_CONF_STARTED / CALL_CONF_ENDED events
---
9. JTAPI Event Processing Deep Dive
9.1 Correlation Service
The CorrelationService (in Cisco Connector) runs a scheduled background job that:
-
Fetches unprocessed JTAPI events from the database
-
Deduplicates events (skips duplicate CALL_CONNECTED, CALL_DISCONNECTED)
-
Groups events by globalCallId
-
Validates event groups (ensures equal CONNECTED and DISCONNECTED counts)
-
Detects fake legs (first event = CALL_TRANSFERRED followed by CALL_CONNECTED)
-
Collects events of one call (links consult legs to parent legs)
-
Detects call type: SIMPLE, DIRECT_TRANSFER, CONSULT, CONSULT_TRANSFER, CONFERENCE
-
Builds CallLeg objects with start/end times, reasons, and durations
-
Saves legs and histories to database
9.2 Call Type Detection
CallType detectCallType(List<JtapiEvent> events) {
// Conference
if (hasConfStart && hasConfEnd && hasConsult) return CallType.CONFERENCE;
// Consult Transfer
if (hasConsult && consultStartAt != null) {
for (JtapiEvent e : events) {
if (e.getEventType() == CALL_TRANSFERRED && e.getCreatedAt().isAfter(consultStartAt))
return CallType.CONSULT_TRANSFER;
}
}
// Plain Consult
if (hasConsult) return CallType.CONSULT;
// Direct Transfer vs Simple
if (connectedConnIds.size() >= 2) return CallType.DIRECT_TRANSFER;
return CallType.SIMPLE;
}
```
9.3 Building Call Legs
CallLeg buildLeg(String callId, JtapiEvent start, JtapiEvent end,
LegStartReason startReason, LegEndReason endReason) {
String agentExtension;
if (startReason == INBOUND || startReason == CONSULT ||
startReason == CONSULT_TRANSFER || startReason == CONSULT_CONFERENCE) {
agentExtension = start.getCalledExtension();
} else {
agentExtension = start.getCallingExtension();
}
CallLeg leg = new CallLeg();
leg.setCallId(callId);
leg.setGlobalCallId(start.getGlobalCallId());
leg.setConnectionId(start.getConnectionId());
leg.setJtapiId(start.getJtapiId());
leg.setAgentExtension(agentExtension);
leg.setStartTime(start.getCreatedAt());
leg.setEndTime(end.getCreatedAt());
leg.setDuration(Duration.between(start.getCreatedAt(), end.getCreatedAt()).toMillis());
leg.setStartReason(startReason);
leg.setEndReason(endReason);
return leg;
}
```
9.4 Cisco Record Correlation
The ConversationService runs a scheduled job that:
-
Fetches unsynced CallLegs
-
Groups legs by callId
-
Fetches Cisco TCD (Termination Call Detail) or CCD (Contact Call Detail) records from Cisco database
-
Matches Cisco records to CallLegs by closest end-time
-
Enriches CallLegs with:
-
serviceIdentifier -
customerIdentifier -
agentName -
agentId -
wrapUps -
Builds CIM messages: CALL_LEG_STARTED, CALL_LEG_ENDED, WRAPUP
-
Sends to CCM via
/conversation-manager/activities/voice
CCE Query (Termination_Call_Detail):
```sql
SELECT
tcd.ANI,
tcd.DigitsDialed,
tcd.WrapupData AS WrapUps,
tcd.AgentSkillTargetID AS AgentID,
tcd.PeripheralCallKey AS JtapiId,
tcd.CallTerminatedDateTimeUTC AS endDateTime,
a.EnterpriseName AS AgentName,
tcd.InstrumentPortNumber AS extension,
dd.LastName as callId,
dd.CallResult as callResult,
CASE WHEN tcd.PeripheralCallType IN (7,17,18,27,28,29,30,31,32,33,34,35,36,37,41)
THEN 'OUTBOUND' ELSE 'INBOUND' END AS conversationType
FROM Termination_Call_Detail AS tcd
LEFT JOIN Agent AS a ON tcd.AgentSkillTargetID = a.SkillTargetID
LEFT JOIN Dialer_Detail AS dd ON tcd.PeripheralCallKey = dd.PeripheralCallKey
WHERE tcd.PeripheralCallKey IN (...)
```
CCX Query (ContactCallDetail):
```sql
SELECT
ccd.contactid AS JtapiId,
ccd.originatordn AS ANI,
ccd.callednumber AS DigitsDialed,
acd.callwrapupdata AS WrapUps,
COALESCE(ccd.enddatetime, cl.enddatetime, acd.enddatetime) as enddatetime,
r.resourceid AS AgentId,
r.resourcename AS AgentName,
r.extension,
dl.lastname as CallId
FROM ContactCallDetail ccd
LEFT JOIN AgentConnectionDetail acd ON acd.contactid = ccd.contactid
LEFT JOIN ConsultLegDetail cl ON cl.contactid = ccd.contactid
LEFT JOIN resource r ON r.resourceid = COALESCE(acd.resourceid, cl.destinationresourceid, ccd.originatorid)
LEFT JOIN dialingList dl ON dl.dialinglistid = acd.dialinglistid
WHERE ccd.contactid IN (...)
```
---
10. Recording Architecture
10.1 EFSwitch Recording (FreeSWITCH-based)
Recording Flow:
```
FreeSWITCH bridges A-leg (customer) and B-leg (agent)
│
▼
FreeSWITCH starts recording via:
• dialplan configuration or
• Lua script: set_recording_name.lua
│
▼
Recording is saved as WAV file:
/app/files/wav/<date>/<uuid>:<agent>:<ani>:<startTime>.wav
│
▼
Filename format:
<dialogId>:<agentExtension>:<customerNumber>:<epochSeconds>.wav
│
▼
Files are optionally encrypted with AES/CFB/NoPadding
```
set_recording_name.lua:
```lua
local callType = session:getVariable("sip_h_X-CallType")
local record_path = session:getVariable("record_path")
local uuid = session:getVariable("uuid")
local ext = session:getVariable("record_ext")
-- For consult calls
if (callType == "CONSULT") then
session:execute("stop_record_session","all")
local filename = uuid .. "." .. ext
session:setVariable("recording_filename", filename)
session:setVariable("recording_command",
"nolocal:execute_on_answer=record_session " .. record_path .. "/" .. filename)
return
-- For outbound calls
elseif (callType == "OUT") then
customer_leg_uuid = session:getVariable("customer_leg_uuid")
session:setVariable("recording_command",
"nolocal:execute_on_answer=record_session " .. record_path .. "/" .. customer_leg_uuid .. "." .. ext)
session:setVariable("recording_filename", customer_leg_uuid .. "." .. ext)
return
end
-- For inbound calls
local filename = uuid
local count = 0
while (fileExists(filename .. '.' .. ext)) do
count = count + 1
filename = uuid .. '_' .. count
end
filename = uuid .. suffix .. "." .. ext
session:setVariable("recording_filename", filename)
session:setVariable("recording_command",
"nolocal:execute_on_answer=record_session " .. record_path .. "/" .. filename)
```
10.2 Eleveo Recording (Cisco BiB)
For Eleveo deployments, recording is handled by Cisco Built-in-Bridge (BiB) on Cisco phones:
Cisco Phone (Agent)
│
▼ Built-in-Bridge forks audio
├─→ A-leg: Customer conversation
└─→ B-leg: Recording stream → Eleveo recorder
│
▼
Eleveo stores recording with metadata (JTAPI_CISCO_ID, COUPLE_START_REASON, etc.)
│
▼
Recording Middleware queries Eleveo API to retrieve files
10.3 Call Forking & Recording Architectures
Source: Confluence (4.8) Call Forking Concepts (CX space)
Call forking is the process of duplicating a single call into multiple parallel RTP streams, allowing the same audio to be routed to different destinations simultaneously. In CX Voice Recording, this is the foundational mechanism by which recordings are captured.
Three Forking Sources for CX Media Server:
|
Source |
Technology |
Description |
|---|---|---|
|
Cisco CUBE |
SIPREC |
Cisco Unified Border Element forks media via SIPREC to CX Media Server |
|
CX SIP Proxy |
SIP |
Internal SIP proxy forks media streams for recording or monitoring |
|
Cisco BiB |
Built-in-Bridge |
Phone-level forking — each agent phone duplicates its audio stream |
Forking Flow:
Incoming Call
│
▼
System duplicates the call (forks)
│
├─► A-leg: Original call → Agent
│
├─► B-leg: Recording stream → FreeSWITCH / Eleveo
│
└─► C-leg (optional): Screen capture → Screen recorder
│
▼
Parallel Processing:
• Agent continues normal conversation
• Recorder writes audio to disk
• NLU engine (optional) receives stream for transcription
SIPREC-based Recording (Drachtio + FreeSWITCH)
Source: Confluence (4.8) SIPrec-based recording using a drachtio server, and FreeSWITCH as the media server
An alternative architecture uses Drachtio (open-source SIP server framework) to receive SIPREC streams from a Cisco CUBE or SBC, then bridges them to FreeSWITCH for recording.
Drachtio Server Setup:
# Install dependencies
sudo apt install libcurl4-openssl-dev
Build drachtio-server
Configure admin port
Run as service
FreeSWITCH Dialplan for SIPREC Hairpin & Record:
<!-- /etc/freeswitch/dialplan/public.xml -->
<extension name="hairpin_and_record">
<condition field="${sip_h_X-Return-Token}" expression="^(.+)$">
<action application="export" data="sip_h_X-Return-Token=${sip_h_X-Return-Token}" />
<action application="export" data="_nolocal_jitterbuffer_msec=100"/>
<action application="set" data="RECORD_STEREO=true"/>
<action application="set" data="call_id=${strftime(%Y%m%d_%H%M%S)}_${sip_from_tag}"/>
<action application="set" data="outfile=$${base_dir}/recordings/${call_id}.wav"/>
<action application="record_session" data="${outfile}"/>
<action application="set" data="hangup_after_bridge=true"/>
<action application="bridge" data="sofia/external/${destination_number}@${network_addr}"/>
</condition>
</extension>
Important: Internal FreeSWITCH SIP profile must listen on 5065 (not 5060) because Drachtio uses 5060 for incoming SIPREC.
SIPREC Test Results Matrix:
|
Test Scenario |
Result |
Notes |
|---|---|---|
|
Normal call |
✅ Working |
Full audio captured |
|
Consult call |
❌ Not working |
Only A1↔C audio; A1↔A2 consult is silent |
|
Consult-Transfer call |
❌ Not working |
Same as consult; post-transfer also silent |
|
Direct-Transfer call |
✅ Working |
Audio continues across transfer (C↔A2) |
|
Hold/Unhold call |
⚠️ Partial |
Hold music (MOH) is recorded into file |
|
Pause/Resume recording |
✅ Working |
Hardcoded filename required |
Note: The SIPREC-based approach has known limitations with consult calls and transfers. The FreeSWITCH-native recording (via ESL/CX SIP Proxy) is the recommended production approach for full call-type coverage.
10.4 Recording Use Cases Enabled by Call Forking
|
Use Case |
Fork Destination |
Technology |
|---|---|---|
|
Voice Recording / QM |
FreeSWITCH / Eleveo |
SIPREC, BiB, or direct SIP fork |
|
Voice Biometrics & Authentication |
Voice biometric engine |
Real-time stream fork to IVR + engine |
|
Agent Assist & Transcription |
NLU/ASR engine |
Real-time transcription, translation, suggestions |
|
Screen Recording |
Screen capture server |
Agent workstation |
|
Conversational IVR |
NLU bot |
Media Server integrates ASR/NLU for IVR |
|
Sentiment Analysis |
Analytics engine |
Fork to real-time sentiment analysis service |
---
11. Recording Middleware & Link Activities
11.1 Recording Middleware
The Recording Middleware is a Spring Boot application that serves recording files via REST API.
Endpoints:
```
GET /{legId}/recording-file
→ Returns audio/wav file (EFSwitch) or merged bytes (Eleveo)
```
Backend Selection (Spring @ConditionalOnProperty):
```java
// EFSwitch backend (default)
@ConditionalOnProperty(name = "recording.backend", havingValue = "EFSWITCH", matchIfMissing = true)
public class EfswitchRecordingService implements RecordingServiceInterface { ... }
// Eleveo backend
@ConditionalOnProperty(name = "recording.backend", havingValue = "ELEVEO")
public class EleveoRecordingService implements RecordingServiceInterface { ... }
```
EFSwitchRecordingService
public Resource getRecordingFile(String legId) {
// legId format: dialogId:agent:ani:epochTime
String[] legIdParts = splitOnLastThreeColons(legId);
String dialogId = legIdParts[0];
String agent = legIdParts[1];
String ani = legIdParts[2];
Long cxLegStartTime = Long.parseLong(legIdParts[3].substring(0, legIdParts[3].length() - 3));
// Query FusionPBX database for file path
List<EfswitchCallLeg> efswitchCallLegs = getEfswitchCallLegs(dialogId, agent, ani, cxLegStartTime);
// Find closest match by start time
EfswitchCallLeg matchedLeg = findClosestMatch(efswitchCallLegs, cxLegStartTime);
// Return decrypted or plain file
return encryptionEnabled ? getDecryptedResource(matchedLeg) : getPlainResource(matchedLeg);
}
```
Decryption (AES/CFB/NoPadding):
```java
byte[] keyBytes = hexStringToByteArray(hexKey);
Key secretKey = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CFB/NoPadding");
byte[] iv = new byte[16];
inputStream.read(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(cipherMode, secretKey, ivSpec);
```
EleveoRecordingService
public Resource getRecordingFile(String legId) {
String[] legIdParts = legId.split(":");
String dialogId = legIdParts[0];
String agentExt = legIdParts[1];
String customerNum = legIdParts[2];
long cxLegStartTime = parseLegStartTime(legIdParts[3]);
// Fetch conversations from Eleveo API
List<EleveoCallLeg> eleveoCallLegs = getEleveoCallLegs(dialogId, customerNum);
// Merge legs broken by hold/retrieve/conference
mergeHoldEndedLegs(eleveoCallLegs, agentExt);
mergeRetrievedLegs(eleveoCallLegs, agentExt);
mergeConferenceLegs(eleveoCallLegs, agentExt);
// Find matching leg
EleveoCallLeg matchedLeg = findMatchedLeg(eleveoCallLegs, dialogId, agentExt, cxLegStartTime);
// Authenticate with Eleveo and download
String jsessionId = getEleveoJsessionId();
return buildRecordingFile(matchedLeg, jsessionId);
}
```
Eleveo Authentication Flow:
1. Get JSESSIONID via /login?loginname=admin&password=xxx
2. Get download token via /api/download?type=1&action=download&sid=<legId>
3. Download file via /api/download?token=<token>
11.2 Recording Link Activities
The Recording Link Activities service pushes recording URLs to CCM as third-party activities.
Flow:
```
Scheduled Job (every N minutes)
│
▼
1. Calculate time interval (from last pushed time to now)
│
▼
2. Fetch CX Voice call legs from CCM Voice Activities API
GET /conversation-manager/activities/voice?startTime=...&endTime=...
│
▼
3. Fetch corresponding recording legs from backend:
• EFSwitch: query FusionPBX DB + filesystem
• Eleveo: query Eleveo Conversations API
│
▼
4. Map CX legs to recording legs
│
▼
5. Push recording link to CCM Third Party Activities API
POST /conversation-manager/activities/third-party
CIM Message:
{
"header": {
"intent": "VOICE_RECORDING",
"sender": { "type": "CONNECTOR", "senderName": "CX-Recording-Link-Activities" },
"channelSessionId": "<channelSessionId>"
},
"body": {
"legId": "<legId>",
"voiceRecordingUrl": "https://middleware/{legId}/recording-file"
}
}
│
▼
6. Update cache with last pushed time
```
Helper.createRecordingLinkPayload():
```java
public CimMessage createRecordingLinkPayload(String channelSessionId, String legId) {
MessageHeader header = new MessageHeader();
header.setSender(this.sender);
header.setChannelSessionId(channelSessionId);
header.setIntent("VOICE_RECORDING");
ObjectNode node = mapper.createObjectNode();
node.put("legId", legId);
node.put("voiceRecordingUrl", globalProperties.getMiddlewareApi() + "/" + legId + "/recording-file");
UrlMessage body = new UrlMessage();
body.setAdditionalDetails(node);
return new CimMessage(UUID.randomUUID().toString(), header, body);
}
```
---
12. Call Failure Scenarios
12.1 No Agent Available
FreeSWITCH calls request_agent() → Voice Connector → CCM → Routing Engine
│
▼
No agents available
│
▼
CCM sends NO_AGENT_AVAILABLE to Voice Connector
│
▼
Voice Connector plays "no_agent_available.wav" to customer
│
▼
FreeSWITCH schedules hangup after 6 seconds
12.2 Transfer Failure
Voice Connector sends uuid_transfer command to FreeSWITCH
│
▼
ESL response contains "-ERR"
│
▼
Voice Connector logs "TRANSFER FAILED"
│
▼
Voice Connector sends END_CHAT to CCM
│
▼
Call is terminated
12.3 Dialer Connection Failure (Outbound)
Voice Connector attempts to send agent details to Dialer
│
▼
Dialer returns 503 (Service Unavailable)
│
▼
Voice Connector sends END_CHAT to CCM
│
▼
Throws DialerConnectionException
---
13. Key Source Files Reference
13.1 Cisco Connector
|
File |
Path |
Purpose |
|---|---|---|
|
|
|
Main JTAPI event listener, persists events |
|
|
|
Correlates JTAPI events into CallLegs |
|
|
|
Fetches Cisco TCD/CCD, builds CIM messages |
|
|
|
JDBC queries for Cisco DB |
|
|
|
JTAPI address-level observer |
|
|
|
JTAPI provider state observer |
13.2 JTAPI Connector (Legacy)
|
File |
Path |
Purpose |
|---|---|---|
|
|
|
Singleton JTAPI provider connection |
|
|
|
Call-level event observer |
|
|
|
Call correlation and database operations |
13.3 Voice Connector (ecx_generic_connector)
|
File |
Path |
Purpose |
|---|---|---|
|
|
|
Core voice connector logic |
|
|
|
Receives FreeSWITCH HTTP requests |
|
|
|
Receives CCM CIM messages |
13.4 FreeSWITCH Scripts
|
File |
Path |
Purpose |
|---|---|---|
|
|
|
Main inbound/rona/agent request script |
|
|
|
Sets recording filename based on call type |
|
|
|
Hangup hook processing |
|
|
|
Per-tenant environment variables |
13.5 Recording Middleware
|
File |
Path |
Purpose |
|---|---|---|
|
|
|
REST API for recording files |
|
|
|
EFSwitch backend implementation |
|
|
|
Eleveo backend implementation |
13.6 Recording Link Activities
|
File |
Path |
Purpose |
|---|---|---|
|
|
|
Pushes EFSwitch recording links |
|
|
|
Pushes Eleveo recording links |
|
|
|
Common utilities, CIM payload builder |
---
14. Data Models & Entity Relationships
14.1 Cisco Connector Entities
JtapiEvent
├── id (UUID)
├── eventType (CALL_CONNECTED, CALL_HELD, CALL_RESUMED, CALL_TRANSFERRED, CALL_CONSULT_STARTED, CALL_CONF_STARTED, CALL_CONF_ENDED, CALL_DISCONNECTED)
├── globalCallId (String)
├── jtapiId (String)
├── connectionId (String)
├── previousGlobalCallId (String, for consult calls)
├── callingTerminal (String)
├── calledTerminal (String)
├── callingExtension (String)
├── calledExtension (String)
├── callDirection (INBOUND, OUTBOUND, INTERNAL)
└── createdAt (Timestamp)
CallLeg
├── id (UUID)
├── callId (String, generated UUID for the call)
├── globalCallId (String, Cisco GCID)
├── connectionId (String, xrefciId)
├── jtapiId (String, Cisco JTAPI ID)
├── agentExtension (String)
├── startTime (Instant)
├── endTime (Instant)
├── duration (Long, milliseconds)
├── startReason (INBOUND, OUTBOUND, CONSULT, CONSULT_TRANSFER, CONSULT_CONFERENCE)
├── endReason (DIALOG_ENDED, DIRECT_TRANSFER, CONSULT_TRANSFER, CONSULT_ENDED, CONFERENCE_ENDED, HOLD, etc.)
├── conversationType (INBOUND, OUTBOUND)
├── callType (SIMPLE, DIRECT_TRANSFER, CONSULT, CONSULT_TRANSFER, CONFERENCE)
├── serviceIdentifier (String, populated from Cisco TCD/CCD)
├── customerIdentifier (String, populated from Cisco TCD/CCD)
├── agentName (String, populated from Cisco TCD/CCD)
├── agentId (String, populated from Cisco TCD/CCD)
├── wrapUps (String, populated from Cisco TCD/CCD)
└── syncWithCisco (Boolean)
CallLegHistory
├── id (UUID)
├── callLeg (CallLeg)
├── eventType (CALL_HELD, CALL_RESUMED)
└── timestamp (Instant)
```
14.2 JTAPI Connector Entities (Legacy)
CallInformation
├── id (UUID)
├── gcid (String, Global Call ID)
├── jtapiId (String)
├── xrefciId (String, Connection ID)
├── uuid (String, FreeSWITCH call UUID)
├── callType (String: SimpleCall, ConsultCall, ConfCall)
├── eventType (CiscoEventType)
├── callEndTime (Date)
├── callingExtension (String)
├── calledExtension (String)
└── ... (various correlation fields for consult/conference)
---
15. Configuration & Environment Variables
15.1 Cisco Connector
|
Variable |
Description |
|---|---|
|
|
Cisco Unified Communications Manager IP address |
|
|
JTAPI application user |
|
|
JTAPI application user password |
|
|
Enable/disable HA with Consul locks |
|
|
Consul server URL (if HA enabled) |
|
|
ExpertFlow CCM API base URL |
|
|
Callback URL for ExpertFlow |
15.2 JTAPI Connector (Legacy)
|
Variable |
Description |
|---|---|
|
|
CUCM IP address |
|
|
JTAPI user |
|
|
JTAPI password |
|
|
HA mode flag |
|
|
Consul URL |
15.3 Recording Middleware
|
Property |
Description |
|---|---|
|
|
|
|
|
Enable AES encryption for files |
|
|
Temporary decryption cache path |
|
|
Eleveo server URL |
|
|
Eleveo admin username |
|
|
Eleveo admin password |
15.4 Recording Link Activities
|
Property |
Description |
|---|---|
|
|
|
|
|
Default lookback interval in days |
|
|
Recording Middleware base URL |
|
|
CCM Third Party Activities API |
|
|
CCM Voice Activities API |
|
|
Enable auth token caching |
---
16. Event Chain Summary
16.1 Inbound Call Event Chain
[Customer] SIP INVITE → [FreeSWITCH]
[FreeSWITCH] HTTP POST /request-agent → [Voice Connector]
[Voice Connector] CIM ASSIGN_RESOURCE_REQUESTED → [CCM]
[CCM] → [Routing Engine] Task QUEUED → RESERVED
[Routing Engine] AGENT_RESERVED → [CCM]
[CCM] CIM AGENT_RESERVED → [Voice Connector]
[Voice Connector] ESL uuid_transfer → [FreeSWITCH]
[FreeSWITCH] SIP INVITE → [Agent Desktop]
[Agent Desktop] SIP 200 OK → [FreeSWITCH]
[Agent Desktop] CIM CALL_ALERTING → [CCM]
[Agent Desktop] CIM CALL_LEG_STARTED → [CCM]
[JTAPI] ConnConnectedEv → [Cisco Connector] → DB jtapi_event
[Customer/Agent] Hangup
[FreeSWITCH] SIP BYE → [Agent Desktop]
[Agent Desktop] CIM CALL_LEG_ENDED → [CCM]
[JTAPI] ConnDisconnectedEv → [Cisco Connector] → DB jtapi_event
[CCM] Conversation closed → [Routing Engine] MRD READY
[Cisco Connector] Correlates JTAPI events → CallLegs
[Cisco Connector] Fetches Cisco TCD/CCD → Enriches CallLegs
[Cisco Connector] CIM CALL_LEG_STARTED + CALL_LEG_ENDED + WRAPUP → [CCM]
[Recording Link Activities] Query recordings → Push VOICE_RECORDING links → [CCM]
16.2 CIM Intents Used
|
Intent |
Direction |
Description |
|---|---|---|
|
|
Connector → CCM |
Request agent assignment |
|
|
Connector → CCM |
Cancel agent request (RONA) |
|
|
CCM → Connector |
Agent reserved notification |
|
|
Agent Desktop → CCM |
Call is ringing on agent |
|
|
Agent Desktop → CCM |
Agent/customer answered |
|
|
Agent Desktop → CCM |
Call ended |
|
|
Cisco Connector → CCM |
Call was put on hold |
|
|
Cisco Connector → CCM |
Call was resumed |
|
|
Cisco Connector → CCM |
Wrap-up codes from Cisco |
|
|
Link Activities → CCM |
Recording URL for activity |
|
|
Connector → CCM |
End conversation |
|
|
Connector → CCM |
Call result notification |
---
17. Cisco-Side Configuration Reference
This section consolidates all Cisco-side configuration requirements extracted from ExpertFlow Confluence documentation (spaces: VRS, CX). It covers CUCM configuration, FreeSWITCH recorder setup, CX Unified Admin configuration, AgentDesk environment variables, SIP Proxy integration, and recording security settings.
---
17.1 CUCM Configuration Requirements (Voice Recording)
Source: Confluence .CUCM Configuration Requirements v14.0 (VRS space)
The following configurations are required on Cisco Unified Communications Manager (CUCM) to capture voice streams for audio recording.
17.1.1 Recording Profile
-
Create a Recording Profile in CUCM:
-
Navigate to Device → Device Settings → Recording Profile
-
Create a new profile and associate it with your Recording Destination Address (the DN or Route Point where recordings are sent)
-
Create a SIP Profile:
-
Enable the PING Option (helps in troubleshooting trunk health)
-
Configure appropriate codec preferences
-
Create a SIP Security Profile:
-
Set Incoming Transport to
TCP + UDP -
Set Outgoing Transport to
TCP -
Enable Built-in-Bridge (BiB) on all agent phones that will be recorded:
-
Navigate to Device → Phone → [Select Phone] → Device Configuration
-
Set Built-in-Bridge to
On -
Link Recording Profile to Phones:
-
On each phone configuration, set the Recording Profile field to the profile created above
17.1.2 SIP Trunk Configuration
Create a SIP Trunk pointing to the ExpertFlow Recording Server (FreeSWITCH/VRS):
|
Field |
Value |
|---|---|
|
Trunk Type |
SIP Trunk |
|
Device Protocol |
SIP |
|
Destination Address |
Recording Server IP |
|
Destination Port |
|
|
SIP Profile |
Use the SIP Profile created above |
|
SIP Security Profile |
Use the SIP Security Profile created above |
|
Recording Enabled Gateway |
Check "This trunk is connected to recording enabled gateway" |
17.1.3 Network & Firewall Requirements
-
Disable firewall OR allow the following ports:
-
5060TCP (SIP signaling, inbound + outbound) -
All UDP ports for RTP media streams
17.1.4 Troubleshooting Checklist
|
Symptom |
Check |
|---|---|
|
SIP Trunk status DOWN |
Verify VRS/Recorder is UP and listening; restart FreeSWITCH |
|
No recordings received |
Verify Recording Profile is linked to Destination Address (DN/Route Point) |
|
Trunk connection issues |
Verify SIP Security Profile has TCP as outgoing port |
|
BiB not recording |
Verify BiB is enabled on agent phones |
---
17.2 FreeSWITCH Recorder Configuration (VRS)
Source: Confluence Recorder Configurations (VRS space)
17.2.1 Recording Script Setup
-
Place the
record.luascript in the FreeSWITCH scripts directory: -
Source package:
/usr/local/freeswitch/scripts/ -
Package install:
/usr/share/freeswitch/scripts/ -
Update the IP addresses in
record.luato point to your server:
-- Update these URLs in record.lua
url = "http://<IP-ADDRESS>:9900/mixer/sip-data"
url = "http://<IP-ADDRESS>:8080/vrs/recording-rules/evaluate"
url = "http://<IP-ADDRESS>:9900/mixapi"
-
Create the recording directories:
mkdir -p /var/vrs/recordings/cucmRecording/streams
mkdir -p /var/vrs/recordings/cucmRecording/sessions
chmod 777 -R /var/vrs/
-
Update directory paths in
record.lua:
recording_dir = "/var/vrs/recordings/cucmRecording/streams/"
recording_path = "/var/vrs/recordings/cucmRecording/streams/"
mixedRecordingDir = "/var/vrs/recordings/cucmRecording/sessions/"
17.2.2 Dialplan Configuration
Edit /usr/local/freeswitch/conf/dialplan/public.xml and add:
<!-- Mark outside calls -->
<extension name="outside_call" continue="true">
<condition>
<action application="set" data="outside_call=true"/>
<action application="export" data="RFC2822_DATE=${strftime(%a, %d %b %Y %T %z)}"/>
</condition>
</extension>
<!-- CUCM Recording Profile trigger -->
<extension name="CUCM Recording Profile">
<action application="log" data="INFO Entering Call from CUCM"/>
<condition field="${sip_from_host}" expression="<CUCM_IP_ADDRESS>">
<action application="lua" data="record.lua"/>
</condition>
</extension>
```
For multiple CUCM servers, use pipe-delimited IPs:
<condition field="${sip_from_host}" expression="192.168.1.26|192.168.1.27|192.168.1.28">
<action application="lua" data="record.lua"/>
</condition>
</extension>
17.2.3 SIP Profile Settings
Edit both internal.xml and external.xml in /usr/local/freeswitch/conf/sip_profiles/:
-
Enable 3PCC (Third Party Call Control):
<param name="enable-3pcc" value="true"/>
-
Parse all INVITE headers (add to
internal.xml):
<param name="parse-all-invite-headers" value="true"/>
-
Set IP addresses in
external.xml:
<param name="rtp-ip" value="<recorder-ip>"/>
<param name="sip-ip" value="<recorder-ip>"/>
<param name="ext-rtp-ip" value="<recorder-ip>"/>
<param name="ext-sip-ip" value="<recorder-ip>"/>
17.2.4 Access Control List (ACL)
Edit /usr/local/freeswitch/conf/autoload_configs/acl.conf.xml and add CUCM IPs:
<configuration name="acl.conf" description="Network Lists">
<network-lists>
<!-- existing entries -->
<node type="allow" domain="$${domain}"/>
<!-- Add CUCM IP(s) -->
<node type="allow" cidr="<CUCM_IP>/32"/>
<!-- Repeat for each CUCM -->
</network-lists>
</configuration>
17.2.5 Disable STUN
Edit /usr/local/freeswitch/conf/vars.xml and comment out STUN lines:
<!-- Comment these out -->
<!-- <X-PRE-PROCESS cmd="stun-set" data="external_rtp_ip=stun:stun.freeswitch.org"/> -->
<!-- <X-PRE-PROCESS cmd="stun-set" data="external_sip_ip=stun:stun.freeswitch.org"/> -->
Restart FreeSWITCH:
sudo systemctl restart freeswitch
17.2.6 Required Utilities
Install Lua dependencies:
sudo apt install -y lua-socket
sudo apt install -y lua-dkjson
---
17.3 CX Unified Admin — Cisco Voice Channel Configuration
Source: Confluence (4.8) Cisco Voice Channel Configuration Guide and (4.8) Cisco Voice Channel Configuration Guide for CX4.2 (CX space)
17.3.1 Pre-Requisites & Recommendations
-
Finesse and CIM Server time must be synced (NTP)
-
Customer Activity Timeout in CX must be greater than the call timeout configured on Cisco (Finesse) for the voice channel
-
For CX4.2+ use
CISCO_CCchannel type; for earlier versions useVOICE
17.3.2 Channel Provider Configuration (Unified Admin)
-
Navigate to Unified Admin → Channel Management → Channel Providers
-
Add a provider for
CISCO_CC(CX4.2+) orVOICE(earlier) channel type: -
The provider webhook is not required for Cisco integration
-
Add a Channel Connector for the configured provider
17.3.3 Channel Configuration
Add a channel with the following settings:
|
Field |
Value |
|---|---|
|
Channel Name |
Descriptive name (e.g., "Cisco Voice") |
|
Service Identifier |
The DN (Dial Number) configured for the contact center |
|
Service Identifier Note |
Must match |
|
Bot |
Select configured bot |
|
Channel Connector |
Select the connector created above |
|
Channel Mode |
|
|
Activity Timeout |
Must be > Cisco call timeout |
|
Routing Mode |
|
|
Default Outbound Channel |
Enable for outbound dialing |
17.3.4 MRD (Media Routing Domain) Configuration
For the default CISCO_CC (or VOICE) MRD:
|
Setting |
Value |
|---|---|
|
|
|
|
|
|
For each user/agent, set max task request to 1 for the Cisco CC MRD.
---
17.4 AgentDesk Environment Configurations for Cisco
Source: Confluence (4.7) AgentDesk Environment Configurations for Cisco (CX space)
Update the ef-unified-agent-configmap.yaml at:
cim-solution/kubernetes/cim/ConfigMaps/ef-unified-agent-configmap.yaml
17.4.1 Required CTI Config Variables
# To enable Cisco contact center integration
isCiscoEnabled: "true"
Cisco Finesse domain name
Secondary Finesse domain (leave blank if no HA)
Cisco Finesse BOSH URL
Secondary Finesse BOSH URL (leave blank if no HA)
SSO backend URL (if Finesse is SSO-enabled)
Cisco UCCE flavor: UCCE | UCCX
RONA timeout as configured on Finesse
Enable if Finesse runs in HA
Finesse FQDN or IP address
Secondary Finesse URL (HA only)
Static identifier used as channel identifier in CX
17.4.2 Encrypted Credentials
Share cf-admin credentials with ExpertFlow support. They will encrypt and update:
ctiParam: "<Encrypted cf admin username>"
ctiParam2: "<Encrypted cf admin password>"
17.4.3 Apply ConfigMap
cd cim-solution/kubernetes
kubectl delete -f cim/ConfigMaps/ef-unified-agent-configmap.yaml
kubectl apply -f cim/ConfigMaps/ef-unified-agent-configmap.yaml
kubectl delete -f cim/Deployments/ef-unified-agent-deployment.yaml
kubectl apply -f cim/Deployments/ef-unified-agent-deployment.yaml
```
---
17.5 Cisco Elements Configurations for CX SIP Proxy
Source: Confluence (4.7) Cisco Elements Configurations for CX SIP Proxy (CX space)
17.5.1 Inbound Call Configuration
CUBE (Cisco Unified Border Element) Settings:
-
Incoming Dial-Peer Configuration:
-
Set the destination IP address of the SIP Proxy in the
session target ipv4field -
Trusted List:
-
Add the CX SIP Proxy server IP(s) to the trusted list for security
CVP (Cisco Voice Portal) Settings:
-
SIP Proxy Group Assignment:
-
Create a group containing CVP IP addresses
-
Map this group to a Dial Number (DN) on the SIP Proxy from CUBE
-
CVP OAMP Configuration:
-
Configure SIP Proxy IP for specific labels:
45199,9191,9292 -
These correspond to EF SIP Proxy configurations
-
VVB (Cisco Voice Browser) Integration:
-
On SIP Proxy, assign a group containing VVB IP addresses to the labels above
-
CUCM Integration:
-
In CVP OAMP, set SIP Proxy IP against CUCM Extension Numbers
-
On SIP Proxy, assign a group containing CUCM IP addresses for call routing
17.5.2 Outbound Call Configuration
Manual Outbound Calls:
-
CUCM SIP Trunk:
-
Create a SIP Trunk on CUCM connecting to the SIP Proxy
-
SIP Proxy Group & Pattern:
-
Create a group containing CUBE IP addresses
-
Map to specific dialing prefixes (e.g.,
96***********)
Dialer Connected Calls:
-
Cisco Dialer Registry:
-
Set
SIPServerAddressto the SIP Proxy Server IP -
SIP Proxy Group & Pattern:
-
Same as manual outbound — group CUBE IPs and map to dialing patterns
---
17.6 ESL Configuration for Pause and Resume Recording
Source: Confluence ESL Configuration for Pause and Resume Recording (VRS space)
The Event Socket Library (ESL) allows external control of FreeSWITCH for pause/resume recording functionality.
17.6.1 ESL Configuration
Edit /etc/freeswitch/autoload_configs/event_socket.conf.xml (or /usr/local/freeswitch/conf/autoload_configs/):
<settings>
<param name="listen-ip" value="192.168.1.106"/> <!-- Server IP -->
<param name="listen-port" value="8021"/> <!-- Default ESL port -->
<param name="password" value="<strong-password>"/><!-- NOT default ClueCon -->
<param name="apply-inbound-acl" value="esl"/>
</settings>
Edit acl.conf.xml in the same directory:
<configuration name="acl.conf" description="Network Lists">
<network-lists>
<!-- existing lists -->
<list name="esl" default="allow">
<node type="allow" cidr="0.0.0.0/0"/>
</list>
</network-lists>
</configuration>
Note: Restrict cidr to specific IPs in production (do not use 0.0.0.0/0).
17.6.2 Apply Changes
fs_cli
> reloadxml
> reloadacl
sudo systemctl restart freeswitch
Test ESL connection
---
17.7 Recording File Encryption/Decryption Configuration
Source: Confluence Call Recording File Encryption/Decryption Configuration for EFCX (VRS space)
17.7.1 FreeSWITCH Event Hook
Edit /etc/freeswitch/autoload_configs/lua.conf.xml and add under <!-- Subscribe to events -->:
<hook event="RECORD_STOP" subclass="" script="encrypt.lua"/>
Restart FreeSWITCH:
systemctl restart freeswitch
17.7.2 Lua Encryption Script
Create /usr/share/freeswitch/scripts/encrypt.lua:
package.cpath = "/usr/lib/x86_64-linux-gnu/lua/5.2/?.so;" .. package.cpath
package.path = "/usr/share/lua/5.2/?.lua;" .. package.path
local json = require("cjson")
local eventClass = event:getHeader("Event-Subclass")
local uuid = event:getHeader("variable_uuid")
local call_uuid = event:getHeader("variable_call_uuid")
local call_id = event:getHeader("variable_sip_h_X-Call-ID")
local time = event:getHeader("Event-Date-GMT")
local filename = event:getHeader("recording_filename")
freeswitch.consoleLog("INFO", "UUID: " .. tostring(uuid) .. "\n")
freeswitch.consoleLog("INFO", "Filename: " .. tostring(filename) .. "\n")
local input_file_path = event:getHeader("Record-File-Path")
local command = string.format("python3 /usr/share/freeswitch/pythonScript/encrypt.py '%s'", input_file_path)
os.execute(command)
```
Install Lua if needed:
sudo apt update
sudo apt install lua5.3 -y
17.7.3 Python Encryption Script
Create /usr/share/freeswitch/pythonScript/encrypt.py:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import sys
import logging
import os
log_file = "/var/log/encrypt.log"
logging.basicConfig(filename=log_file, level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s")
def encrypt_file(mixedRecordingPathName, key):
try:
iv = b'1234567890123456'
backend = default_backend()
cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=backend)
encryptor = cipher.encryptor()
with open(mixedRecordingPathName, 'rb') as f:
data = f.read()
padder = padding.PKCS7(algorithms.AES.block_size).padder()
padded_data = padder.update(data) + padder.finalize()
encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
with open(mixedRecordingPathName, 'wb') as f:
f.write(iv + encrypted_data)
logging.info(f"Successfully encrypted: {mixedRecordingPathName}")
return mixedRecordingPathName
except Exception as e:
logging.error(f"Error encrypting {mixedRecordingPathName}: {str(e)}")
return None
if __name__ == "__main__":
key = bytes.fromhex('42066107bda481f0266fd709627faf98b422e29a29b01495daa3ef3640ee6fe6')
mixedRecordingPathName = sys.argv[1]
encrypted_file = encrypt_file(mixedRecordingPathName, key)
```
Install dependencies:
sudo apt install python3 -y
sudo apt install python3-cryptography -y
17.7.4 Dialplan Recording Filename Export
In FusionPBX (or FreeSWITCH dialplan), add to the user_record dialplan (last group):
|
Tag |
Type |
Data |
Group |
Order |
Enabled |
|---|---|---|---|---|---|
|
action |
export |
|
9 |
275 |
true |
17.7.5 Log File Setup
sudo touch /var/log/encrypt.log
sudo chmod 777 /var/log/encrypt.log
---
17.8 High Availability (HA) / Duplex Deployment
Source: Confluence High Availability (HA) / Duplex Deployment (VRS space)
17.8.1 Prerequisites
-
A shared file system based on NAS or SAN (note: this is a single point of failure)
-
Two VMs with identical specifications (4+ cores, 4+ GB RAM, 300+ GB disk)
17.8.2 Recorder Failover
Deploy core recorder service on two servers with identical configurations. Configure two SIP trunks in CUCM.
|
Scenario |
Behavior |
|---|---|
|
Recorder-A (active) is down |
CUCM sends SIP invites to Recorder-B |
|
Recorder-A restores |
CUCM makes Recorder-A active again |
|
Both recorders down |
No recordings until one restores |
|
Link between Recorder-A and CUCM down |
CUCM fails over to Recorder-B |
|
Recorder-A crashes with active calls |
Active recordings marked as abnormal (no files) |
17.8.3 Other Services HA
Mixer, REST Server, Archival Process — High availability is achieved via Docker Swarm:
-
Docker Swarm manages container orchestration across multiple Docker hosts
-
If a worker node becomes unavailable, Docker schedules tasks on other nodes
-
If a Docker engine goes down, the Linux OS typically restarts it automatically
17.8.4 SIP Trunk Failover in CUCM
CUCM keeps track of recorder servers and sends SIP invites to one active recorder. The active recorder saves recordings on the shared network file system.
---
17.9 Cisco Generic Connector JTAPI Commands Reference
Source: Confluence Generic Connector JTAPI (GC space)
The Generic Connector exposes JTAPI-based commands for agent state and call control. These are relevant for understanding the Cisco-side event vocabulary that the CX system consumes.
17.9.1 Agent Commands
|
Input |
Category |
Message Type |
Parameters |
|---|---|---|---|
|
Send Heartbeat |
system |
|
— |
|
Agent Login |
agent_state |
|
|
|
Mobile Agent Login |
agent_state |
|
|
|
Agent Logout |
agent_state |
|
— |
|
Make Ready |
agent_state |
|
— |
|
Make Not Ready |
agent_state |
|
— |
|
Make Not Ready with Reason |
agent_state |
|
|
|
Make Call |
agent_call |
|
|
|
Answer Call |
agent_call |
|
|
|
Hold Call |
agent_call |
|
|
|
Retrieve Call |
agent_call |
|
|
|
Release Call |
agent_call |
|
|
|
Consult/Transfer Initiation |
agent_call |
|
|
|
Complete Transfer |
agent_call |
|
|
|
Complete Conference |
agent_call |
|
|
|
Wrap-up Code |
agent_dialog |
|
|
17.9.2 GC Output Events (relevant to CX)
|
Code |
Output |
Description |
|---|---|---|
|
O-001 |
Agent State |
|
|
O-008 |
Inbound Call State |
Call state updates with DialogID |
|
O-009 |
New Inbound Call |
New call notification |
|
O-010 |
Consult Call State |
Consult/transfer state |
|
O-014 |
Dialog State |
|
|
O-028 |
New Outbound Call |
Outbound preview/new call |
17.9.3 Agent States
UNKNOWN, READY, NOT_READY, LOGOUT, TALKING, RESERVED,
RESERVED_OUTBOUND, RESERVED_OUTBOUND_PREVIEW, WORK, WORK_READY, HOLD
17.9.4 Dialog States
INITIATED, INITIATING, ALERTING, ACTIVE, HELD, FAILED, DROPPED, ACCEPTED
---
17.10 Understanding HA vs Non-HA in CX Voice
HA = High Availability — a deployment architecture where critical components run in redundant pairs (or clusters) so that if one instance fails, another automatically takes over with minimal or no service interruption.
Non-HA = Single-instance deployment — each component runs on exactly one server. If that server fails, the service is completely unavailable until manual recovery.
---
17.10.1 The Core Idea (Analogy)
Imagine a bridge with one lane vs. a bridge with two lanes:
|
Scenario |
Non-HA (Single Lane) |
HA (Dual Lane) |
|---|---|---|
|
Normal traffic |
One lane handles all cars |
Both lanes share traffic |
|
Lane closes (failure) |
Total traffic halt — cars cannot cross |
Traffic rerouted to the open lane — cars keep moving |
|
Recovery |
Manual repair required before any traffic resumes |
Automatic — open lane already handling traffic |
In software terms, the "lane" is a running service instance (JVM, Docker container, or VM). HA ensures there is always a backup ready to take over.
---
17.10.2 Where HA Matters in This Architecture
The CX Voice + Cisco system has four distinct HA domains. Each operates independently — you can configure HA for some and not others.
A. JTAPI Connector HA (Cisco-Connector / JTAPI-Connector)
What it does: Listens to Cisco CUCM call events via JTAPI and persists them to the database.
|
Aspect |
Non-HA Mode |
HA Mode |
|---|---|---|
|
Deployment |
Single |
Two |
|
Coordination |
None — single listener |
Consul distributed locks — only one node processes events for a given call at a time |
|
Failover |
If the node crashes, no events are captured until restarted |
Consul detects the lock timeout; the standby node acquires the lock and resumes processing |
|
Config flag |
|
|
|
Data safety |
Events may be lost during downtime |
Events continue flowing; lock handover prevents duplicate processing |
How the Consul lock works:
Node-A (Active) Node-B (Standby) Consul Server
│ │ │
├─ acquireLock(callId) ──►│ │
│◄──── lock granted ───────┤ │
│ │ │
├─ process JTAPI event ──► │ │
│ │ │
[Node-A CRASHES] │ │
│ ├─ acquireLock(callId) ──►│
│ │◄──── lock granted ──────┤
│ │ │
│ ├─ process JTAPI event ──► │
The lock is per call ID, so even in HA mode, events for the same call are never processed by both nodes simultaneously (preventing duplicates).
---
B. Finesse HA (Agent Desktop CTI)
What it does: The AgentDesk (unified-agent) connects to Cisco Finesse for agent state management, call control, and screen-pop.
|
Aspect |
Non-HA Mode |
HA Mode |
|---|---|---|
|
Deployment |
Connects to one Finesse server |
Connects to primary + secondary Finesse servers |
|
Configuration |
|
|
|
Failover trigger |
N/A |
When primary Finesse stops responding or returns HTTP errors |
|
Agent experience |
If Finesse1 fails, agents cannot log in or control calls |
Agents are automatically reconnected to Finesse2; state is preserved |
|
Config flag |
|
|
Important: Finesse HA is a Cisco-side feature (two Finesse servers configured as a pair in Cisco UCCE/UCCX). CX simply connects to whichever is active.
---
C. Recorder HA (Voice Recording Server / FreeSWITCH)
What it does: Receives SIPREC/BiB streams from CUCM and writes recording files.
|
Aspect |
Non-HA Mode |
HA Mode |
|---|---|---|
|
Deployment |
One FreeSWITCH recorder VM |
Two FreeSWITCH recorder VMs (Recorder-A, Recorder-B) |
|
CUCM config |
One SIP Trunk to Recorder-A |
Two SIP Trunks — one to each recorder |
|
Storage |
Local disk |
Shared NAS/SAN mounted on both recorders |
|
Failover |
If Recorder-A fails, no recordings are captured |
CUCM detects Recorder-A is down via SIP OPTIONS ping; sends new calls to Recorder-B |
|
Active calls during failure |
Recordings abruptly stop |
Active recordings on Recorder-A are marked as abnormal (no complete file) |
|
Recovery |
Manual restart of Recorder-A |
When Recorder-A comes back, CUCM automatically makes it active again for new calls |
How CUCM decides which recorder to use:
CUCM sends periodic SIP OPTIONS pings to both trunks. The recorder that responds is marked active. If the active recorder stops responding, CUCM switches to the other trunk.
CUCM ──SIP INVITE──► Recorder-A (ACTIVE) ──RTP──► writes to NAS
│
└──SIP OPTIONS──► Recorder-A ◄──200 OK── healthy
│
└──SIP OPTIONS──► Recorder-B ◄──200 OK── healthy (standby)
[Recorder-A stops responding]
CUCM ──SIP INVITE──► Recorder-B (now ACTIVE) ──RTP──► writes to same NAS
```
---
D. Docker Swarm HA (Mixer, REST API, Archival)
What it does: Services like the recording mixer, REST server, and archival process run as Docker containers.
|
Aspect |
Non-HA Mode |
HA Mode |
|---|---|---|
|
Deployment |
Single Docker Engine, one container per service |
Docker Swarm cluster — multiple nodes (managers + workers) |
|
Orchestration |
Manual |
Swarm manager schedules and supervises containers |
|
Failover |
If the VM dies, all containers stop |
Swarm reschedules containers on surviving nodes |
|
Scaling |
Fixed |
Can scale services to multiple replicas for load balancing |
|
Health checks |
None automatic |
Swarm monitors container health and restarts unhealthy ones |
Docker Swarm behavior:
┌─────────────────┐ ┌─────────────────┐
│ Swarm Node 1 │ │ Swarm Node 2 │
│ (Manager) │◄───────►│ (Worker) │
│ │ Mesh │ │
│ • Mixer svc │ Network │ • Mixer svc │
│ • REST API svc │ │ • Archival svc │
└─────────────────┘ └─────────────────┘
[Node 1 crashes]
Swarm Manager (on Node 2 or Node 3) detects failure
│
▼
Reschedules Mixer + REST API containers to Node 2
```
---
17.10.3 Configuration Comparison: HA vs Non-HA
|
Component |
Non-HA Config |
HA Config |
What Changes |
|---|---|---|---|
|
JTAPI Connector |
|
|
Needs Consul server; both connector instances point to same Consul |
|
Finesse (AgentDesk) |
|
|
AgentDesk code handles failover logic |
|
Recorder (VRS) |
One SIP Trunk in CUCM |
Two SIP Trunks in CUCM<br>Shared NAS/SAN |
CUCM admin configures both trunks; both recorders mount same storage |
|
Docker Services |
|
|
Needs multiple VMs; shared storage for persistent data |
---
17.10.4 Trade-offs: When to Use HA
|
Factor |
Non-HA |
HA |
|---|---|---|
|
Cost |
Lower (fewer servers, no shared storage) |
Higher (2x servers, NAS/SAN, load balancers) |
|
Complexity |
Simple to deploy and troubleshoot |
More moving parts; requires Consul, Swarm, or cluster management |
|
Downtime tolerance |
Acceptable for dev/test or small deployments |
Required for production contact centers |
|
Recording compliance |
Risk of gaps if recorder fails |
Continuous recording with failover |
|
Agent productivity |
Agents cannot work during Finesse outage |
Seamless failover; agents stay productive |
|
Data integrity |
Potential for lost JTAPI events during outage |
Lock handover ensures no duplicate or missed events |
Rule of thumb:
- Non-HA = Development, testing, proof-of-concept, or small contact centers (< 20 agents) where occasional downtime is acceptable.
- HA = Production deployments, regulated industries (finance, healthcare), or any environment where every call and recording must be captured.
---
17.10.5 Summary Diagram: Full HA Deployment
┌─────────────────────────────────────────────────────────────────────────────┐
│ PRODUCTION HA DEPLOYMENT │
├─────────────────────────────────────────────────────────────────────────────┤
│ CUCM │ FreeSWITCH Recorders (HA Pair) │
│ ┌─────────┐ │ ┌─────────────────┐ ┌─────────────────┐ │
│ │ SIP Trk │───────────────────┼─►│ Recorder-A │ │ Recorder-B │ │
│ │ (Pri) │ │ │ (Active) │ │ (Standby) │ │
│ └─────────┘ │ │ │ │ │ │
│ ┌─────────┐ │ │ writes to ──────┼──┼─► Shared NAS │ │
│ │ SIP Trk │───────────────────┼─►│ │ │ │ │
│ │ (Sec) │ │ └─────────────────┘ └─────────────────┘ │
│ └─────────┘ │ │
├────────────────────────────────┼─────────────────────────────────────────────┤
│ Finesse Servers (HA Pair) │ CX Components │
│ ┌─────────┐ ┌─────────┐ │ ┌─────────────────┐ ┌─────────────────┐ │
│ │Primary │◄►│Secondary│ │ │ cisco-connector │ │ cisco-connector │ │
│ │Finesse │ │Finesse │ │ │ (Node-A) │ │ (Node-B) │ │
│ └─────────┘ └─────────┘ │ │ │ │ │ │
│ │ │ JTAPI listener │ │ JTAPI listener │ │
│ │ │ Consul lock=✓ │ │ Consul lock=✗ │ │
│ │ └─────────────────┘ └─────────────────┘ │
│ │ │ │ │
│ │ └─────┬──────┘ │
│ │ ▼ │
│ │ ┌─────────────┐ │
│ │ │ Consul │ │
│ │ │ Server │ │
│ │ └─────────────┘ │
├────────────────────────────────┼─────────────────────────────────────────────┤
│ Docker Swarm Cluster │ │
│ ┌─────────┐ ┌─────────┐ │ │
│ │ Manager │◄►│ Worker │ │ │
│ │• Mixer │ │• Mixer │ │ │
│ │• REST │ │• Archival│ │ │
│ └─────────┘ └─────────┘ │ │
└────────────────────────────────┴─────────────────────────────────────────────┘
---
17.11 Pause/Resume EFCX Call Recording
Source: Confluence Pause/Resume EFCX Call Recording (VRS space)
EFCX supports selective recording — the ability to pause and resume recording during a call based on business rules (e.g., PCI-DSS compliance for credit card collection, or agent-initiated privacy holds).
17.11.1 How Pause/Resume Works
The pause/resume mechanism operates at the FreeSWITCH ESL layer, not at the phone level. This means it works uniformly across all call types (inbound, outbound, consult, transfer) regardless of the endpoint device.
Agent triggers pause/resume
│
▼ (via AgentDesk or API)
CX Unified Admin / CCM
│
▼ (CIM message or API call)
Voice Connector (ecx_generic_connector)
│
▼ (ESL command)
FreeSWITCH
│
├─► Pause: uuid_record <uuid> stop
│ OR uuid_broadcast <uuid> pause_recording
│
└─► Resume: uuid_record <uuid> start <file>
OR uuid_broadcast <uuid> resume_recording
│
▼
Recording file contains audio ONLY for non-paused segments
Key ESL Commands:
|
Action |
ESL Command |
Description |
|---|---|---|
|
Pause |
|
Stops writing to the recording file |
|
Resume |
|
Resumes writing to the same or new file |
|
Mask via broadcast |
|
Sends internal pause signal |
|
Resume via broadcast |
|
Sends internal resume signal |
17.11.2 FreeSWITCH Dialplan Configuration for Pause/Resume
Add bind_meta_app actions to the recording dialplan to enable DTMF-triggered pause/resume:
<!-- In the hairpin_and_record extension -->
<extension name="hairpin_and_record">
<condition field="${sip_h_X-Return-Token}" expression="^(.+)$">
<action application="export" data="sip_h_X-Return-Token=${sip_h_X-Return-Token}" />
<action application="export" data="_nolocal_jitterbuffer_msec=100"/>
<action application="set" data="RECORD_STEREO=true"/>
<action application="record_session" data="/tmp/mytestingfile.wav"/>
<!-- DTMF-triggered pause/resume -->
<action application="bind_meta_app" data="2 ab s lua::prtask_pause.lua"/>
<action application="bind_meta_app" data="3 ab s lua::prtask_resume.lua"/>
<action application="bridge" data="sofia/external/${destination_number}@${network_addr}"/>
</condition>
</extension>
Lua Pause Script (/usr/share/freeswitch/scripts/prtask_pause.lua):
local filename = "/tmp/mytestingfile.wav";
freeswitch.consoleLog("INFO", "==============================================Pausing recording")
session:execute("record_session_pause", filename)
Lua Resume Script (/usr/share/freeswitch/scripts/prtask_resume.lua):
local filename = "/tmp/mytestingfile.wav";
freeswitch.consoleLog("INFO", "==============================================Resuming recording")
session:execute("record_session_resume", filename)
Usage:
- Press *2 from any endpoint to pause recording
- Press *3 from any endpoint to resume recording
- Run reloadxml in fs_cli after dialplan changes
17.11.3 Programmatic Pause/Resume via ESL
The Voice Connector (ecx_generic_connector) can programmatically control recording via the mod_event_socket interface:
// Pseudo-code from VcService flow
String uuid = session.getVariable("uuid");
// Pause recording
eslConnection.sendCommand("uuid_record " + uuid + " stop");
// Resume recording (same file)
eslConnection.sendCommand("uuid_record " + uuid + " start /app/files/wav/" + filename);
```
CCM Integration:
A PAUSE_RECORDING or RESUME_RECORDING intent can be sent from CCM to the Voice Connector via the standard CIM message bus. The connector translates this to the appropriate ESL command.
17.11.4 Compliance Considerations
|
Regulation |
Requirement |
How Pause/Resume Helps |
|---|---|---|
|
PCI-DSS |
Do not record CVV, PIN, or full card numbers |
Pause before sensitive data collection; resume after |
|
GDPR |
Right to privacy during call |
Agent or customer can request recording pause |
|
HIPAA |
PHI must not be recorded without consent |
Pause for verbal exchange of medical identifiers |
|
Internal Policy |
Agent break/personal discussion |
Agent-initiated pause during non-business segments |
Audit Trail: When pause/resume is triggered, the system should log:
- Timestamp of pause and resume events
- Agent/extension who triggered it
- Reason code (if provided)
- Resulting recording file(s) and their durations
---
17.12 Screen Recording for Cisco Configuration and Deployment Guide
Source: Confluence Screen Recording for Cisco Configuration and Deployment Guide (VRS space)
Screen recording complements voice recording by capturing the agent's desktop activity during a call. This is essential for quality management, compliance verification, and agent coaching.
17.12.1 Architecture Overview
Agent Workstation
│
├─► Cisco Phone (BiB) ──► Voice Recording Stream ──► FreeSWITCH / Eleveo
│
└─► Desktop
│
├─► Finesse Agent Desktop (browser)
│ └── Screen Recording Gadget (JavaScript)
│
└─► Screen Recording Controller (Controller.exe)
│
▼
Local Screen Capture
│
▼
SFTP Upload ──► Recording Server (/app/files/screen/)
│
▼
CX Recording Middleware
│
▼
CCM Third Party Activity (SCREEN_RECORDING)
17.12.2 Finesse Screen Recording Gadget
The Finesse gadget is a JavaScript widget embedded in the Cisco Finesse agent desktop. It:
-
Detects when the agent is on an active call (via Finesse event API)
-
Signals the local
Controller.exeto start/stop screen capture -
Displays recording status to the agent
-
Submits metadata (call ID, agent ID, timestamp) alongside the recording
Gadget Configuration:
<!-- Finesse desktop layout XML snippet -->
<gadgets>
<gadget>
<url>https://<recording-server>/screen-recording-gadget/gadget.xml</url>
<height>100</height>
<title>Screen Recording</title>
</gadget>
</gadgets>
17.12.3 Screen Recording Controller (Controller.exe)
The Controller is a Windows-native application installed on each agent workstation.
Responsibilities:
|
Function |
Description |
|---|---|
|
Screen Capture |
Captures the agent's primary monitor at configurable FPS (default: 5-10 FPS) |
|
Audio Sync |
Optional — can capture agent-side audio for lip-sync with screen |
|
File Encoding |
Encodes capture to MP4 or AVI with H.264 codec |
|
SFTP Upload |
Uploads completed files to the recording server via SFTP |
|
PowerShell Config |
Reads configuration from a local PowerShell config file |
PowerShell Configuration Script (deployed per workstation):
# screen-recording-config.ps1
$Config = @{
ServerAddress = "192.168.1.100"
SFTPPort = 22
SFTPUsername = "screenuser"
SFTPKeyPath = "C:\\ScreenRec\\keys\\id_rsa"
UploadPath = "/app/files/screen/"
CaptureFPS = 5
VideoCodec = "H.264"
Resolution = "1920x1080"
StartOnCallAnswer = $true
StopOnCallEnd = $true
LocalBufferPath = "C:\\ScreenRec\\buffer\\"
}
Export-ModuleMember -Variable Config
17.12.4 SFTP Server Setup
The recording server hosts an SFTP endpoint for receiving screen recordings:
# On Debian/Ubuntu recording server
sudo apt install openssh-server
sudo groupadd sftpusers
sudo useradd -m -G sftpusers screenuser
sudo mkdir -p /app/files/screen
sudo chown root:root /app
sudo chown screenuser:sftpusers /app/files/screen
sudo chmod 755 /app/files/screen
Configure sshd for chroot SFTP
17.12.5 File Naming & Storage
Screen recordings follow a naming convention linking them to voice recordings:
/screen/
└── <date>/
└── <dialogId>_<agentExtension>_<timestamp>.mp4
Example: 550e8400-e29b-41d4-a716-446655440000_5001_1718000000.mp4
17.12.6 Screen Recording Link Activity
Similar to voice recording links, screen recordings are pushed to CCM:
// CIM Message for screen recording
{
"header": {
"intent": "SCREEN_RECORDING",
"sender": { "type": "CONNECTOR", "senderName": "CX-Screen-Recording-Link" },
"channelSessionId": "<channelSessionId>"
},
"body": {
"legId": "<legId>",
"screenRecordingUrl": "https://middleware/{legId}/screen-recording-file",
"durationSeconds": 245,
"resolution": "1920x1080"
}
}
17.12.7 Bandwidth & Storage Considerations
|
Parameter |
Estimate |
Formula |
|---|---|---|
|
Screen recording bitrate |
~500 Kbps - 2 Mbps |
Depends on resolution, FPS, and motion |
|
Per-agent bandwidth |
~1 Mbps upstream |
SFTP upload during call |
|
Storage per hour (5 FPS, 1080p) |
~500 MB - 1 GB |
H.264 compressed |
|
Concurrent agents |
N × 1 Mbps |
Plan SFTP server NIC accordingly |
Example: 100 agents, 8 hours/day, 5 days/week:
- Upload bandwidth: 100 × 1 Mbps = 100 Mbps upstream
- Weekly storage: 100 × 8 × 5 × 750 MB = ~3 TB/week
- Recommendation: Dedicated SFTP server with 1 Gbps NIC and 50+ TB NAS
---
17.13 BiB Compatible Cisco Phones
Source: Confluence BIB compatible cisco phones (CT/VRS space)
Built-in-Bridge (BiB) is a Cisco phone feature that creates a forked RTP stream of the call audio for recording purposes. BiB is the foundation of Cisco-native recording (used by Eleveo and other Cisco-compatible recorders).
17.13.1 How BiB Works
Customer ──► CUCM ──► Agent Phone
│
├─► Normal audio path (agent headset/speaker)
│
└─► BiB fork ──► Recording stream ──► Eleveo Recorder
(separate RTP stream)
When BiB is enabled on a phone:
1. The phone creates a second RTP session (the BiB leg)
2. This leg carries a copy of the mixed audio (agent + customer)
3. CUCM routes the BiB leg to the configured recording server (SIP trunk)
4. The recording server receives and stores the audio
17.13.2 BiB-Compatible Phone Models
The following Cisco IP Phone models support Built-in-Bridge for recording. Phones running SCCP or SIP firmware are both supported, but SIP is preferred for newer deployments.
|
Model |
Protocol |
BiB Support |
Notes |
|---|---|---|---|
|
Cisco 7821 |
SIP |
✅ |
Entry-level, no video |
|
Cisco 7841 |
SIP |
✅ |
Standard agent phone |
|
Cisco 7861 |
SIP |
✅ |
Extended function keys |
|
Cisco 8811 |
SIP |
✅ |
Gigabit Ethernet |
|
Cisco 8841 |
SIP |
✅ |
Color display |
|
Cisco 8845 |
SIP |
✅ |
Video capable |
|
Cisco 8851 |
SIP |
✅ |
Bluetooth, USB |
|
Cisco 8861 |
SIP |
✅ |
Wi-Fi option |
|
Cisco 8865 |
SIP |
✅ |
Premium video |
|
Cisco 8941 |
SIP |
✅ |
Legacy model |
|
Cisco 8945 |
SIP |
✅ |
Legacy model |
|
Cisco 8961 |
SIP |
✅ |
Legacy premium |
|
Cisco 9951 |
SIP |
✅ |
Executive |
|
Cisco 9971 |
SIP |
✅ |
Executive video |
|
Cisco IP Communicator |
SCCP/SIP |
✅ |
Soft phone |
|
Cisco Jabber |
SIP |
✅ |
Soft client |
|
Cisco 7942G |
SCCP/SIP |
✅ |
Legacy, EoS |
|
Cisco 7945G |
SCCP/SIP |
✅ |
Legacy, EoS |
|
Cisco 7962G |
SCCP/SIP |
✅ |
Legacy, EoS |
|
Cisco 7965G |
SCCP/SIP |
✅ |
Legacy, EoS |
|
Cisco 7975G |
SCCP/SIP |
✅ |
Legacy, EoS |
|
Cisco 7937G |
SCCP |
✅ |
Conference station |
|
Cisco 8831 |
SIP |
✅ |
Conference phone |
|
Cisco 8845-K9 |
SIP |
✅ |
Specific SKU |
|
Cisco 8851-K9 |
SIP |
✅ |
Specific SKU |
|
Cisco 8865-K9 |
SIP |
✅ |
Specific SKU |
Total: 25+ officially supported models. Newer 8800 series and 7800 series phones are the current recommendation.
17.13.3 Enabling BiB on CUCM
Per-phone configuration in CUCM Administration:
-
Navigate to Device → Phone
-
Find the agent's phone
-
Set Built In Bridge =
On -
Set Recording Option =
AutomaticorSelective -
Set Recording Profile to the recording server SIP trunk
-
Save and apply configuration
Phone Configuration XML (bulk provisioning):
<device>
<phoneConfig>
<builtInBridgeEnable>1</builtInBridgeEnable>
<recordingOption>Automatic</recordingOption>
<recordingProfile>
<recordingCss>
<partition>PT-Recording</partition>
</recordingCss>
<destination>recording-server.example.com</destination>
</recordingProfile>
</phoneConfig>
</device>
17.13.4 BiB vs. SPAN vs. SIPREC
|
Method |
Recording Point |
Pros |
Cons |
|---|---|---|---|
|
BiB |
Phone |
Per-phone granularity; works with any CUCM version |
Requires compatible phones; phone CPU load |
|
SPAN |
Network switch |
No phone dependency; captures all calls |
Requires mirror port; no per-agent granularity |
|
SIPREC |
CUBE/SBC |
Standards-based; carrier-grade |
Requires CUBE/SBC; more complex config |
|
CX SIP Proxy Fork |
Media Server |
Integrated with CX routing |
Requires CX Media Server in call path |
Recommendation: Use BiB for Cisco UCCE/UCCX environments with compatible endpoints. Use SIPREC if the customer has a CUBE or third-party SBC. Use CX SIP Proxy Fork for pure CX-native deployments.
---
17.14 Solution Prerequisites & Hardware Sizing
Source: Confluence Solution Prerequisites (VRS space)
Before deploying the CX Voice Recording Solution with Cisco, verify all hardware, software, and network prerequisites.
17.14.1 Recording Server Hardware Sizing
Formula for Storage Planning:
Total Daily Storage (GB) = Concurrent Calls × Hours/Day × Recording Bitrate (Mbps) × 3600 × 0.125
Where:
• Concurrent Calls = Expected peak simultaneous recorded calls
• Recording Bitrate = ~64 kbps (mono, 8 kHz, G.711) to ~128 kbps (stereo WAV)
• 0.125 = Conversion factor (Mbps → MB/s → GB/hour)
```
Storage Calculation Example:
|
Parameter |
Value |
|---|---|
|
Agents |
500 |
|
Peak concurrency |
70% = 350 calls |
|
Hours/day |
12 |
|
Bitrate |
128 kbps (stereo WAV) = 0.128 Mbps |
|
Daily storage |
350 × 12 × 0.128 × 3600 × 0.125 = 241.9 GB/day |
|
Weekly storage |
241.9 × 7 = ~1.7 TB/week |
|
Monthly storage |
241.9 × 30 = ~7.3 TB/month |
|
Annual storage |
241.9 × 365 = ~88 TB/year |
|
With redundancy (RAID 10) |
~176 TB raw capacity |
Retention Planning:
|
Retention Period |
Required Capacity (500 agents, 12h/day) |
|---|---|
|
30 days |
7.3 TB |
|
90 days |
22 TB |
|
1 year |
88 TB |
|
3 years |
264 TB |
|
7 years (financial compliance) |
616 TB |
17.14.2 Recording Server Specifications
|
Component |
Minimum |
Recommended |
HA Pair |
|---|---|---|---|
|
OS |
Debian 12 (Bookworm) |
Debian 12 |
Debian 12 |
|
CPU |
4 cores |
8+ cores |
8+ cores each |
|
RAM |
8 GB |
16 GB |
16 GB each |
|
Disk |
500 GB SSD |
1 TB NVMe SSD |
Shared NAS/SAN |
|
Network |
1 Gbps |
10 Gbps |
10 Gbps |
|
Storage backend |
Local disk |
NAS/SAN with RAID 10 |
Shared NAS/SAN (required) |
17.14.3 SQL Server Requirements (UCCE / Historical DB)
|
Parameter |
Requirement |
|---|---|
|
Engine |
Microsoft SQL Server 2016+ or SQL Server Express (for small deployments) |
|
Database |
|
|
Authentication |
SQL Server Authentication (username/password) |
|
Permissions |
Read access to |
|
Port |
1433 (TCP) |
|
Connectivity |
Direct TCP from cisco-connector / QM-connector VMs |
17.14.4 Docker & Container Prerequisites
For deployments using Docker Swarm or Kubernetes (CX 4.8+):
|
Component |
Version |
Notes |
|---|---|---|
|
Docker Engine |
24.0+ |
Required for all containerized services |
|
Docker Compose |
2.20+ |
For single-node deployments |
|
Docker Swarm |
Any (part of Docker Engine) |
For HA orchestration |
|
Kubernetes |
1.28+ |
For CX 4.8+ K8s-native deployments |
|
Helm |
3.12+ |
For QM deployment charts |
|
Container images |
|
Pulled from Expertflow registry |
17.14.5 FusionPBX Prerequisites (EFSwitch Backend)
|
Parameter |
Requirement |
|---|---|
|
FusionPBX |
Latest stable (bundled with CX) |
|
FreeSWITCH |
1.10.7+ with mod_event_socket, mod_lua enabled |
|
Database |
PostgreSQL (FusionPBX internal) |
|
OS |
Debian 12 |
|
SIP Port |
5060 (TCP/UDP) |
|
ESL Port |
8021 (TCP, localhost only) |
|
WebSocket |
7443 (TCP, for WebRTC) |
17.14.6 Time Synchronization (NTP)
Accurate time synchronization is critical for recording correlation. All components must sync to the same NTP source:
# On all Linux servers (Debian/Ubuntu)
sudo apt install chrony
sudo systemctl enable chrony
/etc/chrony/chrony.conf
sudo systemctl restart chrony
```
Components requiring NTP sync:
- CUCM (internal NTP or external source)
- UCCE/UCCX Admin Workstation
- FreeSWITCH / FusionPBX
- cisco-connector VMs
- Recording server
- Agent workstations (for screen recording timestamp alignment)
Maximum allowed drift: < 1 second across all systems. Drift > 5 seconds will cause recording correlation failures.
---
17.15 Port Utilisation
Source: Confluence Port Utilisation (VRS space)
The following network ports must be open for the CX Voice Recording Solution to function. This table covers CUCM, FreeSWITCH, recording servers, databases, and agent endpoints.
17.15.1 Core Recording Ports
|
Protocol |
Port |
Application |
Direction |
Description |
|---|---|---|---|---|
|
TCP |
5060 |
FreeSWITCH / Drachtio |
Inbound/Outbound |
SIP signaling (internal/external) |
|
TCP |
5065 |
FreeSWITCH |
Inbound |
Internal SIP UAS (when Drachtio uses 5060) |
|
TCP |
5080 |
FreeSWITCH |
Inbound |
External SIP UAS |
|
TCP |
5066 |
FreeSWITCH |
Inbound |
WebRTC SIP signaling |
|
TCP/UDP |
5070 |
FreeSWITCH |
Inbound |
NAT profile SIP |
|
TCP |
8021 |
FreeSWITCH |
Localhost + Inbound |
ESL (Event Socket Library) |
|
TCP |
7443 |
FreeSWITCH |
Inbound |
WebSocket (WSS) for WebRTC |
|
UDP |
16384-32768 |
FreeSWITCH |
Inbound/Outbound |
RTP/RTCP multimedia streaming |
|
UDP |
3478-3479 |
FreeSWITCH |
Inbound/Outbound |
STUN service (NAT traversal) |
17.15.2 Cisco Infrastructure Ports
|
Protocol |
Port |
Application |
Direction |
Description |
|---|---|---|---|---|
|
TCP |
2748 |
CUCM / CTI |
Inbound |
CTI (Computer Telephony Integration) |
|
TCP |
2749 |
CUCM / CTI |
Inbound |
CTI-encrypted |
|
TCP |
8443 |
CUCM / AXL |
Inbound |
Administrative XML (AXL) SOAP API |
|
TCP |
8080 |
CUCM / AXL |
Inbound |
AXL (unencrypted, dev only) |
|
TCP |
443 |
CUCM / Finesse |
Inbound |
Finesse REST API, Admin UI |
|
TCP |
6970 |
CUCM / TFTP |
Inbound |
TFTP phone firmware/config |
|
TCP |
1433 |
SQL Server |
Inbound |
UCCE/UCCX historical database |
|
TCP |
80/443 |
Eleveo |
Inbound |
Eleveo Web UI and REST API |
|
TCP |
22 |
Recording Server |
Inbound |
SSH / SFTP (screen recordings) |
17.15.3 CX & Middleware Ports
|
Protocol |
Port |
Application |
Direction |
Description |
|---|---|---|---|---|
|
TCP |
8080 |
cisco-connector |
Inbound |
REST API, health checks |
|
TCP |
8090 |
recording-middleware |
Inbound |
Recording file serving API |
|
TCP |
8091 |
recording-link-activities |
Inbound |
Link activity service API |
|
TCP |
5672 |
RabbitMQ |
Inbound |
AMQP message bus (CIM messages) |
|
TCP |
15672 |
RabbitMQ |
Inbound |
Management UI |
|
TCP |
5432 |
PostgreSQL |
Inbound |
CX internal database |
|
TCP |
6379 |
Redis |
Inbound |
Cache / session store |
|
TCP |
8500 |
Consul |
Inbound |
HA coordination (JTAPI locks) |
|
TCP |
443 |
CX Unified Admin |
Inbound |
HTTPS admin UI |
17.15.4 Firewall Rules Summary
# Example iptables rules for a recording server
sudo iptables -A INPUT -p tcp --dport 5060 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 5065 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 8021 -s 127.0.0.1 -j ACCEPT
sudo iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
sudo iptables-save
---
17.16 Eleveo Recording Middleware Reference
Source: Confluence (4.8) Eleveo Recording Middleware (CX space)
The Eleveo Recording Middleware bridges CX with an existing Eleveo (Zoom Call Recording / Cisco-compatible) recording infrastructure. It is used when the customer already has an Eleveo deployment and wants CX to consume those recordings.
17.16.1 Typical Eleveo Deployment
|
System |
IP |
Version |
UI Credentials |
|---|---|---|---|
|
Eleveo Recorder |
192.168.1.236 |
7.1.5 |
admin/zoomcallrec12345 |
|
Eleveo Replay Server |
192.168.1.237 |
7.1.5 |
admin/zoomcallrec12345 |
|
Eleveo QM |
192.168.1.237 |
7.1.5 |
ccmanager/zoomcallrec12345 |
17.16.2 Integration Flow
CX Unified Admin
│ (configured with ELEVEO backend)
▼
Recording Middleware (ELEVEO profile)
│
├─► Authenticates to Eleveo (JSESSIONID)
├─► Searches conversations by JTAPI_CISCO_ID metadata
├─► Downloads individual call legs
├─► Merges split legs (hold/retrieve/conference)
│
▼
Returns merged audio file to requesting client
17.16.3 Middleware Configuration
# application.yml (recording-middleware)
recording:
backend: ELEVEO
eleveo:
base-url: https://192.168.1.236
username: admin
password: zoomcallrec12345
download-timeout: 30000
17.16.4 Known Limitations
|
Limitation |
Workaround |
|---|---|
|
Eleveo splits recordings on hold/retrieve |
Middleware merges legs using |
|
Eleveo splits recordings on conference |
Middleware merges using |
|
Authentication requires admin UI credentials |
Store credentials in Kubernetes secrets |
|
Eleveo 7.x API changes |
Middleware version-locked to tested Eleveo versions |
---
17.17 Quality Management (QM) Deployment Guide
Source: Confluence (4.8) QM Deployment and Configuration Guide (CX space)
Quality Management (QM) is a CX add-on module that provides conversation evaluation, scoring, and agent coaching. It integrates with the recording infrastructure to allow supervisors to review voice (and optionally screen) recordings and score agent performance.
17.17.1 QM Architecture
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ CX Core │────►│ QM-Backend │────►│ QM-Frontend │
│ (CCM, MRE) │ │ (PostgreSQL)│ │ (Angular UI) │
└──────────────┘ └─────────────┘ └──────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Recording Middleware │
│ (EFSwitch or Eleveo backend) │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Cisco UCCE (awdb) ──► QM-Connector │
│ (reads TCD, ACD for wrap-up codes) │
└─────────────────────────────────────────────┘
17.17.2 Prerequisites
-
EF CX deployed with Cisco voice channel configured
-
Recording backend configured (EFSwitch or Eleveo)
-
CISCO_CC channels created in Unified Admin for both Inbound and Outbound
-
Outbound service identifier set as
UCCE_OB_SERVICE_IDENTIFIERinvalues.yaml -
Routing Mode =
External -
Channel Model =
HYBRID -
Keycloak user
vrscreated with allrealm-managementroles
17.17.3 QM-Connector Environment Variables
|
Variable |
Description |
Example |
|---|---|---|
|
|
Recording Server IP |
|
|
|
CX FQDN |
|
|
|
Webhook for conversation creation |
|
|
|
UCCE DB engine |
|
|
|
UCCE IP |
|
|
|
UCCE DB port |
|
|
|
UCCE |
|
|
|
UCCE DB user |
|
|
|
UCCE DB password |
|
|
|
Outbound service identifier |
|
|
|
Keycloak realm |
|
|
|
Keycloak client |
|
|
|
Keycloak client secret |
|
|
|
Keycloak service user |
|
|
|
Keycloak service password |
|
|
|
Agent role name |
|
|
|
Agent role UUID |
|
|
|
VRS database name |
|
|
|
VRS DB user |
|
|
|
VRS DB password |
|
|
|
VRS DB engine |
|
|
|
VRS DB host |
|
|
|
VRS DB port |
|
|
|
JDBC driver |
|
17.17.4 Deployment Commands
# Add Expertflow Helm repo
helm repo add expertflow https://expertflow.github.io/charts
helm repo update expertflow
Create QM database in Postgres
kubectl -n ef-external exec -it ef-postgresql-client -- bash
psql --host ef-postgresql -U sa postgres -p 5432
CREATE DATABASE qm_db;
\c qm_db;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
\q
Fetch and customize values
Deploy QM
17.17.5 Keycloak QM Roles
Two new roles are introduced by QM:
|
Role |
Permissions |
|---|---|
|
Quality Manager |
Access Reviews List, Schedules, Conversation List |
|
Evaluator |
Access Reviews List only |
Import sequence:
1. Partial import quality-management-realm.json to create roles
2. Import quality-management-authz.json into the cim client Authorization tab
17.17.6 QM Limitations (as of CX 4.8)
|
Limitation |
Detail |
|---|---|
|
Wrap-up code search |
Must be added manually if not found in search |
|
Cisco wrap-up codes |
Must exist in Unified-Admin wrap-up codes for schedules/filters |
|
Conference calls |
Not available for inbound or outbound |
|
Consult calls |
Not available for outbound |
|
Consult→Transfer gap |
Consulted call transferred to next agent may not create a conversation |
|
Agent deletion |
Restricted when linked to QM evaluations — see Agent Team docs |
---
Appendix: Repository Locations
All referenced source code is available in the following repositories:
|
Repository |
Path |
Description |
|---|---|---|
|
|
|
Cisco JTAPI integration, event correlation, CIM generation |
|
|
|
Legacy JTAPI event listener |
|
|
|
Recording file serving (EFSwitch/Eleveo) |
|
|
|
Pushes recording links to CCM |
|
|
|
Voice Connector (FreeSWITCH ↔ CCM bridge) |
|
|
|
FreeSWITCH Lua scripts for call flows |
|
|
|
CCM (Conversation Manager) |
|
|
|
Task/queue/agent routing |
|
|
|
Agent Desktop Angular applications |
---
Document generated from: CX Voice System Overview Guide (Confluence) + Cisco Configuration docs (VRS, CX spaces) + production source code analysis
Date: 2026-06-08
Version: 2.0 (Expanded with Cisco & EFCX Recording Profiling)
Author: AI-assisted technical documentation
## 18. Complete Behavioral Reference
This section documents every behavioral detail of the CX Voice + Cisco system, organized by component. It covers state machines, transition rules, edge cases, and recovery behaviors for both Cisco-side and EFCX-side components.
---
18.1 Agent State Machine & MRD Behavior
18.1.1 Cisco Finesse Agent States
Cisco Finesse maintains an independent agent state machine. The CX AgentDesk subscribes to Finesse events via BOSH (Bidirectional-streams Over Synchronous HTTP) and maps them to CX MRD (Media Routing Domain) states.
Finesse State Machine:
LOGOUT → LOGIN → NOT_READY → READY → RESERVED → TALKING → WORK → READY
↓ ↓
RESERVED_OUTBOUND WORK_READY
↓
TALKING (outbound)
Finesse States and Meanings:
|
Finesse State |
Description |
CX MRD Mapping |
|---|---|---|
|
|
Agent not logged in |
NOT_READY |
|
|
Agent logged in but not accepting calls |
NOT_READY |
|
|
Agent available for inbound calls |
READY |
|
|
Agent selected for inbound call, ringing |
RESERVED |
|
|
Agent selected for outbound preview call |
RESERVED |
|
|
Agent on active call |
BUSY |
|
|
After-call work (wrap-up) |
NOT_READY |
|
|
After-call work, then auto-ready |
NOT_READY → READY |
|
|
Call on hold |
BUSY |
18.1.2 CX MRD State Machine
The CX Routing Engine maintains its own MRD state per agent, synchronized with Finesse states.
NOT_READY ←──────┬──────→ READY
↑ │ ↓
└────────────┘ RESERVED
↓
BUSY
↓
NOT_READY (after wrap-up)
State Transitions:
|
From |
To |
Trigger |
Source |
|---|---|---|---|
|
|
|
Agent logs in |
AgentDesk → Finesse → CX |
|
|
|
Agent clicks "Make Ready" |
AgentDesk → Finesse → CX |
|
|
|
Agent clicks "Make Not Ready" |
AgentDesk → Finesse → CX |
|
|
|
Routing Engine assigns inbound task |
Routing Engine → CCM → AgentDesk |
|
|
|
Routing Engine assigns outbound preview |
Routing Engine → CCM → AgentDesk |
|
|
|
Agent answers call |
sip-wrapper.js → Angular → CX |
|
|
|
RONA (no answer) |
FreeSWITCH → Voice Connector → CCM → Routing Engine |
|
|
|
Call ends + wrap-up required |
sip-wrapper.js → Angular → CX |
|
|
|
Call ends + auto-ready enabled |
sip-wrapper.js → Angular → CX |
|
|
|
Consult/transfer (still on call) |
sip-wrapper.js → Angular → CX |
18.1.3 Hardcoded MRD and Channel Type
Critical Implementation Detail: The AgentDesk hardcodes the voice MRD name as "VOICE" (for pre-CX4.2) or "CISCO_CC" (for CX4.2+). This mapping is configured in the AgentDesk ConfigMap:
# ef-unified-agent-configmap.yaml
isCiscoEnabled: "true"
# The MRD name must match what's configured in Unified Admin
# For CX < 4.2: "VOICE"
# For CX >= 4.2: "CISCO_CC"
Behavioral Impact:
- If the MRD name in Unified Admin does not match the hardcoded value in AgentDesk, the agent state updates will fail silently
- The AgentDesk identifies active voice channel sessions by the "VOICE" channel type name
- On leaving a conversation with an active voice channel session, the client must send the hangup command to the correct voice platform
18.1.4 Agent State Change Flow
Agent clicks "Make Ready" in UI
│
▼
Angular emits changeAgentState event
│
▼
cti.service.ts sends state change to Finesse via BOSH
PUT /finesse/api/User/{agentId}
{ state: "READY" }
│
▼
Finesse updates agent state in CUCM
│
▼
Finesse publishes state change via XMPP BOSH
<Update>
<user>
<state>READY</state>
<stateChangeTime>2026-06-08T10:00:00.000Z</stateChangeTime>
</user>
</Update>
│
▼
AgentDesk cti.service.ts receives XMPP update
│
▼
Maps Finesse state to CX MRD state
│
▼
Emits agentStateChanged event to Angular
│
▼
Angular updates UI (Ready button green, timer starts)
│
▼
Sends AGENT_STATE_CHANGED to CCM via REST API
POST /conversation-manager/channel-session/agent-state
{ agentId, mrdId, state: "READY" }
│
▼
CCM updates agent MRD state
│
▼
Routing Engine receives AGENT_STATE_CHANGED event
→ Agent is now eligible for task assignment
---
18.2 Complete Call Control Behaviors
18.2.1 Hold Call
Trigger: Agent clicks "Hold" button in Agent Desktop UI.
Cisco-side Behavior:
```
Agent clicks HOLD
│
▼
sip.service.ts → postMessages({action: "holdCall", parameter: {dialogId}})
│
▼
sip-wrapper.js hold_call(dialogId):
1. Finds session in calls[] by dialogId
2. Calls session.invite({
requestDelegate: {
onAccept: () => { / hold accepted / }
}
})
3. SIP re-INVITE sent to FreeSWITCH with SDP indicating hold
(sendonly/inactive direction in SDP)
│
▼
FreeSWITCH receives re-INVITE
→ Bridges customer to hold music (MOH)
→ Agent leg is parked
│
▼
JTAPI event: CallCtlTermConnHeldEv
→ Cisco Connector receives event
→ Persists to jtapi_event table
→ CorrelationService processes → creates CALL_HOLD CIM
│
▼
Cisco Connector sends CALL_HOLD to CCM
│
▼
CCM updates channel session state
│
▼
Angular UI shows "On Hold" with resume timer
```
EFCX-side Recording Behavior:
- FreeSWITCH uuid_record continues writing to file
- Hold music (MOH) audio is mixed into the recording
- For Eleveo: BiB continues streaming; hold music is recorded
- For EFSwitch: The recording file contains the hold music segment
Post-Hold State:
| Component | State After Hold |
|---|---|
| Finesse | TALKING (unchanged) |
| CX MRD | BUSY (unchanged) |
| SIP.js session | Established (re-INVITE with hold SDP) |
| FreeSWITCH bridge | Customer → MOH, Agent leg parked |
| Recording | Continues (includes MOH) |
18.2.2 Retrieve Call (Resume from Hold)
Trigger: Agent clicks "Resume" button.
Agent clicks RESUME
│
▼
sip.service.ts → postMessages({action: "retrieveCall", parameter: {dialogId}})
│
▼
sip-wrapper.js retrieve_call(dialogId):
1. Finds session in calls[]
2. Sends re-INVITE with normal SDP (sendrecv)
│
▼
FreeSWITCH receives re-INVITE
→ Unparks agent leg
→ Re-bridges customer and agent
→ Stops MOH
│
▼
JTAPI event: CallCtlTermConnTalkingEv
→ Cisco Connector → CALL_RESUME CIM → CCM
│
▼
Angular UI shows "On Call" (hold timer stops)
Recording Implications:
- EFSwitch: Recording continues seamlessly in same file
- Eleveo: Recording may split on hold/retrieve (middleware merges legs)
18.2.3 Blind Transfer (Single Step Transfer - SST)
Trigger: Agent selects transfer target and clicks "Transfer" without consulting.
Agent A (1005) is on call with Customer
│
▼
Agent A clicks Transfer → selects Agent B (1006)
│
▼
sip.service.ts → postMessages({action: "transferCall", parameter: {dialogId, to: "1006"}})
│
▼
sip-wrapper.js transfer_call(dialogId, target):
1. Finds session in calls[]
2. SIP REFER sent to FreeSWITCH
Refer-To: <sip:1006@expertflow>
Referred-By: <sip:1005@expertflow>
│
▼
FreeSWITCH processes REFER:
1. Receives REFER from Agent A
2. Sends 202 Accepted to Agent A
3. Initiates new INVITE to Agent B on behalf of Customer
4. When Agent B answers, bridges Customer to Agent B
5. Hangs up Agent A leg
│
▼
Agent A receives NOTIFY: sipfrag;status=200
→ sip-wrapper.js: dialog.state = DROPPED
→ callback to Angular
│
▼
Angular: handleDialogDroppedEvent()
→ CALL_LEG_ENDED sent to CCM
│
▼
CCM: Conversation remains active (new leg started)
│
▼
JTAPI events:
- ConnDisconnectedEv on Agent A leg
- ConnConnectedEv on Agent B leg
- CiscoTransferStartEv
│
▼
Cisco Connector correlates events
→ CallType = DIRECT_TRANSFER
→ Creates CALL_LEG_ENDED for Agent A
→ Creates CALL_LEG_STARTED for Agent B
Key Behavioral Details:
- The customer experiences a brief silence/blind transfer (no consult)
- Agent A is immediately dropped from the call
- If Agent B does not answer, the call may go to voicemail or RONA
- Recording: For EFSwitch, the recording continues with the same file (same UUID)
- For Eleveo, a new recording leg is created for Agent B
18.2.4 Consult Transfer (Two-Step Transfer)
Trigger: Agent clicks "Consult" → dials target → talks → clicks "Complete Transfer".
Phase 1: Consult Call Initiation
```
Agent A on call with Customer
│
▼
Agent A clicks Consult → enters Agent B extension
│
▼
sip.service.ts → postMessages({action: "consultCall", parameter: {dialogId, consultTo: "1006"}})
│
▼
sip-wrapper.js consult_call(dialogId, target):
1. Creates new SIP.Inviter for consult call
2. Sets SIP headers:
- X-Calltype: CONSULT
- X-Consult-Parent-Dialog: <originalDialogId>
3. Sends INVITE to FreeSWITCH
│
▼
FreeSWITCH processes consult INVITE
→ Creates new B-leg for consult call
→ Customer is put on hold (or remains bridged to Agent A)
│
▼
Agent B receives consult call INVITE
→ Dialog shows "Consult from 1005"
│
▼
JTAPI events:
- CiscoConsultCallActiveEv (new consult call)
- CallCtlTermConnHeldEv on original call (if customer put on hold)
```
Phase 2: Consult Call Active
```
Agent B answers consult call
│
▼
Agent A and Agent B are talking
Customer is on hold (hearing MOH)
│
▼
JTAPI: ConnConnectedEv on consult leg
```
Phase 3: Complete Transfer
```
Agent A clicks "Complete Transfer"
│
▼
sip.service.ts → postMessages({action: "completeTransfer", parameter: {dialogId, consultDialogId}})
│
▼
sip-wrapper.js:
1. Sends BYE to consult leg (Agent A ↔ Agent B)
2. SIP REFER on original leg to transfer customer to Agent B
│
▼
FreeSWITCH:
1. Bridges Customer to Agent B
2. Hangs up Agent A
│
▼
Customer is now connected to Agent B
Agent A is dropped
│
▼
JTAPI events:
- CiscoTransferStartEv
- ConnDisconnectedEv on Agent A leg
- ConnConnectedEv on Agent B leg (now primary)
│
▼
Cisco Connector:
→ CallType = CONSULT_TRANSFER
→ Original leg ended (Agent A)
→ New leg started (Agent B)
→ Consult leg is closed
```
Key Behavioral Details:
- The consult call is a separate SIP dialog with its own UUID
- FreeSWITCH may or may not put the customer on hold during consult
- If Agent B rejects the consult, Agent A can click "Cancel Consult" and return to the customer
- Recording: For EFSwitch, consult calls may have separate recording files
- For Eleveo, consult audio is typically NOT recorded (BiB only records primary call)
18.2.5 Conference
Trigger: Agent clicks "Conference" → dials participant → clicks "Merge".
Phase 1: Conference Initiation
```
Agent A on call with Customer
│
▼
Agent A clicks Conference → dials Agent B
│
▼
Same as consult call initiation
→ New consult leg created
→ Customer on hold
```
Phase 2: Merge (Conference Active)
```
Agent A clicks "Merge"
│
▼
FreeSWITCH creates three-way bridge:
Customer ↔ Agent A ↔ Agent B
│
▼
All three parties can hear each other
│
▼
JTAPI events:
- CiscoConferenceStartEv
- All legs linked under same conference callId
│
▼
Cisco Connector:
→ CallType = CONFERENCE
→ All conference participants linked to same conversation
```
Phase 3: Conference End
```
Any participant hangs up
│
▼
If Customer hangs up:
→ Conference ends for all
→ All agents dropped
│
▼
If Agent A hangs up:
→ Agent B and Customer remain connected
→ Conference becomes regular two-party call
│
▼
JTAPI: CiscoConferenceEndEv
```
Recording Behavior:
- EFSwitch: Records the entire conference as a single file (mixed audio)
- Eleveo: Each agent's BiB records separately; middleware must merge
---
18.3 CCM Conversation Lifecycle
18.3.1 Conversation State Machine
The CCM (Conversation Manager) maintains a conversation object that represents the entire customer interaction across all channels.
CREATED → ACTIVE → CLOSED
↓ ↓ ↓
(queued) (talking) (ended)
State Definitions:
|
State |
Description |
Entry Trigger |
Exit Trigger |
|---|---|---|---|
|
|
Conversation initialized, no active legs |
ASSIGN_RESOURCE_REQUESTED received |
First CALL_LEG_STARTED |
|
|
At least one leg is active |
CALL_LEG_STARTED |
All legs ended + wrap-up complete |
|
|
All legs ended, conversation complete |
All channel sessions ended |
— |
18.3.2 Channel Session Lifecycle
Each voice call creates a channel session within the conversation:
INITIATED → ALERTING → ACTIVE → ENDED
↓ ↓ ↓ ↓
(call (ringing (talking (hangup
created) on agent) started) complete)
Channel Session State Transitions:
|
From |
To |
Trigger Event |
CIM Intent |
|---|---|---|---|
|
— |
|
ASSIGN_RESOURCE_REQUESTED |
— |
|
|
|
CALL_ALERTING |
|
|
|
|
CALL_LEG_STARTED |
|
|
|
|
CALL_HOLD |
|
|
|
|
CALL_RESUME |
|
|
|
|
CALL_LEG_ENDED |
|
|
|
|
CALL_LEG_ENDED (RONA) |
|
18.3.3 Conversation Creation Flow
Voice Connector sends ASSIGN_RESOURCE_REQUESTED
│
▼
CCM receives CIM message
│
▼
CCM.ActivityService.createConversation():
1. Creates Conversation object
2. Generates conversationId (UUID)
3. Sets channelType = "VOICE" / "CISCO_CC"
4. Sets customerIdentifier = callingNumber
5. Sets serviceIdentifier = DN
6. Creates ChannelSession:
- channelSessionId = UUID
- channelType = VOICE
- state = INITIATED
- direction = INBOUND/OUTBOUND
7. Persists to database
│
▼
CCM sends AssignResourceRequest to Routing Engine
│
▼
Routing Engine creates Task and enqueues
18.3.4 Conversation Closure Flow
CCM receives CALL_LEG_ENDED
│
▼
CCM.ActivityService.processLegEnded():
1. Finds channel session by callId
2. Updates channelSession.state = ENDED
3. Checks if ALL channel sessions are ended
│
▼
If all sessions ended:
1. Updates Conversation.state = CLOSED
2. Calculates total duration
3. Publishes CONVERSATION_ENDED event
4. Triggers wrap-up flow if configured
│
▼
Routing Engine receives CONVERSATION_ENDED
→ TaskManager.changeStateOnMediaClose()
→ Updates agent MRD state
Wrap-up Behavior:
- If autoWrapUp = true in MRD config: Agent goes to WORK state for configured duration
- If autoWrapUp = false: Agent immediately becomes READY (if no other tasks)
- Wrap-up timer is enforced by Routing Engine
- During wrap-up, agent cannot receive new tasks
---
18.4 Routing Engine Task State Machine
18.4.1 Task Lifecycle
The Routing Engine manages tasks that represent work items to be assigned to agents.
QUEUED → RESERVED → ACTIVE → COMPLETED
↓ ↓ ↓ ↓
(waiting (agent (agent (work
in queue) selected) talking) done)
Task State Definitions:
|
State |
Description |
Timeout Behavior |
|---|---|---|
|
|
Task in queue, waiting for agent |
Queue timeout may trigger overflow/IVR |
|
|
Agent selected, waiting for answer |
RONA timeout → task revoked |
|
|
Agent answered, call in progress |
Call timer, max call duration |
|
|
Call ended, work complete |
— |
|
|
Task cancelled (RONA, transfer, system) |
— |
18.4.2 Task Media State Machine
Each task has a TaskMedia object representing the voice media:
QUEUED → RESERVED → ACTIVE → ENDED
TaskMedia State Transitions:
|
From |
To |
Trigger |
Action |
|---|---|---|---|
|
— |
|
Task created |
Add to queue |
|
|
|
Agent found |
Send AGENT_RESERVED to CCM |
|
|
|
Agent answers |
Update agent MRD to BUSY |
|
|
|
RONA / Cancel |
Revoke task, set agent NOT_READY (RONA only) |
|
|
|
Call ended |
Close conversation, update agent state |
18.4.3 RONA Behavior in Detail
When RONA (Ring No Answer) occurs:
FreeSWITCH originate to Agent times out
│
▼
FreeSWITCH sends CANCEL to Agent Desktop
│
▼
Path A: Agent Desktop
- sip-wrapper.js onCancel fires
- callEndReason = "NO_ANSWER"
- stateChange → Terminated
- dialog.state = DROPPED
- Agent Desktop does NOT send CALL_LEG_ENDED to CCM
- UI clears call notification
│
▼
Path B: FreeSWITCH Backend
- vcApi.lua rona branch executes
- hup_cause = "RONA"
- cancel_agent() → POST /cancel-agent to Voice Connector
│
▼
Voice Connector → CCM
- CIM: intent = CANCEL_RESOURCE_REQUESTED
- reasonCode = "RONA"
│
▼
CCM → Routing Engine
- CancelResourceRequestedEvent.handle()
- DELETE /tasks/conversation/{id}/direction/INBOUND/voice/RONA
│
▼
Routing Engine:
1. TasksService.revokeVoiceResourceByDirection()
2. Finds task by conversationId + INBOUND
3. Checks media.state == RESERVED
4. Sets agentStateToBeUpdatedToNotReady = true
5. taskManager.revokeInProcessVoiceTask(t, false, media)
→ Task state = REVOKED
→ TaskMedia state = ENDED
6. If RONA: agentStateService.agentState(NOT_READY)
→ Agent MRD = NOT_READY
│
▼
FreeSWITCH requeues:
- vcApi.lua calls request_agent()
- Creates NEW task (old one is closed)
- Customer hears hold music
Critical Behavioral Detail: The original task is REVOKED (deleted), not requeued. A completely new task is created with a new taskId. This means:
- The original conversation may be closed if no other media
- Call history shows the RONA event
- Agent state goes to NOT_READY (must manually make ready)
---
18.5 SIP.js State Machine (Agent Desktop)
18.5.1 Complete State Diagram
┌─────────────┐
INVITE → │ ALERTING │ ← onInvite (inbound)
│ (ringing) │
└──────┬──────┘
│ accept()
▼
┌─────────────┐
hold → │ ACTIVE │ ← onAccept (outbound)
retrieve→ │ (talking) │
└──────┬──────┘
│ bye() / onBye
▼
┌─────────────┐
│ DROPPED │
│ (ended) │
└─────────────┘
18.5.2 Dialog Object State Transitions
The dialog object in sip-wrapper.js tracks call state:
|
Event |
dialog.state |
participants[0].state |
participants[0].actions |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18.5.3 SIP.js Session States
Internally, SIP.js tracks session state:
Initial → Establishing → Established → Terminated
|
SIP.js State |
Meaning |
UI State |
|---|---|---|
|
|
Session created, no INVITE sent |
|
|
|
INVITE sent, waiting for response |
|
|
|
200 OK received, media flowing |
|
|
|
BYE sent, cleanup in progress |
|
|
|
Session closed |
|
18.5.4 INVITE Header Processing
When an inbound INVITE arrives, sip-wrapper.js extracts these headers:
|
Header |
Dialog Property |
Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Campaign data |
|
|
|
|
Behavioral Note: If X-Call-Dropped-Custom-Reason is present in a BYE, it overrides the default reason code. This allows FreeSWITCH to signal specific hangup causes to the Agent Desktop.
---
18.6 FreeSWITCH Dialplan Deep Dive
18.6.1 Dialplan Processing Order
FreeSWITCH processes dialplan extensions in order until a match is found:
1. public.xml (external profile) - for calls from CUCM/SIP trunk
2. default.xml (internal profile) - for calls from agents/WebRTC
3. Features / IVR extensions
18.6.2 Inbound Call Dialplan (Customer → FreeSWITCH)
<!-- Simplified inbound dialplan flow -->
<extension name="outside_call" continue="true">
<condition>
<action application="set" data="outside_call=true"/>
<action application="export" data="RFC2822_DATE=${strftime(%a, %d %b %Y %T %z)}"/>
</condition>
</extension>
<extension name="inbound_ivr">
<condition field="destination_number" expression="^(\d+)$">
<action application="answer"/>
<action application="set" data="session_in_hangup_hook=true"/>
<action application="set" data="api_hangup_hook=lua cx_hangup.lua"/>
<action application="lua" data="vcApi.lua"/>
</condition>
</extension>
```
Behavioral Sequence:
1. SIP INVITE arrives on external profile (port 5080)
2. FreeSWITCH matches destination_number against dialplan
3. answer() - Accepts the call (200 OK sent)
4. session_in_hangup_hook=true - Ensures hangup hook runs
5. api_hangup_hook=lua cx_hangup.lua - Sets post-hangup script
6. lua vcApi.lua - Executes main IVR/agent request script
18.6.3 Agent Routing Dialplan (FreeSWITCH → Agent)
When Voice Connector sends uuid_transfer to bridge to an agent:
ESL: uuid_transfer <customer_uuid> <agent_extension> XML <domain>
│
▼
FreeSWITCH:
1. Creates new B-leg INVITE to agent extension
2. Agent's SIP.js receives INVITE via WebSocket
3. Agent answers (200 OK)
4. FreeSWITCH bridges A-leg and B-leg
5. RTP audio flows between customer and agent
18.6.4 Recording Dialplan Extension
<extension name="record_call" continue="true">
<condition field="${call_direction}" expression="^inbound$">
<action application="set" data="record_path=/app/files/wav/${strftime(%Y%m%d)}/"/>
<action application="set" data="record_ext=wav"/>
<action application="lua" data="set_recording_name.lua"/>
<action application="export" data="recording_filename=${recording_filename}"/>
<action application="record_session" data="${record_path}/${recording_filename}"/>
</condition>
</extension>
Recording Filename Format:
```
{dialogId}:{agentExtension}:{customerNumber}:{epochSeconds}.wav
```
Example: abc-123:1005:+18005550199:1718000000.wav
18.6.5 Hangup Hook Behavior
When any call leg hangs up:
FreeSWITCH detects hangup
│
▼
Executes cx_hangup.lua:
1. Gets hangup_cause (NORMAL_CLEARING, NO_ANSWER, etc.)
2. Gets call UUID
3. Gets recording_filename (if recorded)
4. Posts call end event to Voice Connector
POST /call-ended
{ uuid, hangupCause, recordingFile, duration }
│
▼
If recording exists:
- For encrypted: triggers encrypt.lua via RECORD_STOP hook
- File is moved from temp to final location
18.6.6 Dialplan Variables Set During Call
|
Variable |
Set By |
Purpose |
|---|---|---|
|
|
FreeSWITCH (auto) |
Unique call identifier |
|
|
Dialplan |
INBOUND / OUTBOUND |
|
|
INVITE |
Dialed number (DN) |
|
|
INVITE |
Calling number (ANI) |
|
|
vcApi.lua |
Queue name for routing |
|
|
vcApi.lua |
INBOUND / OUT / CONSULT |
|
|
set_recording_name.lua |
Output filename |
|
|
Dialplan |
Output directory |
|
|
Dialplan |
Flag for external calls |
|
|
Dialplan |
Post-hangup script |
|
|
Dialplan |
Enable hangup hook |
---
18.7 CIM Message Complete Schemas
18.7.1 Message Header Schema
All CIM messages share a common header:
{
"header": {
"intent": "ASSIGN_RESOURCE_REQUESTED",
"conversationId": "conv-uuid",
"channelSessionId": "cs-uuid",
"channelData": {
"channelCustomerIdentifier": "+18005550199",
"serviceIdentifier": "8001"
},
"sender": {
"type": "CONNECTOR",
"senderName": "CX-Voice-Connector"
},
"timestamp": "2026-06-08T10:00:00.000Z"
},
"body": { ... }
}
18.7.2 ASSIGN_RESOURCE_REQUESTED (Voice Connector → CCM)
{
"header": {
"intent": "ASSIGN_RESOURCE_REQUESTED",
"channelData": {
"channelCustomerIdentifier": "+18005550199",
"serviceIdentifier": "8001"
},
"sender": { "type": "CONNECTOR", "senderName": "CX-Voice-Connector" }
},
"body": {
"type": "ASSIGN_RESOURCE_REQUESTED",
"additionalDetail": {
"callId": "abc-123-call-id",
"direction": "INBOUND",
"mode": "QUEUE",
"resource": { "type": "NAME", "value": "Sales" },
"metadata": {
"uuid": "freeswitch-call-uuid",
"eslHost": "192.168.1.161",
"callingNumber": "+18005550199",
"dialedNumber": "8001"
}
}
}
}
18.7.3 AGENT_RESERVED (CCM → Voice Connector)
{
"header": {
"intent": "AGENT_RESERVED",
"conversationId": "conv-uuid",
"channelSessionId": "cs-uuid",
"sender": { "type": "CCM", "senderName": "CCM" }
},
"body": {
"type": "AGENT_RESERVED",
"additionalDetail": {
"agentExtension": "1005",
"agentId": "agent-uuid",
"agentName": "John Doe",
"callId": "abc-123-call-id",
"uuid": "freeswitch-call-uuid",
"queue": "Sales",
"queueType": "NAME"
}
}
}
18.7.4 CANCEL_RESOURCE_REQUESTED (Voice Connector → CCM)
{
"header": {
"intent": "CANCEL_RESOURCE_REQUESTED",
"conversationId": "conv-uuid",
"sender": { "type": "CONNECTOR", "senderName": "CX-Voice-Connector" }
},
"body": {
"type": "CANCEL_RESOURCE_REQUESTED",
"additionalDetail": {
"reasonCode": "RONA",
"requestType": "VOICE",
"direction": "INBOUND"
}
}
}
18.7.5 CALL_ALERTING (Agent Desktop → CCM)
{
"header": {
"intent": "CALL_ALERTING",
"conversationId": "conv-uuid",
"channelSessionId": "cs-uuid",
"sender": { "type": "AGENT", "senderName": "AgentDesk" }
},
"body": {
"type": "VOICE",
"callId": "abc-123-call-id",
"leg": "abc-123:1005:+18005550199:2026-06-08T10:00:00.000Z",
"reasonCode": "INBOUND"
}
}
18.7.6 CALL_LEG_STARTED (Agent Desktop → CCM)
{
"header": {
"intent": "CALL_LEG_STARTED",
"conversationId": "conv-uuid",
"channelSessionId": "cs-uuid",
"sender": { "type": "AGENT", "senderName": "AgentDesk" }
},
"body": {
"type": "VOICE",
"callId": "abc-123-call-id",
"leg": "abc-123:1005:+18005550199:2026-06-08T10:00:00.000Z",
"reasonCode": "INBOUND"
}
}
18.7.7 CALL_LEG_ENDED (Agent Desktop → CCM)
{
"header": {
"intent": "CALL_LEG_ENDED",
"conversationId": "conv-uuid",
"channelSessionId": "cs-uuid",
"sender": { "type": "AGENT", "senderName": "AgentDesk" }
},
"body": {
"type": "VOICE",
"callId": "abc-123-call-id",
"leg": "abc-123:1005:+18005550199:2026-06-08T10:00:00.000Z",
"reasonCode": "NORMAL_CLEARING"
}
}
Reason Codes:
| Code | Meaning |
|---|---|
| NORMAL_CLEARING | Normal hangup by either party |
| OUTBOUND | Outbound call ended |
| AGENT-HANDLED | Agent hung up (fallback) |
| NO_ANSWER | RONA (not sent to CCM) |
| USER_BUSY | Called number busy |
| CALL_REJECTED | Call explicitly rejected |
| DESTINATION_OUT_OF_ORDER | Target unreachable |
18.7.8 CALL_HOLD / CALL_RESUME (Cisco Connector → CCM)
{
"header": {
"intent": "CALL_HOLD",
"conversationId": "conv-uuid",
"sender": { "type": "CONNECTOR", "senderName": "CX-Cisco-Connector" }
},
"body": {
"type": "VOICE",
"callId": "abc-123-call-id",
"leg": "abc-123:1005:+18005550199:2026-06-08T10:00:00.000Z"
}
}
18.7.9 WRAPUP (Cisco Connector → CCM)
{
"header": {
"intent": "WRAPUP",
"conversationId": "conv-uuid",
"sender": { "type": "CONNECTOR", "senderName": "CX-Cisco-Connector" }
},
"body": {
"type": "VOICE",
"callId": "abc-123-call-id",
"wrapUpCode": "Satisfied",
"wrapUpData": "Customer issue resolved"
}
}
18.7.10 VOICE_RECORDING (Recording Link Activities → CCM)
{
"header": {
"intent": "VOICE_RECORDING",
"channelSessionId": "cs-uuid",
"sender": { "type": "CONNECTOR", "senderName": "CX-Recording-Link-Activities" }
},
"body": {
"legId": "abc-123:1005:+18005550199:1718000000",
"voiceRecordingUrl": "https://middleware/abc-123:1005:+18005550199:1718000000/recording-file"
}
}
---
18.8 Recording File Lifecycle
18.8.1 EFSwitch Recording Lifecycle
Call Starts
│
▼
FreeSWITCH starts recording
→ File created: /tmp/record-uuid.wav (temp)
│
▼
During call:
→ Audio appended to temp file
→ On hold: MOH mixed in
→ Pause: writing stops (if paused)
│
▼
Call Ends
│
▼
RECORD_STOP event fires
│
▼
encrypt.lua triggered (if encryption enabled)
→ Reads temp file
→ Encrypts with AES/CFB
→ Writes IV + encrypted data
│
▼
File moved to final location:
/app/files/wav/{date}/{dialogId}:{agent}:{ani}:{epoch}.wav
│
▼
Recording Middleware can serve file immediately
│
▼
Recording Link Activities queries and pushes link to CCM
│
▼
Archival process (scheduled):
→ Files older than retention period moved to archive storage
│
▼
Deletion process (scheduled):
→ Files older than compliance period permanently deleted
18.8.2 Eleveo Recording Lifecycle
Call Starts
│
▼
Cisco Phone BiB forks audio
→ BiB leg sent to Eleveo recorder via SIP trunk
│
▼
Eleveo receives RTP stream
→ Writes to temporary buffer
│
▼
Call Ends
│
▼
Eleveo finalizes recording
→ Saves to Eleveo database + filesystem
→ Metadata: JTAPI_CISCO_ID, COUPLE_START_REASON, etc.
│
▼
Recording Middleware queries Eleveo API
→ Authenticates with JSESSIONID
→ Searches by JTAPI_CISCO_ID
→ Downloads individual legs
│
▼
Middleware merges split legs (if hold/retrieve/conference)
→ mergeHoldEndedLegs()
→ mergeRetrievedLegs()
→ mergeConferenceLegs()
│
▼
Merged file served to requesting client
│
▼
Recording Link Activities pushes URL to CCM
18.8.3 Recording Retention Configuration
|
Parameter |
EFSwitch |
Eleveo |
|---|---|---|
|
Default retention |
Configurable (e.g., 90 days) |
Configurable per Eleveo policy |
|
Archival trigger |
Cron job / scheduled task |
Eleveo built-in archival |
|
Archive location |
NAS/S3 bucket |
Eleveo archive storage |
|
Encryption at rest |
AES/CFB (optional) |
Eleveo native encryption |
|
Compliance deletion |
Configurable (e.g., 7 years) |
Configurable |
|
Audit trail |
File access logs |
Eleveo audit logs |
---
18.9 WebRTC Registration Flow
18.9.1 Agent Desktop WebRTC Registration
Agent logs into AgentDesk
│
▼
Angular loads sip-wrapper.js
│
▼
sip-wrapper.js initializes:
1. Creates SIP.UserAgent with WebSocket transport
2. WebSocket URL: wss://freeswitch-host:7443
3. URI: sip:{agentExtension}@{domain}
│
▼
SIP.js sends REGISTER via WebSocket
│
▼
FreeSWITCH mod_sofia receives REGISTER:
1. Authenticates (if auth required)
2. Creates SIP contact binding
3. Stores registration in memory
│
▼
FreeSWITCH sends 200 OK to REGISTER
│
▼
Agent Desktop is now registered
→ Can receive inbound INVITEs
→ Can initiate outbound INVITEs
│
▼
Registration refresh (every ~300 seconds):
→ SIP.js auto-sends re-REGISTER
→ FreeSWITCH refreshes binding
18.9.2 WebSocket Connection Behavior
|
Event |
Behavior |
|---|---|
|
WebSocket connects |
SIP.js starts registration sequence |
|
WebSocket disconnects |
All active calls are dropped; auto-reconnect attempts |
|
Reconnect successful |
Re-registration happens automatically |
|
Reconnect fails after N attempts |
Agent shown as offline; must refresh page |
18.9.3 Multiple Tab Behavior
Critical Behavioral Detail: If an agent opens AgentDesk in multiple browser tabs:
- Each tab creates a separate SIP.js UserAgent
- Each registers independently with FreeSWITCH
- FreeSWITCH sends INVITE to all registered contacts
- All tabs ring simultaneously
- Whichever tab answers first gets the call
- Other tabs receive CANCEL
This is a known limitation — agents should use only one tab.
---
18.10 CTI JS Library Behavior
18.10.1 Library Loading
The AgentDesk conditionally loads the CTI library based on configuration:
AgentDesk loads
│
▼
Checks ef-unified-agent-configmap:
isCiscoEnabled: "true"
│
▼
If Cisco enabled:
→ Loads cti-js-library (Finesse BOSH client)
→ Establishes BOSH connection to Finesse
→ Subscribes to agent state events
│
▼
If Cisco disabled:
→ Loads FreeSWITCH SIP.js wrapper only
→ No Finesse integration
18.10.2 BOSH Connection Behavior
cti-js-library initializes
│
▼
Sends BOSH request:
POST https://finesse-host:443/http-bind/
Body: <body rid='1' xmlns='http://jabber.org/protocol/httpbind'
to='finesse-host' xml:lang='en'
wait='60' hold='1' content='text/xml; charset=utf-8'
ver='1.6' xmpp:version='1.0'/>
│
▼
Finesse returns session ID
│
▼
Sends authentication:
<auth mechanism='PLAIN' xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
{base64(agentId\0agentId\0password)}
</auth>
│
▼
Finesse authenticates, returns success
│
▼
Binds resource:
<iq type='set' id='bind_1'>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
<resource>cti-js</resource>
</bind>
</iq>
│
▼
Subscribes to agent state:
<iq type='set' to='pubsub.finesse-host' id='sub1'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<subscribe node='/finesse/api/User/{agentId}'/>
</pubsub>
</iq>
│
▼
Now receives all agent state updates via BOSH
18.10.3 Finesse Event to CX MRD Mapping
When a Finesse event arrives via BOSH:
// cti-js-library event handler
function onFinesseEvent(xml) {
const state = extractState(xml); // e.g., "READY", "NOT_READY", "TALKING"
const reasonCode = extractReasonCode(xml); // e.g., "RONA", "Break"
// Map to CX MRD state
const mrdState = mapFinesseToMrdState(state);
// READY → READY
// NOT_READY → NOT_READY
// TALKING → BUSY
// RESERVED → RESERVED
// WORK → NOT_READY
// Emit event to Angular
emit('agentStateChanged', {
state: mrdState,
reasonCode: reasonCode,
source: 'Finesse'
});
}
18.10.4 Finesse HA Behavior
AgentDesk configured with:
finesseURLForAgent: "https://finesse-primary"
SECONDARY_FINESSE_URL: "https://finesse-secondary"
IS_FINESSE_HA_ENABLED: "true"
│
▼
Initial connection to primary Finesse
│
▼
If primary fails (HTTP error / timeout):
1. Detects failure after 3 consecutive errors
2. Disconnects from primary
3. Attempts connection to secondary
4. Re-registers agent on secondary
5. Resumes state monitoring
│
▼
If primary recovers:
1. AgentDesk stays on secondary (no automatic failback)
2. On next login, connects to primary again
---
18.11 Outbound Campaign / Dialer Behavior
18.11.1 Cisco Outbound Campaign Flow
Cisco Dialer
│
▼
1. Campaign configured in Cisco Admin
- Dialing mode: Preview / Progressive / Predictive
- Contact list imported
- Agent mapping configured
│
▼
2. Dialer selects contact from list
│
▼
3. For Preview mode:
- Contact presented to agent
- Agent reviews contact info
- Agent clicks "Dial" or "Skip"
│
▼
4. If agent clicks "Dial":
- Cisco reserves agent (RESERVED_OUTBOUND)
- Cisco initiates call via CUCM
- CUCM routes call through CX SIP Proxy
- FreeSWITCH handles outbound call
- Voice Connector notified
- CCM creates outbound conversation
│
▼
5. Customer answers:
- Agent state = TALKING
- CCM receives CALL_LEG_STARTED
- Recording starts
│
▼
6. Call ends:
- Cisco records call result
- Wrap-up code collected
- Contact list updated with result
18.11.2 CX Dialer Integration
The CX Dialer is a separate component that can work with or without Cisco campaigns:
CX Dialer
│
▼
1. Campaign created in CX Unified Admin
- Contact list uploaded
- Script configured
- Agents assigned
│
▼
2. Dialer selects contact
- Reserves agent via Routing Engine
- Sends preview to AgentDesk
│
▼
3. Agent accepts preview
- AgentDesk initiates outbound call
- SIP INVITE sent to FreeSWITCH
│
▼
4. FreeSWITCH routes outbound
- Via CUCM → PSTN
- Or via SIP trunk directly
│
▼
5. Customer answers
- Agent on call
- CCM conversation active
│
▼
6. Call ends
- Agent disposition (wrap-up)
- Contact result recorded
- Next contact queued
18.11.3 Direct Preview Dialing Mode
In direct preview mode (supported by FreeSWITCH):
1. Dialer selects next contact
│
▼
2. Agent receives preview notification
- Customer phone number
- Customer name (if available)
- Campaign script
│
▼
3. Agent has N seconds to review (configurable)
│
▼
4. Agent clicks "Call"
→ Outbound call initiated
│
▼
5. Or agent clicks "Skip"
→ Contact marked skipped
→ Next contact presented
│
▼
6. If timeout expires:
→ Contact auto-skipped
→ Next contact presented
---
18.12 Multi-tenancy Voice Behavior
18.12.1 Tenant Isolation Model
The CX Voice system supports multi-tenancy with the following isolation behaviors:
|
Resource |
Isolation Level |
Behavior |
|---|---|---|
|
SIP Trunk |
Per-tenant |
Each tenant has dedicated SIP trunk to CUCM |
|
FreeSWITCH domain |
Per-tenant |
Each tenant has separate SIP domain |
|
Dialplan |
Per-tenant |
|
|
Recording path |
Per-tenant |
|
|
Agent extension range |
Per-tenant |
Tenant-specific extension prefixes |
|
Queue names |
Per-tenant |
Tenant-specific queue definitions |
|
Cisco Connector |
Shared |
One connector listens to all CUCM events |
|
JTAPI events |
Filtered |
Correlation filters by tenant |
18.12.2 Per-Tenant FreeSWITCH Configuration
-- cx_env_8001.lua (tenant with DN 8001)
local cx_env = {
tenant_id = "tenant_8001",
ivr_prompts_folder = "/app/prompts/tenant_8001/",
queue = "Sales_8001",
queueType = "NAME",
recording_enabled = true,
record_path = "/app/files/wav/tenant_8001/",
voicemail_enabled = false,
transferType = "NAMED",
-- Other tenant-specific settings
}
return cx_env
Behavioral Flow:
1. Inbound call arrives at DN 8001
2. FreeSWITCH dialplan matches destination_number = 8001
3. Loads cx_env_8001.lua
4. All subsequent operations use tenant-specific config
5. Recordings saved to tenant-specific path
6. Agent extensions looked up in tenant scope
18.12.3 Tenant Routing in Voice Connector
// VcService.java - tenant routing logic
public void routeAgent(String direction, String serviceIdentifier) {
// serviceIdentifier = DN = tenant identifier
TenantConfig tenantConfig = tenantRepository
.findByServiceIdentifier(serviceIdentifier);
// Use tenant-specific ESL host
String eslHost = tenantConfig.getEslHost();
// Use tenant-specific queue
String queue = tenantConfig.getQueue();
// Route within tenant boundary
eslConnection.sendCommand(
"uuid_transfer", uuid, agentExtension, "XML", tenantConfig.getDomain()
);
}
---
18.13 Wrap-up Code Flow
18.13.1 Wrap-up Code Collection
Call ends
│
▼
Agent Desktop: handleDialogDroppedEvent()
│
▼
If wrap-up is configured:
1. Angular shows wrap-up panel
2. Agent selects wrap-up code from dropdown
3. Agent can add notes
4. Agent clicks "Submit"
│
▼
If auto-wrap-up:
1. System assigns default wrap-up code
2. Timer starts (e.g., 30 seconds)
3. Agent can change code during timer
4. After timer expires, code is locked
│
▼
Wrap-up submitted to CCM
POST /conversation-manager/activities/wrapup
{ conversationId, wrapUpCode, wrapUpNotes }
│
▼
CCM stores wrap-up activity
│
▼
Cisco Connector queries Cisco DB for wrap-up data
- UCCE: WrapupData from Termination_Call_Detail
- CCX: callwrapupdata from AgentConnectionDetail
│
▼
Cisco Connector sends WRAPUP CIM to CCM
│
▼
CCM correlates wrap-up with conversation
18.13.2 Wrap-up Code Configuration
In CX Unified Admin:
- Wrap-up codes are configured per channel
- Codes can be mandatory or optional
- Default code can be set
- Auto-wrap-up duration configurable per MRD
In Cisco:
- Wrap-up codes configured in UCCE/UCCX admin
- Codes mapped to skill groups
- Wrap-up timer configured per agent team
Synchronization: Cisco wrap-up codes must be manually replicated in CX Unified Admin for them to appear in QM schedules and filters.
---
18.14 Call Variable Passing
18.14.1 Call Variable Flow
Call variables (screen-pop data) flow from Cisco → FreeSWITCH → Agent Desktop:
Cisco UCCE/UCCX
│
▼
Call variables set in ICM script / CCX script
- Call.PeripheralVariable1 = "Account#12345"
- Call.PeripheralVariable2 = "VIP"
- etc.
│
▼
CUCM includes variables in SIP INVITE headers
X-Call-Variable0: Account#12345
X-Call-Variable1: VIP
X-Call-Variable2: ...
│
▼
FreeSWITCH receives INVITE
→ Preserves variables as channel variables
→ Passes to Agent Desktop in outbound INVITE
│
▼
Agent Desktop (sip-wrapper.js) extracts:
dialog.callVariables = {
"CallVariable": [
{ name: "callVariable0", value: "Account#12345" },
{ name: "callVariable1", value: "VIP" }
]
}
│
▼
Angular displays in UI
→ Screen pop can use variables
→ CRM integration can pass variables
18.14.2 Call Variable Limitations
|
Limitation |
Detail |
|
|---|---|---|
|
Max variables |
10 (CallVariable0-9) |
|
|
Max length per variable |
40 characters (Cisco limit) |
|
|
Encoding |
ASCII only; special chars may be stripped |
|
|
Special characters |
|
` may cause parsing issues |
|
Empty variables |
Sent as empty string, not omitted |
18.14.3 Variable Mapping in AgentDesk
// sip.service.ts - variable processing
handleNewInboundCallEvent(event: any) {
const callVars = event.dialog.callVariables?.CallVariable || [];
// Extract known variables
const accountNumber = callVars.find(v => v.name === 'callVariable0')?.value;
const priority = callVars.find(v => v.name === 'callVariable1')?.value;
const campaignId = callVars.find(v => v.name === 'callVariable2')?.value;
// Use for customer identification
if (accountNumber) {
this.identifyCustomerByAccount(accountNumber);
}
// Use for screen pop
this.screenPopData = { accountNumber, priority, campaignId };
}
---
18.15 Comprehensive Error Recovery Behaviors
18.15.1 FreeSWITCH Crash During Active Call
FreeSWITCH crashes
│
▼
Agent Desktop:
- WebSocket disconnects
- SIP.js session state → Terminated
- onBye/onCancel not received
- Dialog state = DROPPED (after timeout)
→ CALL_LEG_ENDED sent to CCM with reason "NETWORK_ERROR"
│
▼
CCM:
- Receives CALL_LEG_ENDED
- Marks channel session ENDED
- If all sessions ended → closes conversation
│
▼
Customer:
- Call dropped (no media path)
- May hear reorder tone or silence
│
▼
Recovery:
- FreeSWITCH restarted (systemd/docker)
- Agents must refresh page to re-register
- New calls use restarted instance
18.15.2 Voice Connector Failure
Voice Connector crashes or becomes unreachable
│
▼
FreeSWITCH vcApi.lua:
- HTTP POST to Voice Connector fails
- HTTP timeout or connection refused
│
▼
Behavioral fallback:
1. Retry HTTP POST (up to 3 times)
2. If all retries fail:
- Play error message to customer
- Hangup call
- Log failure
│
▼
Agent Desktop:
- If already on call: call continues (media via FreeSWITCH)
- If call waiting: never receives AGENT_RESERVED
- Call eventually times out and is dropped
│
▼
Recovery:
- Voice Connector restarts
- FreeSWITCH health check passes
- Normal operation resumes
18.15.3 CCM Failure
CCM becomes unavailable
│
▼
Voice Connector:
- Cannot send CIM to CCM
- HTTP timeout on /ccm/message/receive
│
▼
Behavior:
- Voice Connector queues messages locally (if configured)
- Or drops messages and logs error
- FreeSWITCH continues handling calls independently
│
▼
Routing Engine:
- Cannot receive ASSIGN_RESOURCE_REQUESTED
- No tasks created
- Agents remain idle
│
▼
Agent Desktop:
- Cannot send CALL_ALERTING / CALL_LEG_STARTED / CALL_LEG_ENDED
- Call state not reflected in CX
- CRM/activity tracking breaks
│
▼
Recovery:
- CCM restarts
- Pending CIM messages may be lost
- Manual reconciliation may be needed
18.15.4 Routing Engine Failure
Routing Engine becomes unavailable
│
▼
CCM:
- Cannot send AssignResourceRequest
- HTTP timeout
│
▼
Behavior:
- CCM retries (exponential backoff)
- After max retries: marks request failed
- Voice Connector receives NO_AGENT_AVAILABLE
│
▼
FreeSWITCH:
- Plays "no agent available" message
- Hangs up after configured timeout
│
▼
Recovery:
- Routing Engine restarts
- Queue state restored from database
- New requests processed normally
18.15.5 Cisco Connector JTAPI Disconnect
JTAPI connection to CUCM drops
│
▼
JtapiListenerService:
- Provider state = SHUTDOWN
- All CallObservers removed
│
▼
Behavior:
1. Cisco Connector attempts reconnection
- Retry every 30 seconds (configurable)
2. During disconnect:
- No JTAPI events captured
- Call legs not correlated
- Recording links may fail to push
3. After reconnection:
- New events captured normally
- Missed events are LOST (no backfill)
│
▼
HA Mode (JTAPI_HA_MODE=true):
- Standby node detects primary disconnect
- Standby acquires Consul lock
- Standby starts processing events
- Minimal event loss (only during failover)
│
▼
Non-HA Mode:
- All events lost during disconnect
- Call legs may have incomplete data
- Manual reconciliation may be needed
18.15.6 Recording Server Disk Full
Recording server disk reaches 95% capacity
│
▼
FreeSWITCH:
- Cannot write new recording files
- `record_session` fails with I/O error
│
▼
Behavior:
1. FreeSWITCH logs error: "Cannot open file for writing"
2. Call continues normally (no recording)
3. Recording Middleware returns 404 for missing files
│
▼
Monitoring:
- Disk usage alert fires
- Admin notified
│
▼
Recovery:
1. Free up disk space (delete old recordings)
2. Or expand storage
3. Or enable archival to move old files
4. New recordings resume automatically
18.15.7 Database Connection Failure (Cisco Connector)
Cisco Connector loses connection to local DB
│
▼
JtapiListenerService:
- Cannot persist JTAPI events
- Events dropped
│
▼
CorrelationService:
- Cannot read unprocessed events
- No leg correlation happens
│
▼
ConversationService:
- Cannot read CallLegs
- Cannot write enriched legs
- Cannot push CIM to CCM
│
▼
Behavior:
- JTAPI events lost
- Call legs incomplete
- No wrap-up codes pushed
- Recording links may still work (independent)
│
▼
Recovery:
- Database connection restored
- New events processed normally
- Lost events are NOT recoverable
---
18.16 Summary: Behavioral Coverage Matrix
|
Behavior |
Cisco Side |
EFCX Side |
Section |
|---|---|---|---|
|
Agent state transitions |
Finesse state machine |
MRD state machine |
18.1 |
|
Hold/Retrieve |
re-INVITE with hold SDP |
CALL_HOLD/CALL_RESUME CIM |
18.2.1-2 |
|
Blind Transfer |
SIP REFER |
CALL_LEG_ENDED + new leg |
18.2.3 |
|
Consult Transfer |
Consult call + REFER |
CONSULT_TRANSFER type |
18.2.4 |
|
Conference |
Three-way bridge |
CONFERENCE type |
18.2.5 |
|
Conversation lifecycle |
— |
CREATED → ACTIVE → CLOSED |
18.3 |
|
Task state machine |
— |
QUEUED → RESERVED → ACTIVE |
18.4 |
|
RONA |
CANCEL + NO_ANSWER |
Task revoked + NOT_READY |
18.4.3 |
|
SIP.js states |
— |
Initial → Established → Terminated |
18.5 |
|
Dialplan processing |
CUCM routing |
FreeSWITCH XML dialplan |
18.6 |
|
CIM schemas |
JTAPI → CIM |
All message schemas |
18.7 |
|
Recording lifecycle |
BiB forking |
File write → encrypt → serve |
18.8 |
|
WebRTC registration |
— |
REGISTER via WebSocket |
18.9 |
|
CTI JS Library |
BOSH to Finesse |
State mapping |
18.10 |
|
Outbound campaigns |
Cisco Dialer |
CX Dialer |
18.11 |
|
Multi-tenancy |
Extension ranges |
Tenant-specific config |
18.12 |
|
Wrap-up codes |
Cisco DB |
CCM activity |
18.13 |
|
Call variables |
Peripheral variables |
SIP headers → dialog |
18.14 |
|
Error recovery |
JTAPI reconnect |
Component restart |
18.15 |
This section provides a complete architectural and behavioral reference for the Recording Mixer and Media Server components. These are foundational to the VRS (Voice Recording Solution) and are distinct from the CCM-side recording middleware.
---
19.1 What is the Recording Mixer?
The Recording Mixer is a Java/Spring Boot microservice that performs post-processing on raw call recordings produced by the Media Server (FreeSWITCH). Its primary role is to transform fragmented, per-leg audio files into cohesive, final call recordings that are suitable for playback, archival, and Quality Management (QM) review.
Key Functions:
|
Function |
Description |
|---|---|
|
Mixing |
Combines A-leg (customer) and B-leg (agent) audio into a single stereo or mono WAV file |
|
Splicing |
Concatenates multiple call segments (before hold, after hold, after retrieve) into a continuous recording |
|
Rule Evaluation |
Applies recording rules (who to record, when, under what conditions) |
|
Format Conversion |
Converts between audio formats (sample rate, bit depth, codec) if needed |
|
File Management |
Moves processed files from temporary to final storage, manages file naming |
|
Metadata Enrichment |
Associates recording files with call metadata (callId, agent, timestamps) |
|
Queue Management |
Manages a processing queue for large volumes of recordings |
Technology Stack:
|
Layer |
Technology |
|---|---|
|
Runtime |
Java 17 + Spring Boot |
|
Audio Processing |
Java Sound API / External audio tools |
|
Message Queue |
RabbitMQ / JMS |
|
Storage |
Local filesystem / NAS / S3-compatible |
|
Container |
Docker (managed via Docker Swarm in HA) |
---
19.2 What is the Media Server in This Context?
In the VRS architecture, the Media Server refers to FreeSWITCH acting as the recording endpoint. Its role is to:
-
Receive forked media streams from CUCM via SIP trunk (BiB recordings)
-
Write raw audio to disk as individual call leg files
-
Generate call metadata (timestamps, call IDs, file paths)
-
Notify the Mixer that a recording is ready for processing
FreeSWITCH as Media Server vs. FreeSWITCH as CX Media Server:
|
Aspect |
VRS Media Server |
CX Media Server |
|---|---|---|
|
Primary Role |
Recording only |
Call routing + media handling |
|
SIP Trunk |
From CUCM (BiB) |
From CUCM / SIP Proxy (call path) |
|
Recording |
Writes raw WAV per BiB leg |
May also record via dialplan |
|
Mixer |
Feeds Mixer for post-processing |
N/A (CCM handles recording links) |
|
Scripts |
|
|
|
User Interaction |
None (headless) |
Agent calls, transfers, holds |
---
19.3 Mixer Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ RECORDING PIPELINE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ CUCM ──BiB fork──► ┌──────────────────┐ │
│ │ Media Server │ │
│ │ (FreeSWITCH) │ │
│ │ │ │
│ │ • Receives SIP │ │
│ │ • Writes WAV │ │
│ │ • Generates meta │ │
│ └────────┬─────────┘ │
│ │ │
│ │ HTTP POST /mixer/sip-data │
│ │ (SIP metadata + file path) │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Recording Mixer │ │
│ │ (Java/Spring) │ │
│ │ │ │
│ │ • Evaluates rules│ │
│ │ • Mixes legs │ │
│ │ • Splices segments│ │
│ │ • Formats output │ │
│ └────────┬─────────┘ │
│ │ │
│ │ Move to final storage │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Final Storage │ │
│ │ (NAS/S3/local) │ │
│ └────────┬─────────┘ │
│ │ │
│ │ REST API │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Recording │ │
│ │ Middleware │ │
│ │ (serves files) │ │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
---
19.4 Media Server (FreeSWITCH) Recording Behavior
19.4.1 Incoming BiB Stream Handling
When CUCM sends a BiB recording stream to the Media Server:
CUCM SIP INVITE (BiB leg)
│
▼
FreeSWITCH external profile receives INVITE
│
▼
Dialplan matches: ${sip_from_host} = <CUCM_IP>
│
▼
Executes: lua record.lua
│
▼
record.lua:
1. Extracts metadata from SIP headers:
- x-cisco-calling-number (ANI)
- x-cisco-called-number (DN)
- x-cisco-global-call-id (GCID)
- x-cisco-jtapi-call-id
2. Determines recording path
3. Starts recording via:
session:execute("record_session", path)
│
▼
FreeSWITCH writes RTP audio to WAV file
/var/vrs/recordings/cucmRecording/streams/{gcid}_{timestamp}.wav
│
▼
On call end (BYE):
1. FreeSWITCH stops recording
2. Closes WAV file
3. Triggers RECORD_STOP event
4. Executes: lua cx_hangup.lua (post-processing hook)
│
▼
cx_hangup.lua:
1. Collects call metadata
2. Posts to Mixer: POST http://mixer:9900/mixer/sip-data
{
"callId": "gcid-123",
"agentExtension": "1005",
"customerNumber": "+18005550199",
"startTime": "2026-06-08T10:00:00Z",
"endTime": "2026-06-08T10:05:30Z",
"filePath": "/var/vrs/recordings/.../gcid-123_20260608100000.wav",
"recordingType": "BIB"
}
19.4.2 Media Server File Layout
The Media Server organizes recordings in a structured filesystem:
/var/vrs/recordings/
├── cucmRecording/
│ ├── streams/ # Raw per-leg recordings
│ │ ├── 20260608/
│ │ │ ├── gcid-123_20260608100000.wav # Agent leg
│ │ │ ├── gcid-123_20260608100001.wav # Same call, hold segment
│ │ │ └── gcid-124_20260608100002.wav # Different call
│ │ └── 20260609/
│ └── sessions/ # Mixed final recordings (Mixer output)
│ ├── 20260608/
│ │ ├── gcid-123_mixed.wav
│ │ └── gcid-124_mixed.wav
│ └── 20260609/
19.4.3 Media Server SIP Profile Configuration
The Media Server uses a dedicated FreeSWITCH SIP profile optimized for recording:
<!-- /usr/local/freeswitch/conf/sip_profiles/recording.xml -->
<profile name="recording">
<settings>
<param name="sip-ip" value="192.168.1.100"/>
<param name="sip-port" value="5060"/>
<param name="auth-calls" value="false"/>
<param name="apply-inbound-acl" value="cucm"/>
<param name="disable-transcoding" value="false"/>
<param name="media-mode" value="proxy"/>
</settings>
</profile>
Key Differences from CX Media Server Profile:
- Authentication disabled (auth-calls=false) — CUCM SIP trunk does not authenticate
- ACL restricts to CUCM IPs only
- Media mode is proxy (pass-through) since the server only records, not processes media
19.4.4 Media Server Recording Rules
The Media Server can evaluate basic recording rules via the record.lua script:
-- record.lua snippet
function evaluate_recording_rules(callInfo)
-- Query Mixer / VRS for recording rules
local rules_url = "http://" .. MIXER_IP .. ":8080/vrs/recording-rules/evaluate"
local payload = {
agentExtension = callInfo.agentExtension,
callingNumber = callInfo.callingNumber,
calledNumber = callInfo.calledNumber,
globalCallId = callInfo.globalCallId,
timestamp = callInfo.timestamp
}
local response = http_post(rules_url, payload)
if response and response.record == true then
return true, response.quality -- e.g., "stereo", "mono"
else
return false, nil
end
end
---
19.5 Mixer Processing Pipeline
19.5.1 Stage 1: Receive and Queue
Mixer receives POST /mixer/sip-data
│
▼
SipDataController:
1. Validates payload (required fields: callId, filePath, startTime, endTime)
2. Checks for duplicate (idempotency by callId + timestamp)
3. Creates MixerJob entity:
- jobId = UUID
- status = RECEIVED
- callId = {gcid}
- rawFilePath = {path}
- receivedAt = now()
4. Persists to Mixer database
5. Publishes job to RabbitMQ queue: "mixer.processing"
│
▼
RabbitMQ queue holds jobs until workers available
19.5.2 Stage 2: Rule Evaluation
Mixer Worker pulls job from queue
│
▼
RuleEvaluationService:
1. Queries recording rules from VRS database
2. Evaluates rules against call metadata:
Rule Types:
┌────────────────────┬─────────────────────────────────────┐
│ Rule Type │ Example │
├────────────────────┼─────────────────────────────────────┤
│ Agent-based │ Record all agents in "Sales" team │
│ DN-based │ Record calls to DN 8001 only │
│ Time-based │ Record 9 AM - 6 PM only │
│ Percentage-based │ Record 100% of calls to DN 8001 │
│ │ Record 10% of calls to DN 8002 │
│ ANI-based │ Do not record calls from +1234... │
│ Duration-based │ Only record calls > 10 seconds │
└────────────────────┴─────────────────────────────────────┘
3. If rules say "DO NOT RECORD":
- job.status = SKIPPED
- raw file deleted (or moved to skipped folder)
- Job complete
4. If rules say "RECORD":
- job.status = RULES_PASSED
- Determines recording parameters:
- format: "stereo" (agent left, customer right) or "mono" (mixed)
- sampleRate: 8000 or 16000 Hz
- bitDepth: 16
5. Proceeds to Stage 3
19.5.3 Stage 3: Leg Discovery
LegDiscoveryService:
1. Queries Mixer database for all raw files matching callId
2. Discovers related legs:
- Primary call leg (agent ↔ customer)
- Hold segments (if call was held/retrieved)
- Transfer legs (if call was transferred)
- Conference legs (if conference occurred)
3. Orders legs chronologically:
leg_1: 10:00:00 - 10:02:30 (primary)
leg_2: 10:02:30 - 10:03:00 (hold - MOH, not recorded)
leg_3: 10:03:00 - 10:05:30 (retrieved)
4. For transfers:
- Discovers new agent leg via globalCallId linkage
- Links legs: parent_call_id → child_call_id
5. Creates leg graph:
┌─────────────┐
│ Customer │
└──────┬──────┘
│
┌─────┴─────┐
▼ ▼
Agent A Agent B
(leg_1) (leg_2)
10:00 10:03
19.5.4 Stage 4: Audio Mixing
AudioMixingService:
1. Loads raw WAV files for all discovered legs
2. Decodes to PCM audio buffers
3. Applies mixing based on configuration:
STEREO MIXING:
┌─────────────────────────────────────────────┐
│ Left Channel │ Right Channel │
│ (Agent audio) │ (Customer audio) │
│ │ │
│ ████████████ │ ████████████ │
│ ████████████ │ ████████████ │
│ ████████████ │ ████████████ │
│ ████████████ │ ████████████ │
└─────────────────────────────────────────────┘
MONO MIXING:
┌─────────────────────────────────────────────┐
│ Mixed Channel (Agent + Customer combined) │
│ │
│ ████████████████████████████████████████ │
│ ████████████████████████████████████████ │
│ ████████████████████████████████████████ │
│ ████████████████████████████████████████ │
└─────────────────────────────────────────────┘
4. For held calls:
- MOH segments are optionally excluded from final mix
- Or included with a "system audio" marker
5. For conferences:
- All participant audio mixed into single stream
- Or split into multi-channel output (if supported)
6. Normalizes audio levels (AGC - Automatic Gain Control)
7. Writes output to temporary file:
/tmp/mixer/{jobId}_mixed.wav
19.5.5 Stage 5: Splicing (for Hold/Retrieve)
SplicingService:
1. Takes ordered list of audio segments:
segment_1: 0:00 - 2:30 (primary)
segment_2: 2:30 - 3:00 (hold gap - excluded or silent)
segment_3: 3:00 - 5:30 (retrieved)
2. Concatenates segments with optional transitions:
- Hard splice: immediate cut (default)
- Fade splice: 50ms fade in/out (smoother)
3. Adds metadata markers at splice points:
- Marker: "hold_start@2:30"
- Marker: "hold_end@3:00"
4. Output: single continuous WAV file
19.5.6 Stage 6: Metadata Enrichment
MetadataService:
1. Generates recording metadata JSON:
{
"recordingId": "rec-uuid",
"callId": "gcid-123",
"globalCallId": "gcid-123",
"agentExtension": "1005",
"agentId": "agent-uuid",
"agentName": "John Doe",
"customerNumber": "+18005550199",
"calledNumber": "8001",
"startTime": "2026-06-08T10:00:00Z",
"endTime": "2026-06-08T10:05:30Z",
"duration": 330000, // milliseconds
"segments": [
{ "start": 0, "end": 150000, "type": "TALKING", "agent": "1005" },
{ "start": 150000, "end": 180000, "type": "HOLD", "agent": "1005" },
{ "start": 180000, "end": 330000, "type": "TALKING", "agent": "1005" }
],
"format": "stereo",
"sampleRate": 8000,
"bitDepth": 16,
"codec": "PCM",
"fileSize": 5280000, // bytes
"checksum": "md5:abc123..."
}
2. Stores metadata in Mixer database
3. Associates with recording file
19.5.7 Stage 7: Storage and Archival
StorageService:
1. Moves mixed file from /tmp/ to final location:
{FINAL_STORAGE_PATH}/{date}/{recordingId}.wav
2. Copies metadata JSON alongside:
{FINAL_STORAGE_PATH}/{date}/{recordingId}.json
3. Updates MixerJob:
- status = COMPLETED
- outputFilePath = {final_path}
- completedAt = now()
4. Optional: triggers archival process
- If retention < threshold: move to cold storage
- If compliance period expired: mark for deletion
5. Notifies downstream systems:
- Recording Middleware (file ready)
- QM Connector (recording available for evaluation)
---
19.6 Mixer API Reference
19.6.1 POST /mixer/sip-data
Receives SIP metadata from FreeSWITCH when a recording leg completes.
Request:
```json
{
"callId": "gcid-abc-123",
"globalCallId": "gcid-abc-123",
"jtapiCallId": "12345",
"agentExtension": "1005",
"callingNumber": "+18005550199",
"calledNumber": "8001",
"direction": "INBOUND",
"startTime": "2026-06-08T10:00:00.000Z",
"endTime": "2026-06-08T10:05:30.000Z",
"filePath": "/var/vrs/recordings/cucmRecording/streams/gcid-abc-123_20260608100000.wav",
"fileSize": 2640000,
"recordingType": "BIB",
"tenantId": "default"
}
```
Response:
```json
{
"jobId": "mixer-job-uuid",
"status": "QUEUED",
"message": "Recording job accepted and queued for processing"
}
```
19.6.2 GET /mixer/jobs/{jobId}
Check the status of a mixer processing job.
Response:
```json
{
"jobId": "mixer-job-uuid",
"status": "COMPLETED",
"callId": "gcid-abc-123",
"receivedAt": "2026-06-08T10:05:31.000Z",
"startedAt": "2026-06-08T10:05:32.000Z",
"completedAt": "2026-06-08T10:05:35.000Z",
"inputFile": "/var/vrs/recordings/streams/...",
"outputFile": "/var/vrs/recordings/sessions/.../rec-uuid.wav",
"duration": 330000,
"format": "stereo"
}
```
Status Values:
|
Status |
Meaning |
|---|---|
|
|
Job received, not yet queued |
|
|
Job in RabbitMQ queue |
|
|
Worker actively processing |
|
|
Recording rules evaluated, will record |
|
|
Recording rules say do not record |
|
|
Audio mixing in progress |
|
|
Segment splicing in progress |
|
|
Final file written to storage |
|
|
Processing error (see errorMessage) |
19.6.3 GET /mixer/recordings/{recordingId}
Retrieve recording metadata.
19.6.4 GET /mixer/recordings/{recordingId}/download
Download the mixed recording file.
---
19.7 Mixer Recording Rules Engine
19.7.1 Rule Types and Evaluation
Rules are stored in the VRS database and evaluated per call:
-- Recording rules table
CREATE TABLE recording_rules (
id UUID PRIMARY KEY,
name VARCHAR(255),
rule_type VARCHAR(50), -- AGENT, DN, TIME, PERCENTAGE, ANI, DURATION
condition VARCHAR(500), -- JSON condition
action VARCHAR(50), -- RECORD, SKIP
parameters JSONB, -- { format: "stereo", quality: "high" }
priority INT DEFAULT 0,
enabled BOOLEAN DEFAULT true,
tenant_id VARCHAR(100)
);
Example Rules:
// Rule 1: Record all Sales team agents
{
"id": "rule-1",
"name": "Record Sales Calls",
"rule_type": "AGENT",
"condition": { "team": "Sales", "record": true },
"action": "RECORD",
"parameters": { "format": "stereo", "sampleRate": 8000 },
"priority": 100
}
// Rule 2: Skip internal calls
{
"id": "rule-2",
"name": "Skip Internal",
"rule_type": "ANI",
"condition": { "pattern": "^8\d{3}$", "record": false },
"action": "SKIP",
"priority": 200
}
// Rule 3: 50% sampling for DN 8002
{
"id": "rule-3",
"name": "Sample 8002",
"rule_type": "PERCENTAGE",
"condition": { "dn": "8002", "percentage": 50 },
"action": "RECORD",
"parameters": { "format": "mono" },
"priority": 150
}
```
19.7.2 Rule Evaluation Order
1. Rules evaluated by priority (highest first)
2. First matching rule wins (short-circuit)
3. If no rule matches: DEFAULT action (usually RECORD)
4. For PERCENTAGE rules: deterministic hash of callId % 100 < percentage
---
19.8 Mixer HA and Failover
19.8.1 Docker Swarm Deployment
# docker-compose.yml for Mixer HA
version: '3.8'
services:
mixer:
image: expertflow/vrs-mixer:latest
deploy:
replicas: 2
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
environment:
- SPRING_RABBITMQ_HOST=rabbitmq
- MIXER_STORAGE_PATH=/app/recordings
- MIXER_TEMP_PATH=/tmp/mixer
volumes:
- recordings:/app/recordings
- mixer-temp:/tmp/mixer
mixer-worker:
image: expertflow/vrs-mixer-worker:latest
deploy:
replicas: 3
environment:
- SPRING_RABBITMQ_HOST=rabbitmq
- MIXER_WORKER_QUEUE=mixer.processing
```
19.8.2 Failover Behavior
|
Failure |
Behavior |
|---|---|
|
Mixer API instance down |
Docker Swarm routes to healthy instance; jobs still accepted |
|
Mixer Worker down |
Unprocessed jobs remain in RabbitMQ; other workers pick up |
|
RabbitMQ down |
Jobs queued in Mixer database; processed when RabbitMQ recovers |
|
Storage (NAS) unreachable |
Jobs fail with STORAGE_ERROR; retried with exponential backoff |
|
FreeSWITCH post fails |
Mixer never receives job; raw recording remains on Media Server |
19.8.3 Recovery Behavior
Mixer Worker crashes mid-processing
│
▼
RabbitMQ detects consumer disconnect
│
▼
Message requeued (with delivery count)
│
▼
Another worker picks up the job
│
▼
Idempotency check:
- If output file exists and checksum matches → skip (COMPLETED)
- If partial file exists → delete and restart
- If no output → process normally
│
▼
Job completes (no duplicate output)
---
19.9 Mixer vs. Recording Middleware vs. Recording Link Activities
These three components are often confused. Here is the definitive behavioral distinction:
|
Aspect |
Recording Mixer |
Recording Middleware |
Recording Link Activities |
|---|---|---|---|
|
Layer |
VRS (post-processing) |
CCM integration |
CCM activity push |
|
Input |
Raw WAV files from FreeSWITCH |
Recording files + metadata |
CCM voice activities |
|
Output |
Mixed/spliced WAV files |
Audio stream (bytes) to client |
VOICE_RECORDING activity in CCM |
|
When it runs |
After call ends (async) |
On-demand (when user clicks play) |
Scheduled job (periodic) |
|
Audio processing |
Mixing, splicing, format conversion |
Decryption, streaming |
None (just URLs) |
|
Storage |
NAS/S3 (permanent) |
Local cache (temporary) |
Database (activity records) |
|
Consumer |
QM, archival, compliance |
Agent/supervisor playback UI |
CCM conversation timeline |
|
Technology |
Java + audio libs |
Java REST API |
Java scheduled job |
|
Container |
Docker Swarm |
Kubernetes pod |
Kubernetes pod |
Data Flow Between Them:
FreeSWITCH (Media Server)
│
▼ (raw WAV)
Recording Mixer
│
▼ (mixed WAV + metadata)
NAS / Shared Storage
│
├──────────────┐
▼ ▼
Recording Recording Link
Middleware Activities
│ │
▼ ▼
Playback UI CCM Activities
---
19.10 Common Mixer Issues and Troubleshooting
|
Symptom |
Root Cause |
Check |
Fix |
|---|---|---|---|
|
Recordings missing from QM |
Mixer job FAILED |
|
Check worker logs, retry job |
|
Audio only on one channel |
Wrong mix format |
Check rule parameters |
Set |
|
Hold music in recording |
MOH not excluded |
Check splicing config |
Add |
|
Gaps in recording |
Missing segments |
Check leg discovery |
Verify all legs have same globalCallId |
|
Recording too quiet |
No AGC applied |
Check audio processing config |
Enable |
|
Files not appearing |
Storage mount issue |
|
Remount NAS, check permissions |
|
High latency in processing |
Queue backlog |
RabbitMQ management UI |
Scale up mixer workers |
|
Duplicate recordings |
Idempotency failure |
Check jobId uniqueness |
Ensure FreeSWITCH sends unique callId |
|
Corrupted WAV files |
Network issue during write |
Check file checksums |
Re-process from raw files |
---
19.11 Summary: Media Server and Mixer Roles
|
Question |
Media Server (FreeSWITCH) |
Recording Mixer |
|---|---|---|
|
What does it do? |
Receives forked audio and writes raw WAV files |
Post-processes raw files into final recordings |
|
When does it run? |
During the call (real-time) |
After the call (async batch) |
|
Where does it sit? |
Between CUCM and storage |
Between raw storage and final storage |
|
Is it stateful? |
No (per-call only) |
Yes (tracks job state in DB) |
|
HA mechanism |
CUCM SIP trunk failover (active/standby) |
Docker Swarm with replicated workers |
|
Scaling concern |
Network bandwidth (RTP streams) |
CPU (audio mixing) + storage I/O |
|
Failure impact |
No recording captured |
Recording delayed or incomplete |
|
Recovery |
CUCM fails to standby |
Jobs requeued, reprocessed by workers |
This section provides complete architectural diagrams and behavioral flows for both Cisco UCCE (Unified Contact Center Enterprise) and Cisco UCCX (Unified Contact Center Express) deployments integrated with CX. It maps every Cisco component, every CX component, and every message/event flowing between them.
---
20.1 Cisco UCCE Architecture Overview
UCCE is Cisco's enterprise-grade contact center solution designed for large deployments (hundreds to thousands of agents). It uses a distributed architecture with multiple specialized servers.
20.1.1 UCCE Component Map
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ CISCO UCCE + CX INTEGRATION — COMPLETE VIEW │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ SIP/RTP ┌─────────────┐ SIP/RTP ┌─────────────────────────┐ │
│ │ PSTN │◄──────────────►│ CUBE / │◄──────────────►│ CUCM │ │
│ │ (Carrier) │ │ SBC │ │ (Call Manager) │ │
│ └─────────────┘ └──────┬──────┘ └───────────┬─────────────┘ │
│ │ │ │
│ SIPREC fork│ │JTAPI │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Media Server │ │ JTAPI Provider │ │
│ │ (FreeSWITCH) │ │ (CUCM Service) │ │
│ │ │ └──────────┬───────────┘ │
│ │ • Receives BiB │ │ │
│ │ • Writes WAV │ │JTAPI events │
│ └────────┬─────────┘ ▼ │
│ │ ┌──────────────────────┐ │
│ raw WAV │ │ Cisco Connector │ │
│ ▼ │ (CX Component) │ │
│ ┌──────────────────┐ │ │ │
│ │ Recording Mixer │ │ • JtapiListener │ │
│ │ (Java/Spring) │ │ • CorrelationService │ │
│ │ │ │ • ConversationService│ │
│ │ • Mixes legs │ └──────────┬───────────┘ │
│ │ • Splits merged │ │ │
│ └────────┬─────────┘ │CIM messages │
│ │ ▼ │
│ mixed WAV│ ┌──────────────────────┐ │
│ ▼ │ CCM │ │
│ ┌──────────────────┐ │ (Conversation Mgr) │ │
│ │ NAS / Storage │ └──────────┬───────────┘ │
│ └──────────────────┘ │ │
│ │REST │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Routing Engine │ │
│ │ (Task/Queue/Agent) │ │
│ └──────────┬───────────┘ │
│ │ │
│ │AGENT_RESERVED │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ AGENT WORKSTATION │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ Cisco Phone │ │ Agent Desktop │ │ Finesse Agent │ │ │
│ │ │ (BiB-enabled) │◄──►│ (Angular + │◄──►│ Desktop │ │ │
│ │ │ │SIP │ SIP.js) │BOSH│ (Gadget) │ │ │
│ │ │ • Forks audio │ │ │ │ │ │ │
│ │ │ • BiB to CUCM │ │ • Handles calls │ │ • Agent state │ │ │
│ │ └─────────────────┘ │ • Screen pop │ │ • Wrap-up codes │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ UCCE INFRASTRUCTURE (Back Office) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ PG │◄────►│ ICM │◄────►│ CVP │◄────►│ CUBE │ │ │
│ │ │ (Peripheral │ │ (Intelligent│ │ (Voice │ │ (Border │ │ │
│ │ │ Gateway) │ │ Contact Mgr)│ │ Portal) │ │ Element) │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ • Connects │ │ • Routing │ │ • IVR/VXML │ │ • SIPREC │ │ │
│ │ │ to CUCM │ │ decisions │ │ • Queuing │ │ • Trunking │ │ │
│ │ │ • Sends TCD │ │ • Scripts │ │ • Transfer │ │ • Security │ │ │
│ │ └──────┬──────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │ │
│ │ │ TCD data │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ AW │◄────►│ HDS │ │ │
│ │ │ (Admin │ │ (Historical │ │ │
│ │ │ Workstation)│ │ Data Store)│ │ │
│ │ │ │ │ │ │ │
│ │ │ • awdb │ │ • hds │ │ │
│ │ │ • Real-time │ │ • Historical│ │ │
│ │ └──────┬──────┘ └─────────────┘ │ │
│ │ │ │ │
│ │ │ SQL queries │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Cisco Connector → UCCE SQL Server │ │ │
│ │ │ Queries: Termination_Call_Detail, etc. │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
20.1.2 UCCE Component Definitions
|
Component |
Full Name |
Role |
CX Integration Point |
|---|---|---|---|
|
CUCM |
Cisco Unified Communications Manager |
Call control, phone registration, SIP routing |
JTAPI events → Cisco Connector; SIP trunk → FreeSWITCH |
|
CUBE |
Cisco Unified Border Element |
SBC for SIP trunking, SIPREC forking |
SIPREC → Media Server; SIP trunk ↔ PSTN |
|
CVP |
Cisco Unified Customer Voice Portal |
IVR, VXML, call queuing, transfers |
Passes calls to agents via CUCM; CX SIP Proxy for routing |
|
ICM |
Intelligent Contact Manager |
Routing scripts, skill-based routing |
UCCE makes routing decisions; CX gets post-call TCD data |
|
PG |
Peripheral Gateway |
Connects CUCM to ICM, generates TCD |
TCD database queried by Cisco Connector |
|
AW |
Admin Workstation |
Configuration, real-time monitoring |
|
|
HDS |
Historical Data Store |
Long-term call history |
Historical reporting data |
|
Finesse |
Cisco Finesse |
Agent desktop framework |
AgentDesk BOSH connection; CTI state sync |
|
Cisco Phone |
IP Phone with BiB |
Agent endpoint, audio forking |
BiB streams to Media Server via CUCM |
---
20.2 Cisco UCCX Architecture Overview
UCCX is Cisco's mid-market contact center solution designed for smaller deployments (up to a few hundred agents). It uses an all-in-one architecture with fewer servers.
20.2.1 UCCX Component Map
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ CISCO UCCX + CX INTEGRATION — COMPLETE VIEW │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ SIP/RTP ┌─────────────┐ SIP/RTP ┌─────────────────────────┐ │
│ │ PSTN │◄──────────────►│ CUBE / │◄──────────────►│ CUCM │ │
│ │ (Carrier) │ │ SBC │ │ (Call Manager) │ │
│ └─────────────┘ └──────┬──────┘ └───────────┬─────────────┘ │
│ │ │ │
│ SIPREC fork│ │JTAPI │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Media Server │ │ JTAPI Provider │ │
│ │ (FreeSWITCH) │ │ (CUCM Service) │ │
│ └────────┬─────────┘ └──────────┬───────────┘ │
│ │ │ │
│ │ │JTAPI events │
│ │ ▼ │
│ │ ┌──────────────────────┐ │
│ │ │ Cisco Connector │ │
│ │ │ (CX Component) │ │
│ │ └──────────┬───────────┘ │
│ │ │ │
│ │ │CIM messages │
│ │ ▼ │
│ │ ┌──────────────────────┐ │
│ │ │ CCM │ │
│ │ └──────────┬───────────┘ │
│ │ │ │
│ │ │REST │
│ │ ▼ │
│ │ ┌──────────────────────┐ │
│ │ │ Routing Engine │ │
│ │ └──────────┬───────────┘ │
│ │ │ │
│ │ │AGENT_RESERVED │
│ │ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ AGENT WORKSTATION │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ Cisco Phone │ │ Agent Desktop │ │ Finesse Agent │ │ │
│ │ │ (BiB-enabled) │◄──►│ (Angular + │◄──►│ Desktop │ │ │
│ │ │ │SIP │ SIP.js) │BOSH│ (Gadget) │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ UCCX INFRASTRUCTURE (All-in-One) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ UCCX Server │ │ │
│ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │
│ │ │ │ Engine │ │ IVR │ │ Database │ │ Finesse │ │ │ │
│ │ │ │ │ │ ( prompts) │ │ (db_cra) │ │ Tomcat │ │ │ │
│ │ │ │ • Call │ │ │ │ │ │ │ │ │ │
│ │ │ │ routing │ │ • VXML │ │ • CCD │ │ • Agent │ │ │ │
│ │ │ │ • Scripts │ │ scripts │ │ • Resource │ │ state │ │ │ │
│ │ │ │ • CSQ │ │ • Prompts │ │ • Session │ │ • HTTP API │ │ │ │
│ │ │ │ logic │ │ │ │ • Contact │ │ │ │ │ │
│ │ │ └─────────────┘ └─────────────┘ └──────┬──────┘ └─────────────┘ │ │ │
│ │ └─────────────────────────────────────────────┼──────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ SQL queries │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Cisco Connector → UCCX SQL Server │ │ │
│ │ │ Queries: ContactCallDetail, etc. │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
20.2.2 UCCX Component Definitions
|
Component |
Full Name |
Role |
CX Integration Point |
|---|---|---|---|
|
CUCM |
Cisco Unified Communications Manager |
Same as UCCE |
JTAPI events → Cisco Connector; SIP trunk → FreeSWITCH |
|
CUBE |
Cisco Unified Border Element |
Same as UCCE |
SIPREC → Media Server |
|
UCCX Engine |
Unified CCX Engine |
Call routing, scripts, CSQ management |
IVR handles queuing; CX manages agent assignment |
|
UCCX Database |
UCCX Internal DB ( |
Stores CCD, resource, session data |
Queried by Cisco Connector for wrap-up, call details |
|
UCCX Finesse |
Finesse (embedded in UCCX) |
Agent desktop |
AgentDesk BOSH connection |
|
Cisco Phone |
IP Phone with BiB |
Same as UCCE |
BiB streams to Media Server |
---
20.3 UCCE vs. UCCX — Key Differences for CX
|
Aspect |
UCCE |
UCCX |
|---|---|---|
|
Scale |
500–12,000+ agents |
Up to 400 agents |
|
Architecture |
Distributed (PG, ICM, CVP, AW separate) |
All-in-one (single server/cluster) |
|
Routing |
ICM scripts (complex, skill-based) |
UCCX scripts (simpler, CSQ-based) |
|
IVR |
CVP (separate VXML server) |
Built-in IVR (UCCX Engine) |
|
Database |
|
|
|
Call detail table |
|
|
|
Agent detail table |
|
|
|
Call correlation |
|
|
|
Wrap-up data |
|
|
|
Historical data |
HDS (separate historical store) |
Same |
|
Finesse |
Separate Finesse servers |
Embedded in UCCX |
|
SIP Proxy |
CX SIP Proxy for routing |
CX SIP Proxy for routing |
|
Cisco Connector query |
Complex multi-table JOIN |
Simpler query structure |
---
20.4 Complete UCCE Call Event Flow — Inbound Call
PHASE 1: CUSTOMER DIALS
═══════════════════════════════════════════════════════════════════════════════
[Customer Phone]
│
│ Dials DN (e.g., 8001)
▼
[PSTN Carrier] ──SIP INVITE──► [CUBE/SBC]
│ │
│ │ 1. Normalizes SIP headers
│ │ 2. Applies security policies
│ │ 3. Forwards to CUCM
│ ▼
│ [CUCM]
│ │
│ │ Route Pattern matches 8001
│ │ → Route List → SIP Trunk to CVP
│ ▼
│ [CVP — Ingress Gateway]
│ │
│ │ 1. Answers call (200 OK)
│ │ 2. Starts VXML application
│ │ 3. Plays welcome/IVR prompts
│ │ 4. Collects input (DTMF/speech)
│ ▼
│ [CVP — VXML Server]
│ │
│ │ Executes ICM script:
│ │ - "Check business hours"
│ │ - "Route to Sales CSQ"
│ │ - "Send label to ICM"
│ ▼
│ [ICM — Route Request]
│ │
│ │ 1. ICM evaluates routing script
│ │ 2. Finds available agent in Sales
│ │ 3. Returns route label to CVP
│ ▼
│ [CVP — Transfer to Agent]
│ │
│ │ 1. Initiates transfer to agent DN
│ │ 2. Customer hears ringback/queue music
│ ▼
│ [CUCM]
│ │
│ │ Sends SIP INVITE to Agent Phone
│ ▼
│ [Agent Phone — RINGING]
│ │
│ │ BiB activates:
│ │ • Creates forked RTP stream
│ │ • Sends to Recording SIP Trunk
│ ▼
│ [CUCM ──SIP INVITE──► Media Server]
│
═══════════════════════════════════════════════════════════════════════════════
PHASE 2: AGENT ANSWERS (CX takes over)
═══════════════════════════════════════════════════════════════════════════════
[Agent Phone]
│
│ Agent answers call
▼
[CUCM]
│
│ 1. Media path established: Customer ↔ Agent
│ 2. BiB fork active: Agent audio → Media Server
│ 3. JTAPI CallCtlTermConnTalkingEv fires
│
├─► JTAPI Event ──► [JTAPI Provider]
│ │
│ │ CallCtlTermConnTalkingEv
│ │ { callId, agentExtension, eventType: "TALKING" }
│ ▼
│ [Cisco Connector — JtapiListenerService]
│ │
│ │ 1. Persists event to jtapi_event table
│ │ 2. Event enters correlation queue
│ ▼
│ [Cisco Connector — CorrelationService]
│ │
│ │ (runs on schedule, e.g., every 30s)
│ │ 1. Fetches unprocessed JTAPI events
│ │ 2. Groups by globalCallId
│ │ 3. Detects call type = SIMPLE
│ │ 4. Builds CallLeg:
│ │ - startTime = ConnConnectedEv.timestamp
│ │ - endTime = ConnDisconnectedEv.timestamp (future)
│ │ - agentExtension = calledExtension
│ │ - callType = SIMPLE
│ │ 5. Saves CallLeg to database
│ ▼
│ [Cisco Connector — ConversationService]
│ │
│ │ (runs on schedule, e.g., every 60s)
│ │ 1. Fetches unsynced CallLegs
│ │ 2. Queries UCCE AW database (awdb):
│ │ SELECT tcd.*, a.EnterpriseName
│ │ FROM Termination_Call_Detail tcd
│ │ LEFT JOIN Agent a ON tcd.AgentSkillTargetID = a.SkillTargetID
│ │ WHERE tcd.PeripheralCallKey = {jtapiId}
│ │ 3. Enriches CallLeg:
│ │ - serviceIdentifier = tcd.DigitsDialed
│ │ - customerIdentifier = tcd.ANI
│ │ - agentName = a.EnterpriseName
│ │ - agentId = tcd.AgentSkillTargetID
│ │ - wrapUps = tcd.WrapupData
│ │ 4. Builds CIM messages:
│ │ - CALL_LEG_STARTED (if not already sent)
│ │ - CALL_LEG_ENDED (when ended)
│ │ - WRAPUP (when available)
│ ▼
│ [CCM — receives CIM]
│ │
│ │ Updates conversation activities
│ │ Adds voice recording metadata
│ ▼
│ [CCM Activities Timeline]
│ │
│ │ Shows: Call started, Agent assigned, Wrap-up code
│
├─► Finesse Event ──► [Finesse Server]
│ │
│ │ Publishes XMPP event:
│ │ <Update>
│ │ <Dialog>
│ │ <state>ACTIVE</state>
│ │ <fromAddress>+18005550199</fromAddress>
│ │ <toAddress>1005</toAddress>
│ │ </Dialog>
│ │ </Update>
│ ▼
│ [AgentDesk — cti.service.ts]
│ │
│ │ 1. Receives XMPP update via BOSH
│ │ 2. Maps Finesse state to CX MRD state
│ │ ACTIVE → BUSY
│ │ 3. Updates Angular UI
│ │ 4. No additional CIM needed (JTAPI handles it)
│
├─► BiB Stream ──► [CUCM ──SIP INVITE──► Media Server]
│ │
│ │ FreeSWITCH receives BiB leg
│ │ 1. Accepts SIP INVITE
│ │ 2. Starts recording via record.lua
│ │ 3. Writes raw WAV: /streams/{gcid}_{timestamp}.wav
│ ▼
│ [Media Server — Recording in progress]
│ │
│ │ Raw audio written continuously
│ │ Metadata collected: GCID, ANI, agent, timestamps
│
═══════════════════════════════════════════════════════════════════════════════
PHASE 3: CALL ENDS
═══════════════════════════════════════════════════════════════════════════════
[Agent Phone or Customer]
│
│ Either party hangs up
▼
[CUCM]
│
│ 1. Media path torn down
│ 2. BiB fork torn down
│ 3. JTAPI ConnDisconnectedEv fires
│
├─► JTAPI Event ──► [Cisco Connector]
│ │
│ │ ConnDisconnectedEv persisted
│ │ CorrelationService matches with start event
│ │ CallLeg.endTime = now()
│ │ CallLeg.duration = calculated
│ │ ConversationService queries UCCE for wrap-up
│ │ Sends CALL_LEG_ENDED + WRAPUP CIM to CCM
│
├─► BiB End ──► [Media Server]
│ │
│ │ 1. FreeSWITCH receives BYE
│ │ 2. Stops recording
│ │ 3. Closes WAV file
│ │ 4. RECORD_STOP event fires
│ │ 5. cx_hangup.lua posts to Mixer:
│ │ POST /mixer/sip-data
│ │ { callId, filePath, duration, ... }
│ ▼
│ [Mixer — Queue job]
│ │
│ │ 1. Job queued in RabbitMQ
│ │ 2. Worker picks up job
│ │ 3. Evaluates recording rules
│ │ 4. Mixes audio (stereo/mono)
│ │ 5. Writes final file to NAS
│ ▼
│ [NAS / Shared Storage]
│ │
│ │ Final recording: {date}/{recordingId}_mixed.wav
│ │ Metadata JSON: {date}/{recordingId}.json
│
├─► Finesse Event ──► [AgentDesk]
│ │
│ │ Dialog state = DROPPED
│ │ Wrap-up panel appears (if configured)
│ │ Agent selects wrap-up code
│ │ Agent state transitions: TALKING → WORK → READY
│
├─► PG Event ──► [ICM]
│ │
│ │ Call disposition recorded
│ │ TCD written to awdb
│ │ { WrapupData, CallResult, TalkTime, ... }
│
├─► PG Event ──► [AW / HDS]
│ │
│ │ TCD replicated to historical store
│ │ Available for reporting
│
═══════════════════════════════════════════════════════════════════════════════
PHASE 4: RECORDING LINK PUSHED TO CCM
═══════════════════════════════════════════════════════════════════════════════
[Recording Link Activities — Scheduled Job]
│
│ Runs every N minutes (configurable)
▼
1. Queries CCM Voice Activities API
GET /conversation-manager/activities/voice?startTime=...&endTime=...
2. For each voice call leg:
- Gets callId, agentExtension, startTime
3. Queries NAS / Mixer database for matching recording:
- Match by callId + agentExtension + time window
4. Builds recording URL:
https://middleware/{callId}:{agent}:{ani}:{epoch}/recording-file
5. Pushes to CCM:
POST /conversation-manager/activities/third-party
CIM: intent = "VOICE_RECORDING"
{ legId, voiceRecordingUrl }
6. CCM adds VOICE_RECORDING activity to conversation timeline
7. Agent/supervisor can click URL to play recording
```
---
20.5 Complete UCCX Call Event Flow — Inbound Call
PHASE 1: CUSTOMER DIALS
═══════════════════════════════════════════════════════════════════════════════
[Customer Phone]
│
│ Dials DN (e.g., 8001)
▼
[PSTN] ──SIP──► [CUBE] ──SIP──► [CUCM]
│ │
│ │ Route Pattern 8001 → UCCX Trigger
│ ▼
│ [UCCX Engine]
│ │
│ │ 1. UCCX script executes
│ │ 2. Plays welcome prompt
│ │ 3. Routes to CSQ (e.g., "Sales")
│ │ 4. Customer hears queue music
│ │ 5. UCCX reserves agent via CUCM
│ ▼
│ [CUCM]
│ │
│ │ SIP INVITE to Agent Phone
│ ▼
│ [Agent Phone — RINGING]
│ │
│ │ BiB activates
│ │ Forked stream to Recording SIP Trunk
│ ▼
│ [CUCM ──SIP──► Media Server]
│
═══════════════════════════════════════════════════════════════════════════════
PHASE 2: AGENT ANSWERS (CX takes over)
═══════════════════════════════════════════════════════════════════════════════
[Agent Phone]
│
│ Agent answers
▼
[CUCM]
│
│ 1. Media: Customer ↔ Agent
│ 2. BiB fork: Agent audio → Media Server
│ 3. JTAPI CallCtlTermConnTalkingEv
│
├─► JTAPI Event ──► [Cisco Connector]
│ │
│ │ Same flow as UCCE:
│ │ JtapiListenerService → CorrelationService
│ │ → ConversationService → CCM
│
├─► Finesse Event ──► [AgentDesk]
│ │
│ │ UCCX embedded Finesse publishes XMPP
│ │ AgentDesk receives via BOSH
│ │ State: RESERVED → TALKING → BUSY
│
├─► BiB Stream ──► [Media Server]
│ │
│ │ FreeSWITCH records raw WAV
│ │ Same behavior as UCCE
│
═══════════════════════════════════════════════════════════════════════════════
PHASE 3: CALL ENDS
═══════════════════════════════════════════════════════════════════════════════
[Hangup]
│
▼
[CUCM]
│
│ JTAPI ConnDisconnectedEv
│
├─► Cisco Connector:
│ - Matches start/end events
│ - Queries UCCX database (db_cra):
│ SELECT ccd.contactid, ccd.originatordn, ccd.callednumber,
│ acd.callwrapupdata, r.resourcename, r.extension
│ FROM ContactCallDetail ccd
│ LEFT JOIN AgentConnectionDetail acd ON acd.contactid = ccd.contactid
│ LEFT JOIN Resource r ON r.resourceid = acd.resourceid
│ WHERE ccd.contactid = {jtapiId}
│ - Enriches CallLeg with UCCX data
│ - Sends CALL_LEG_ENDED + WRAPUP to CCM
│
├─► Media Server:
│ - Stops recording
│ - Posts to Mixer
│ - Mixer processes → final file on NAS
│
├─► Finesse:
│ - Agent state: TALKING → WORK → READY
│ - Wrap-up panel (if configured)
│
├─► UCCX Engine:
│ - Wrap-up code stored in db_cra
│ - CSQ statistics updated
│ - Contact result recorded
│
═══════════════════════════════════════════════════════════════════════════════
PHASE 4: RECORDING LINK PUSHED
═══════════════════════════════════════════════════════════════════════════════
[Recording Link Activities]
│
│ Same flow as UCCE
│ Queries CCM → matches recordings → pushes URLs
│
▼
[CCM Activities Timeline]
│
│ Shows: Call events, Wrap-up, Recording link
```
---
20.6 Cisco Connector Database Queries — Complete Reference
20.6.1 UCCE Query (Termination_Call_Detail)
-- Complete UCCE query used by Cisco Connector
SELECT
tcd.ANI,
tcd.DigitsDialed,
tcd.WrapupData AS WrapUps,
tcd.AgentSkillTargetID AS AgentID,
tcd.PeripheralCallKey AS JtapiId,
tcd.CallTerminatedDateTimeUTC AS endDateTime,
a.EnterpriseName AS AgentName,
tcd.InstrumentPortNumber AS extension,
dd.LastName as callId,
dd.CallResult as callResult,
CASE
WHEN tcd.PeripheralCallType IN (7,17,18,27,28,29,30,31,32,33,34,35,36,37,41)
THEN 'OUTBOUND'
ELSE 'INBOUND'
END AS conversationType,
tcd.TalkTime,
tcd.HoldTime,
tcd.WorkTime,
tcd.RingTime,
tcd.QueueTime,
tcd.RecoveryKey,
tcd.CallDisposition,
tcd.CallDispositionFlag,
tcd.NetworkTargetID,
tcd.ServiceTargetID,
tcd.PeripheralID
FROM
Termination_Call_Detail AS tcd
LEFT JOIN
Agent AS a ON tcd.AgentSkillTargetID = a.SkillTargetID
LEFT JOIN
Dialer_Detail AS dd ON tcd.PeripheralCallKey = dd.PeripheralCallKey
WHERE
tcd.PeripheralCallKey IN (...)
AND tcd.CallTerminatedDateTimeUTC BETWEEN '{start}' AND '{end}'
ORDER BY
tcd.CallTerminatedDateTimeUTC;
Key UCCE Fields:
|
Field |
Description |
CX Usage |
|---|---|---|
|
|
Calling number |
customerIdentifier |
|
|
Dialed number |
serviceIdentifier |
|
|
Wrap-up code |
WRAPUP CIM |
|
|
Agent ID |
agentId |
|
|
JTAPI call ID |
Matches jtapi_event.jtapiId |
|
|
End time |
Matches leg endTime |
|
|
Agent name |
agentName |
|
|
Extension |
agentExtension |
|
|
Talk duration seconds |
Duration validation |
|
|
Hold duration seconds |
Recording segment calculation |
|
|
Inbound/outbound flag |
conversationType |
20.6.2 UCCX Query (ContactCallDetail)
-- Complete UCCX query used by Cisco Connector
SELECT
ccd.contactid AS JtapiId,
ccd.originatordn AS ANI,
ccd.callednumber AS DigitsDialed,
acd.callwrapupdata AS WrapUps,
COALESCE(ccd.enddatetime, cl.enddatetime, acd.enddatetime) as enddatetime,
r.resourceid AS AgentId,
r.resourcename AS AgentName,
r.extension,
dl.lastname as CallId,
ccd.sessionid,
ccd.sessionseqnum,
ccd.nodeid,
ccd.profileid,
ccd.applicationname,
ccd.connecttime,
ccd.disconnecttime,
ccd.customvariable1,
ccd.customvariable2,
ccd.customvariable3,
ccd.customvariable4,
ccd.customvariable5,
ccd.customvariable6,
ccd.customvariable7,
ccd.customvariable8,
ccd.customvariable9,
ccd.customvariable10
FROM
ContactCallDetail ccd
LEFT JOIN
AgentConnectionDetail acd ON acd.contactid = ccd.contactid
LEFT JOIN
ConsultLegDetail cl ON cl.contactid = ccd.contactid
LEFT JOIN
Resource r ON r.resourceid = COALESCE(acd.resourceid, cl.destinationresourceid, ccd.originatorid)
LEFT JOIN
DialingList dl ON dl.dialinglistid = acd.dialinglistid
WHERE
ccd.contactid IN (...)
AND COALESCE(ccd.enddatetime, cl.enddatetime, acd.enddatetime)
BETWEEN '{start}' AND '{end}'
ORDER BY
COALESCE(ccd.enddatetime, cl.enddatetime, acd.enddatetime);
Key UCCX Fields:
|
Field |
Description |
CX Usage |
|---|---|---|
|
|
JTAPI call ID |
Matches jtapiId |
|
|
Calling number |
customerIdentifier |
|
|
Dialed number |
serviceIdentifier |
|
|
Wrap-up code |
WRAPUP CIM |
|
|
Agent ID |
agentId |
|
|
Agent name |
agentName |
|
|
Extension |
agentExtension |
|
|
Call connect time |
Leg startTime validation |
|
|
Call disconnect time |
Leg endTime |
|
|
Custom call variables |
Screen-pop data |
|
|
UCCX session |
Correlation |
---
20.7 ICM Script Behavior (UCCE Only)
20.7.1 Typical ICM Inbound Script
[START]
│
▼
[Check Business Hours]
│
├─► Outside hours ──► [Play Closed Message] ──► [HANGUP]
│
▼
[Check VIP Flag]
│
├─► VIP = true ──► [Priority Queue] ──► [Queue to VIP Skill Group]
│
▼
[Select Skill Group]
│
├─► DN = 8001 ──► [Sales Skill Group]
├─► DN = 8002 ──► [Support Skill Group]
└─► DN = 8003 ──► [Billing Skill Group]
│
▼
[Queue Call]
│
├─► Agent available ──► [Route to Agent] ──► [CONNECT]
├─► Queue full ──► [Play Queue Full] ──► [Voicemail]
└─► Wait time > 300s ──► [Offer Callback] ──► [Callback Queue]
│
▼
[Agent Answers]
│
▼
[Wrap-up]
│
▼
[END]
20.7.2 ICM Script to CX Interactions
|
ICM Node |
CX Impact |
Data Flow |
|---|---|---|
|
|
Determines queue name |
DN → Skill Group → CX Queue |
|
|
Call enters CX Routing Engine |
ICM queues internally; CX also queues |
|
|
Agent reserved in CX |
ICM selects agent → CUCM connects → CX notified |
|
|
Wrap-up code collected |
ICM stores → UCCE DB → Cisco Connector → CCM |
|
|
Callback request |
ICM schedules → Callback engine → CX outbound dialer |
Important: In UCCE deployments, ICM makes the routing decision, not CX. CX receives the routed call post-decision. The Routing Engine in CX primarily manages agent MRD states and task lifecycle, but the actual agent selection logic may be driven by ICM.
---
20.8 CVP Call Flow (UCCE Only)
[Customer dials DN]
│
▼
[CUCM Route Pattern] ──► [CVP Ingress Gateway]
│
▼
[CVP Call Server]
│
├─► 1. Answers call (SIP 200 OK)
├─► 2. Requests route from ICM (Route Request)
├─► 3. ICM returns route label
│
├─► If label = "RUN_VXML_APP":
│ │
│ ▼
│ [CVP VXML Server]
│ │
│ ├─► Plays IVR prompts (VXML)
│ ├─► Collects DTMF/speech input
│ ├─► Validates input
│ ├─► May transfer to self-service application
│ └─► Returns to ICM for routing
│
├─► If label = "QUEUE":
│ │
│ ▼
│ [CVP Queue]
│ │
│ ├─► Customer hears queue music
│ ├─► Periodic "your position in queue" announcements
│ └─► ICM monitors for available agent
│
├─► If label = "AGENT_DN":
│ │
│ ▼
│ [Transfer to Agent]
│ │
│ ├─► CVP sends REFER or re-INVITE
│ ├─► CUCM connects to agent phone
│ └─► Agent answers
│
▼
[Call ends]
CVP to CX SIP Proxy Integration:
When CX SIP Proxy is used with CVP:
[CUBE] ──SIP──► [CX SIP Proxy] ──SIP──► [CUCM]
│
│ SIP Proxy routes:
│ - Inbound: CUBE → CVP
│ - Outbound: CUCM → CUBE
│ - Recording: CUCM → Media Server
│
▼
[CX SIP Proxy Config]
- Group: CVP servers
- Group: CUCM servers
- Group: Media Server
- Patterns: DN-based routing
---
20.9 Finesse Gadget Integration
20.9.1 Finesse Desktop Layout
┌─────────────────────────────────────────────────────────────┐
│ CISCO FINESSE DESKTOP │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Cisco │ │ CX │ │ Screen │ │
│ │ Gadget │ │ AgentDesk │ │ Recording │ │
│ │ (CTI) │ │ (iframe) │ │ Gadget │ │
│ │ │ │ │ │ │ │
│ │ • Agent │ │ • Call │ │ • Record │ │
│ │ state │ │ controls │ │ toggle │ │
│ │ • Queue │ │ • CRM │ │ • Status │ │
│ │ stats │ │ screen │ │ display │ │
│ │ • Wrap-up │ │ • Customer │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
20.9.2 Finesse to AgentDesk Communication
[Finesse Server]
│
│ XMPP BOSH
▼
[cti-js-library] (loaded by AgentDesk)
│
│ Events: agentStateChanged, newDialog, dialogStateChanged
▼
[Angular AgentDesk]
│
├─► Updates agent state UI
├─► Shows incoming call popup
├─► Enables/disables buttons
└─► Triggers screen pop
Finesse Gadget Configuration XML:
<!-- Finesse desktop layout -->
<layout>
<gadgets>
<gadget>
<url>https://finesse-server/finesse/gadget/AgentGreeting</url>
<height>100</height>
</gadget>
<gadget>
<url>https://cx-server/agentdesk</url>
<height>600</height>
</gadget>
<gadget>
<url>https://vrs-server/screen-recording-gadget</url>
<height>100</height>
</gadget>
</gadgets>
</layout>
---
20.10 Complete Component Interaction Matrix
|
Source |
Destination |
Protocol |
Message/Event |
When |
|---|---|---|---|---|
|
Customer Phone |
PSTN |
TDM/SIP |
Voice call |
Call initiation |
|
PSTN |
CUBE |
SIP/RTP |
INVITE, RTP |
Call routing |
|
CUBE |
CUCM |
SIP/RTP |
INVITE, RTP |
Call routing |
|
CUCM |
CVP (UCCE) |
SIP |
INVITE |
IVR routing |
|
CVP |
ICM (UCCE) |
MRCP/ICM |
Route Request |
Routing decision |
|
ICM |
CVP (UCCE) |
ICM |
Route Label |
Agent selection |
|
CVP |
CUCM |
SIP |
REFER/re-INVITE |
Transfer to agent |
|
CUCM |
Agent Phone |
SIP/RTP |
INVITE, RTP |
Call delivery |
|
CUCM |
Media Server |
SIP/RTP |
INVITE (BiB), RTP |
Recording fork |
|
Agent Phone |
CUCM |
SIP/RTP |
200 OK, RTP |
Answer call |
|
CUCM |
JTAPI Provider |
JTAPI |
Call events |
Real-time |
|
JTAPI Provider |
Cisco Connector |
JTAPI |
CallConnected, CallDisconnected |
Real-time |
|
Cisco Connector |
Local DB |
JDBC |
INSERT jtapi_event |
Real-time |
|
Cisco Connector |
UCCE AW |
JDBC |
SELECT TCD |
Periodic (30-60s) |
|
Cisco Connector |
UCCX DB |
JDBC |
SELECT CCD |
Periodic (30-60s) |
|
Cisco Connector |
CCM |
HTTPS/REST |
CIM Messages |
Periodic |
|
CCM |
Routing Engine |
HTTPS/REST |
AssignResourceRequest |
Real-time |
|
Routing Engine |
CCM |
HTTPS/REST |
AGENT_RESERVED |
Real-time |
|
CCM |
Voice Connector |
HTTPS/REST |
CIM Messages |
Real-time |
|
Voice Connector |
FreeSWITCH |
ESL |
uuid_transfer |
Real-time |
|
FreeSWITCH |
Agent Desktop |
SIP/WebSocket |
INVITE via WSS |
Real-time |
|
Agent Desktop |
FreeSWITCH |
SIP/WebSocket |
200 OK via WSS |
Real-time |
|
Finesse |
Agent Desktop |
BOSH/XMPP |
Agent state events |
Real-time |
|
Agent Desktop |
CCM |
HTTPS/REST |
CALL_LEG_STARTED/ENDED |
Real-time |
|
Media Server |
Mixer |
HTTPS/REST |
POST /mixer/sip-data |
Post-call |
|
Mixer |
NAS |
File I/O |
Write mixed WAV |
Post-call |
|
Recording Link Activities |
CCM |
HTTPS/REST |
VOICE_RECORDING |
Scheduled |
|
Recording Middleware |
NAS |
File I/O |
Read WAV file |
On-demand |
---
20.11 Summary: UCCE vs. UCCX Integration Differences
|
Aspect |
UCCE Integration |
UCCX Integration |
|---|---|---|
|
Cisco Connector DB |
|
|
|
Call detail table |
|
|
|
Agent table |
|
|
|
Correlation key |
|
|
|
Wrap-up field |
|
|
|
IVR |
CVP (separate) |
Built-in UCCX |
|
Routing |
ICM (complex scripts) |
UCCX (simpler scripts) |
|
Finesse |
Separate servers |
Embedded in UCCX |
|
TCD query complexity |
Complex (6+ table JOIN) |
Simpler (4 table JOIN) |
|
Scale |
Large enterprise |
Mid-market |
|
CX components affected |
Cisco Connector query only |
Cisco Connector query only |
|
All other CX components |
Same behavior |
Same behavior |
Critical Point: From the CX perspective, the only difference between UCCE and UCCX is the database schema and query used by the Cisco Connector. All other CX components (CCM, Routing Engine, Voice Connector, Agent Desktop, Recording Middleware) behave identically regardless of whether the backend is UCCE or UCCX.
---
## 21. Cisco Event Ingestion, Correlation & Call Leg Handling — Complete Deep Dive
This section provides exhaustive detail on how every Cisco event enters CX components, how events are correlated into call legs and conversations, and how different call types produce different leg structures. This is the definitive reference for debugging event flow issues.
---
21.1 Event Sources & Entry Points into CX
CX receives events from Cisco infrastructure through three distinct ingress channels:
┌─────────────────────────────────────────────────────────────────────────────┐
│ CISCO EVENT INGRESS INTO CX │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ CHANNEL 1: JTAPI (Primary — Real-time Call Control) │
│ ═══════════════════════════════════════════════════ │
│ │
│ [CUCM JTAPI Provider] │
│ │ │
│ │ JTAPI events (TCP, port 2748) │
│ ▼ │
│ [Cisco Connector — JtapiListenerService] │
│ │ │
│ │ Saves to jtapi_event table │
│ ▼ │
│ [PostgreSQL — cisco_connector schema] │
│ │
│ CHANNEL 2: FINESSE XMPP (Agent State & Dialog Events) │
│ ═══════════════════════════════════════ │
│ │
│ [Finesse Server] │
│ │ │
│ │ XMPP over BOSH (HTTPS, port 5222/8445) │
│ ▼ │
│ [AgentDesk — cti.service.ts] │
│ │ │
│ │ Updates agent MRD state │
│ ▼ │
│ [Routing Engine — PUT /voice/agents/{id}] │
│ │
│ CHANNEL 3: DATABASE POLL (Post-call Enrichment) │
│ ════════════════════════════════════════════ │
│ │
│ [UCCE AW / UCCX db_cra] │
│ │ │
│ │ JDBC SQL queries (scheduled, 30-60s intervals) │
│ ▼ │
│ [Cisco Connector — ConversationService] │
│ │ │
│ │ Enriches CallLeg, sends CIM to CCM │
│ ▼ │
│ [CCM — Conversation Manager] │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
---
21.2 JTAPI Event Deep Dive — Every Event Type
21.2.1 JTAPI Event Taxonomy
JTAPI events are Java objects fired by the Cisco JTAPI Provider. The Cisco Connector subscribes to these events and persists them. Here is the complete event hierarchy:
JTAPI Event Hierarchy
├── javax.telephony.events.Ev
│ ├── javax.telephony.events.TermEv
│ │ ├── javax.telephony.events.TermConnEv
│ │ │ ├── CallCtlTermConnTalkingEv ← AGENT ANSWERED
│ │ │ ├── CallCtlTermConnHeldEv ← AGENT PUT ON HOLD
│ │ │ ├── CallCtlTermConnQueuedEv ← CALL QUEUED
│ │ │ ├── CallCtlTermConnRingingEv ← AGENT PHONE RINGING
│ │ │ ├── CallCtlTermConnDroppedEv ← AGENT HUNG UP
│ │ │ ├── CallCtlTermConnUnknownEv ← UNEXPECTED STATE
│ │ │ └── CallCtlTermConnBridgedEv ← CALL BRIDGED (CONFERENCE)
│ │ ├── javax.telephony.events.AddrEv
│ │ │ ├── CallCtlConnOfferedEv ← CALL OFFERED TO AGENT
│ │ │ ├── CallCtlConnEstablishedEv ← CONNECTION ESTABLISHED
│ │ │ ├── CallCtlConnDisconnectedEv ← CALLER DISCONNECTED
│ │ │ ├── CallCtlConnAlertingEv ← ALERTING/RINGING
│ │ │ ├── CallCtlConnNetworkAlertingEv ← NETWORK ALERTING
│ │ │ ├── CallCtlConnNetworkReachedEv ← NETWORK REACHED
│ │ │ ├── CallCtlConnQueuedEv ← QUEUED AT DESTINATION
│ │ │ ├── CallCtlConnInitiatedEv ← CALL INITIATED
│ │ │ ├── CallCtlConnDialingEv ← DIALING
│ │ │ ├── CallCtlConnFailedEv ← CALL FAILED
│ │ │ └── CallCtlConnUnknownEv ← UNKNOWN STATE
│ │ ├── CallObservationEndedEv ← OBSERVATION ENDED
│ │ └── CiscoTermInServiceEv ← TERMINAL IN SERVICE
│ ├── javax.telephony.events.CallEv
│ │ ├── CallActiveEv ← CALL ACTIVATED
│ │ ├── CallInvalidEv ← CALL INVALIDATED
│ │ ├── ConnCreatedEv ← CONNECTION CREATED
│ │ ├── ConnDestroyedEv ← CONNECTION DESTROYED
│ │ ├── TermConnCreatedEv ← TERM CONNECTION CREATED
│ │ ├── TermConnDroppedEv ← TERM CONNECTION DROPPED
│ │ ├── CallCtlCallOfferedEv ← CALL OFFERED
│ │ ├── CallCtlCallDialingEv ← CALL DIALING
│ │ ├── CallCtlCallEstablishedEv ← CALL ESTABLISHED
│ │ ├── CallCtlCallQueuedEv ← CALL QUEUED
│ │ ├── CallCtlCallAlertingEv ← CALL ALERTING
│ │ ├── CallCtlCallActiveEv ← CALL ACTIVE
│ │ ├── CallCtlCallInvalidEv ← CALL INVALID
│ │ ├── CallCtlCallDisconnectedEv ← CALL DISCONNECTED
│ │ ├── CallCtlConferencedEv ← CALL CONFERENCED
│ │ ├── CallCtlConnNetworkReachedEv ← NETWORK REACHED
│ │ └── CallCtlConnNetworkAlertingEv ← NETWORK ALERTING
│ └── javax.telephony.events.ProviderEv
│ ├── ProviderInServiceEv ← PROVIDER IN SERVICE
│ ├── ProviderOutOfServiceEv ← PROVIDER OUT OF SERVICE
│ ├── ProviderShutdownEv ← PROVIDER SHUTDOWN
│ └── AddrAddedEv / AddrRemovedEv ← ADDRESS CHANGES
21.2.2 Critical Events for CX — Full Detail
|
Event |
Fired When |
CX Action |
Data Extracted |
|---|---|---|---|
|
CallCtlTermConnTalkingEv |
Agent answers call |
Create CallLeg start |
|
|
CallCtlTermConnRingingEv |
Agent phone ringing |
Pre-allocate leg (optional) |
|
|
ConnDisconnectedEv |
Any party hangs up |
Mark CallLeg end |
|
|
CallCtlTermConnDroppedEv |
Agent connection dropped |
Finalize leg |
|
|
CallCtlTermConnHeldEv |
Agent presses hold |
Record hold segment |
|
|
CallCtlConferencedEv |
Conference created |
Detect multi-party |
|
|
CallCtlCallInvalidEv |
Call aborted unexpectedly |
Error logging |
|
21.2.3 JTAPI Event Object Structure (What CX Extracts)
When the Cisco Connector receives a JTAPI event, it extracts these fields:
// Pseudo-code of what JtapiListenerService extracts
public class JtapiEventExtractor {
public JtapiEventRecord extract(Event event) {
JtapiEventRecord record = new JtapiEventRecord();
// Core identifiers
record.setGlobalCallId(extractGlobalCallId(event));
record.setCallId(event.getCall().getCallID().getCallManagerID() + "_" +
event.getCall().getCallID().getGlobalCallID());
// Event metadata
record.setEventType(event.getID()); // e.g., 107 = CallCtlTermConnTalkingEv
record.setEventName(event.toString()); // "CallCtlTermConnTalkingEv"
record.setTimestamp(Instant.now());
// Terminal/Address info
if (event instanceof TermConnEv) {
TermConnEv tce = (TermConnEv) event;
record.setTerminalName(tce.getTerminal().getName()); // "SEP123456789ABC"
record.setAddress(tce.getTerminalConnection().getConnection()
.getAddress().getName()); // "1005"
record.setAgentExtension(tce.getTerminalConnection()
.getConnection().getAddress().getName());
}
// Call info
record.setCallState(event.getCall().getState());
record.setConnectionCount(event.getCall().getConnections().length);
// Cisco-specific extensions
if (event instanceof CiscoCallEv) {
CiscoCallEv cce = (CiscoCallEv) event;
record.setCiscoCause(cce.getCiscoCause()); // CAUSE_NORMAL_CLEARING, etc.
record.setCiscoFeatureReason(cce.getCiscoFeatureReason()); // REASON_TRANSFER, etc.
}
return record;
}
}
Sample persisted JTAPI event row:
|
Column |
Example Value |
Description |
|---|---|---|
|
|
152347 |
Auto-increment |
|
|
|
CUCM call manager ID + global call ID |
|
|
|
Unique per call |
|
|
|
JTAPI numeric event type |
|
|
|
Human-readable event name |
|
|
|
Agent's DN |
|
|
|
Phone MAC |
|
|
|
Event time |
|
|
|
Disconnect reason |
|
|
|
Why state changed |
|
|
|
Call state at event time |
|
|
|
Number of connections |
|
|
|
Correlation flag |
|
|
|
DB insertion time |
---
21.3 Correlation Engine — Complete Algorithm
21.3.1 High-Level Correlation Flow
[JTAPI Events Table] ──► [CorrelationService] ──► [CallLegs Table]
│
│ 1. Poll unprocessed events (every 30s)
│ 2. Group by globalCallId
│ 3. Sort by timestamp
│ 4. Detect call type
│ 5. Build legs
│ 6. Mark events processed
▼
[CallLeg Objects]
│
│ 7. Save to call_leg table
│ 8. Queue for enrichment
▼
[ConversationService]
│
│ 9. Poll unsynced legs (every 60s)
│ 10. Query UCCE/UCCX DB
│ 11. Enrich with wrap-up, ANI, etc.
│ 12. Build CIM messages
▼
[CCM via REST]
21.3.2 Correlation Algorithm — Step by Step
Step 1: Fetch Unprocessed Events
-- What CorrelationService executes
SELECT * FROM jtapi_event
WHERE processed = false
ORDER BY global_call_id, timestamp
LIMIT 1000;
Step 2: Group by Global Call ID
Events are grouped by global_call_id. Each group represents one logical call through CUCM.
Global Call ID: 167890
├── Event 1: CallCtlTermConnRingingEv 09:23:42 ext=1005
├── Event 2: CallCtlTermConnTalkingEv 09:23:45 ext=1005 ← LEG START
├── Event 3: CallCtlTermConnHeldEv 09:25:10 ext=1005
├── Event 4: CallCtlTermConnTalkingEv 09:25:30 ext=1005 ← RESUME
├── Event 5: ConnDisconnectedEv 09:28:15 ext=1005 ← LEG END
└── Event 6: CallCtlTermConnDroppedEv 09:28:15 ext=1005
Result: ONE CallLeg (1005, start=09:23:45, end=09:28:15)
```
Step 3: Detect Call Type from Event Pattern
The correlation engine analyzes the event sequence to determine the call type:
public CallType detectCallType(List<JtapiEvent> events) {
boolean hasConference = events.stream()
.anyMatch(e -> e.getEventType() == CallCtlConferencedEv.ID);
boolean hasTransfer = events.stream()
.anyMatch(e -> e.getCiscoFeatureReason() == REASON_TRANSFER);
boolean hasConsult = events.stream()
.anyMatch(e -> e.getCiscoFeatureReason() == REASON_CONSULT);
boolean hasMultipleAgents = events.stream()
.map(e -> e.getAgentExtension())
.filter(Objects::nonNull)
.distinct()
.count() > 1;
boolean hasOutbound = events.stream()
.anyMatch(e -> e.getCallState() == INITIATED &&
e.getAgentExtension() != null);
if (hasConference) return CallType.CONFERENCE;
if (hasConsult) return CallType.CONSULT;
if (hasTransfer && hasMultipleAgents) return CallType.TRANSFER;
if (hasOutbound) return CallType.OUTBOUND;
if (hasMultipleAgents) return CallType.TRANSFER; // blind transfer
return CallType.SIMPLE;
}
Step 4: Build CallLegs from Events
For each agent extension detected in the event group, a CallLeg is created:
public List<CallLeg> buildLegs(List<JtapiEvent> events, CallType callType) {
List<CallLeg> legs = new ArrayList<>();
// Group events by agent extension
Map<String, List<JtapiEvent>> byExtension = events.stream()
.filter(e -> e.getAgentExtension() != null)
.collect(Collectors.groupingBy(JtapiEvent::getAgentExtension));
for (Map.Entry<String, List<JtapiEvent>> entry : byExtension.entrySet()) {
String extension = entry.getKey();
List<JtapiEvent> extEvents = entry.getValue();
// Find start event (first TalkingEv for this extension)
JtapiEvent startEvent = extEvents.stream()
.filter(e -> e.getEventType() == CallCtlTermConnTalkingEv.ID)
.findFirst()
.orElse(null);
// Find end event (last Disconnect/Dropped for this extension)
JtapiEvent endEvent = extEvents.stream()
.filter(e -> e.getEventType() == ConnDisconnectedEv.ID ||
e.getEventType() == CallCtlTermConnDroppedEv.ID)
.reduce((first, second) -> second) // get last
.orElse(null);
if (startEvent != null) {
CallLeg leg = new CallLeg();
leg.setJtapiId(events.get(0).getGlobalCallId());
leg.setAgentExtension(extension);
leg.setStartTime(startEvent.getTimestamp());
leg.setEndTime(endEvent != null ? endEvent.getTimestamp() : null);
leg.setCallType(callType);
leg.setStatus(endEvent != null ? "COMPLETED" : "IN_PROGRESS");
// Calculate hold segments
List<HoldSegment> holds = extractHoldSegments(extEvents);
leg.setHoldSegments(holds);
legs.add(leg);
}
}
return legs;
}
private List<HoldSegment> extractHoldSegments(List<JtapiEvent> events) {
List<HoldSegment> holds = new ArrayList<>();
Instant holdStart = null;
for (JtapiEvent event : events) {
if (event.getEventType() == CallCtlTermConnHeldEv.ID) {
holdStart = event.getTimestamp();
} else if (event.getEventType() == CallCtlTermConnTalkingEv.ID
&& holdStart != null) {
holds.add(new HoldSegment(holdStart, event.getTimestamp()));
holdStart = null;
}
}
return holds;
}
```
21.3.3 CallLeg Data Model
-- call_leg table schema
CREATE TABLE call_leg (
id BIGSERIAL PRIMARY KEY,
jtapi_id VARCHAR(50) NOT NULL, -- Global call ID from CUCM
agent_extension VARCHAR(20) NOT NULL, -- Agent DN
start_time TIMESTAMP NOT NULL, -- TalkingEv timestamp
end_time TIMESTAMP, -- DisconnectEv timestamp
duration_seconds INTEGER, -- Calculated
call_type VARCHAR(20), -- SIMPLE, TRANSFER, CONSULT, CONFERENCE, OUTBOUND, RONA
status VARCHAR(20), -- IN_PROGRESS, COMPLETED, FAILED, CORRELATION_PENDING
-- Enrichment fields (populated by ConversationService)
service_identifier VARCHAR(50), -- Dialed number / DNIS
customer_identifier VARCHAR(50), -- ANI / caller ID
agent_id VARCHAR(50), -- UCCE SkillTargetID / UCCX resourceid
agent_name VARCHAR(100), -- Agent name from DB
wrap_up_code VARCHAR(50), -- Wrap-up code
wrap_up_data TEXT, -- Wrap-up notes
-- Recording fields
recording_file_path VARCHAR(500), -- Path on NAS
recording_url VARCHAR(500), -- Middleware URL
recording_duration INTEGER, -- Actual recording length
-- Metadata
hold_segments JSONB, -- [{start, end}, ...]
cim_sent BOOLEAN DEFAULT FALSE, -- Whether CIM delivered to CCM
correlation_attempts INTEGER DEFAULT 0, -- Retry count
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Indexes for performance
CREATE INDEX idx_call_leg_jtapi ON call_leg(jtapi_id);
CREATE INDEX idx_call_leg_agent ON call_leg(agent_extension);
CREATE INDEX idx_call_leg_time ON call_leg(start_time, end_time);
CREATE INDEX idx_call_leg_status ON call_leg(status) WHERE status != 'COMPLETED';
```
---
21.4 Call Type Deep Dives — Different Leg Structures
21.4.1 SIMPLE Call (Inbound, One Agent)
EVENT SEQUENCE: SIMPLE CALL
═══════════════════════════════════════════════════════════════════════════════
Customer ──► CUCM ──► Agent 1005
JTAPI Events (single globalCallId = 167890):
┌────┬────────────────────────────┬──────────┬────────────┬────────────────────┐
│ # │ Event │ Time │ Extension │ Notes │
├────┼────────────────────────────┼──────────┼────────────┼────────────────────┤
│ 1 │ CallActiveEv │ 09:23:40 │ — │ Call created │
│ 2 │ CallCtlConnAlertingEv │ 09:23:40 │ 8001 │ DNIS ringing │
│ 3 │ CallCtlConnEstablishedEv │ 09:23:41 │ 8001 │ DNIS answered │
│ 4 │ TermConnCreatedEv │ 09:23:41 │ 1005 │ Agent phone added │
│ 5 │ CallCtlTermConnRingingEv │ 09:23:42 │ 1005 │ Agent phone rings │
│ 6 │ CallCtlTermConnTalkingEv │ 09:23:45 │ 1005 │ AGENT ANSWERS ←──┐ │
│ │ │ │ │ │ │
│ │ [TALKING PHASE — 5 minutes]│ │ │ │ │
│ │ │ │ │ │ │
│ 7 │ ConnDisconnectedEv │ 09:28:45 │ 8001 │ Caller hangs up │ │
│ 8 │ CallCtlTermConnDroppedEv │ 09:28:45 │ 1005 │ Agent dropped ←─┘ │
│ 9 │ CallInvalidEv │ 09:28:45 │ — │ Call destroyed │
└────┴────────────────────────────┴──────────┴────────────┴────────────────────┘
CORRELATION RESULT:
┌─────────────────────────────────────────────────────────────┐
│ CallLeg #1 │
│ jtapiId: 167890 │
│ extension: 1005 │
│ startTime: 09:23:45 │
│ endTime: 09:28:45 │
│ duration: 300 seconds │
│ callType: SIMPLE │
│ holdSegments: [] │
│ status: COMPLETED │
└─────────────────────────────────────────────────────────────┘
CIM MESSAGES SENT TO CCM:
1. CALL_LEG_STARTED { legId, agentExtension: 1005, startTime, callType: SIMPLE }
2. CALL_LEG_ENDED { legId, endTime, duration: 300 }
3. WRAPUP { legId, wrapUpCode, wrapUpData } (after DB enrichment)
```
21.4.2 TRANSFER Call (Two Agents, Blind or Warm)
EVENT SEQUENCE: TRANSFER CALL
═══════════════════════════════════════════════════════════════════════════════
Customer ──► Agent 1005 ──► Agent 1006 (transfer)
JTAPI Events (single globalCallId = 167891):
┌────┬────────────────────────────┬──────────┬────────────┬──────────────────────────┐
│ # │ Event │ Time │ Extension │ Notes │
├────┼────────────────────────────┼──────────┼────────────┼──────────────────────────┤
│ 1 │ CallActiveEv │ 09:30:00 │ — │ Call created │
│ 2 │ CallCtlConnAlertingEv │ 09:30:00 │ 8001 │ DNIS ringing │
│ 3 │ CallCtlConnEstablishedEv │ 09:30:01 │ 8001 │ DNIS answered │
│ 4 │ TermConnCreatedEv │ 09:30:01 │ 1005 │ Agent 1005 added │
│ 5 │ CallCtlTermConnRingingEv │ 09:30:02 │ 1005 │ Agent 1005 rings │
│ 6 │ CallCtlTermConnTalkingEv │ 09:30:05 │ 1005 │ AGENT 1005 ANSWERS │
│ │ │ │ │ │
│ │ [TALKING PHASE — 2 min] │ │ │ │
│ │ │ │ │ │
│ 7 │ CallCtlTermConnHeldEv │ 09:32:05 │ 1005 │ Agent 1005 presses hold │
│ 8 │ CallCtlConnInitiatedEv │ 09:32:06 │ 1006 │ Transfer initiated │
│ 9 │ CallCtlConnDialingEv │ 09:32:06 │ 1006 │ Dialing 1006 │
│ 10 │ CallCtlConnAlertingEv │ 09:32:07 │ 1006 │ 1006 ringing │
│ 11 │ CallCtlTermConnRingingEv │ 09:32:07 │ 1006 │ Agent 1006 phone rings │
│ 12 │ CallCtlTermConnTalkingEv │ 09:32:10 │ 1006 │ AGENT 1006 ANSWERS │
│ │ │ │ │ (consultation) │
│ 13 │ CallCtlConferencedEv │ 09:32:15 │ — │ 1005, 1006, customer │
│ │ │ │ │ now in conference │
│ 14 │ CallCtlTermConnDroppedEv │ 09:32:20 │ 1005 │ Agent 1005 drops │
│ │ │ │ │ (transfer complete) │
│ │ │ │ │ │
│ │ [TALKING PHASE — 3 min] │ │ │ │
│ │ │ │ │ │
│ 15 │ ConnDisconnectedEv │ 09:35:20 │ 8001 │ Caller hangs up │
│ 16 │ CallCtlTermConnDroppedEv │ 09:35:20 │ 1006 │ Agent 1006 dropped │
│ 17 │ CallInvalidEv │ 09:35:20 │ — │ Call destroyed │
└────┴────────────────────────────┴──────────┴────────────┴──────────────────────────┘
CORRELATION RESULT:
┌──────────────────────────────────────────────────────────────────────────────┐
│ CallLeg #1 (Original Agent) │
│ jtapiId: 167891 │
│ extension: 1005 │
│ startTime: 09:30:05 │
│ endTime: 09:32:20 │
│ duration: 135 seconds │
│ callType: TRANSFER │
│ holdSegments: [{start: 09:32:05, end: 09:32:15}] │
│ transferredTo: 1006 │
│ status: COMPLETED │
├──────────────────────────────────────────────────────────────────────────────┤
│ CallLeg #2 (Transferred-to Agent) │
│ jtapiId: 167891 │
│ extension: 1006 │
│ startTime: 09:32:10 │
│ endTime: 09:35:20 │
│ duration: 190 seconds │
│ callType: TRANSFER │
│ holdSegments: [] │
│ transferredFrom: 1005 │
│ status: COMPLETED │
└──────────────────────────────────────────────────────────────────────────────┘
CIM MESSAGES SENT TO CCM:
1. CALL_LEG_STARTED { legId: 1, extension: 1005, startTime: 09:30:05, callType: TRANSFER }
2. CALL_LEG_STARTED { legId: 2, extension: 1006, startTime: 09:32:10, callType: TRANSFER }
3. CALL_LEG_ENDED { legId: 1, endTime: 09:32:20, duration: 135 }
4. CALL_LEG_ENDED { legId: 2, endTime: 09:35:20, duration: 190 }
5. WRAPUP { legId: 1, wrapUpCode: "COLD_TRANSFER" } (from UCCE TCD)
6. WRAPUP { legId: 2, wrapUpCode: "RESOLVED" } (from UCCE TCD)
NOTE: Two separate CallLegs are created because the correlation engine detects
multiple agent extensions in the same globalCallId event group. The engine
knows this is a transfer (not a conference) because:
- Agent 1005 dropped before the call ended
- Only one agent remained when caller disconnected
- CiscoFeatureReason contains REASON_TRANSFER
```
21.4.3 CONSULT Call (Consultative Transfer)
EVENT SEQUENCE: CONSULT CALL
═══════════════════════════════════════════════════════════════════════════════
Customer ──► Agent 1005 ──► Agent 1006 (consult) ──► Back to 1005 or to 1006
JTAPI Events (single globalCallId = 167892):
┌────┬────────────────────────────┬──────────┬────────────┬─────────────────────────┐
│ # │ Event │ Time │ Extension │ Notes │
├────┼────────────────────────────┼──────────┼────────────┼─────────────────────────┤
│ 1 │ CallActiveEv │ 10:00:00 │ — │ Call created │
│ 2 │ CallCtlTermConnTalkingEv │ 10:00:05 │ 1005 │ Agent 1005 answers │
│ │ │ │ │ │
│ │ [TALKING — 3 min] │ │ │ │
│ │ │ │ │ │
│ 3 │ CallCtlTermConnHeldEv │ 10:03:00 │ 1005 │ 1005 puts call on hold │
│ 4 │ CallCtlConnInitiatedEv │ 10:03:01 │ 1006 │ 1005 initiates consult │
│ 5 │ CallCtlConnDialingEv │ 10:03:01 │ 1006 │ Dialing 1006 │
│ 6 │ CallCtlTermConnRingingEv │ 10:03:02 │ 1006 │ 1006 phone rings │
│ 7 │ CallCtlTermConnTalkingEv │ 10:03:05 │ 1006 │ AGENT 1006 ANSWERS │
│ │ │ │ │ (private consult) │
│ │ │ │ │ │
│ │ [CONSULTATION — 1 min] │ │ │ │
│ │ │ │ │ │
│
│ SCENARIO A: Consult completed, transfer to 1006
│ ─────────────────────────────────────────────
│ 8a │ CallCtlTermConnDroppedEv │ 10:04:05 │ 1005 │ 1005 completes transfer │
│ 9a │ CallCtlTermConnTalkingEv │ 10:04:05 │ 1006 │ 1006 now with customer │
│ │ │ │ │ │
│ 10a│ ConnDisconnectedEv │ 10:08:00 │ 8001 │ Caller hangs up │
│ 11a│ CallCtlTermConnDroppedEv │ 10:08:00 │ 1006 │ 1006 dropped │
│
│ SCENARIO B: Consult cancelled, back to 1005
│ ─────────────────────────────────────────────
│ 8b │ ConnDisconnectedEv │ 10:04:05 │ 1006 │ 1006 hangs up │
│ 9b │ CallCtlTermConnTalkingEv │ 10:04:05 │ 1005 │ 1005 resumes │
│ │ │ │ │ │
│ 10b│ ConnDisconnectedEv │ 10:08:00 │ 8001 │ Caller hangs up │
│ 11b│ CallCtlTermConnDroppedEv │ 10:08:00 │ 1005 │ 1005 dropped │
└────┴────────────────────────────┴──────────┴────────────┴─────────────────────────┘
CORRELATION RESULT — SCENARIO A (Transfer to 1006):
┌──────────────────────────────────────────────────────────────────────────────┐
│ CallLeg #1 (Agent 1005 — consult initiator) │
│ jtapiId: 167892 │
│ extension: 1005 │
│ startTime: 10:00:05 │
│ endTime: 10:04:05 │
│ duration: 240 seconds │
│ callType: CONSULT │
│ consultTarget: 1006 │
│ consultResult: TRANSFERRED │
│ holdSegments: [{start: 10:03:00, end: 10:04:05}] │
├──────────────────────────────────────────────────────────────────────────────┤
│ CallLeg #2 (Agent 1006 — consult target) │
│ jtapiId: 167892 │
│ extension: 1006 │
│ startTime: 10:03:05 │
│ endTime: 10:08:00 │
│ duration: 295 seconds │
│ callType: CONSULT │
│ consultSource: 1005 │
│ holdSegments: [] │
└──────────────────────────────────────────────────────────────────────────────┘
CORRELATION RESULT — SCENARIO B (Cancelled, back to 1005):
┌──────────────────────────────────────────────────────────────────────────────┐
│ CallLeg #1 (Agent 1005 — full call) │
│ jtapiId: 167892 │
│ extension: 1005 │
│ startTime: 10:00:05 │
│ endTime: 10:08:00 │
│ duration: 480 seconds │
│ callType: CONSULT │
│ consultTarget: 1006 │
│ consultResult: CANCELLED │
│ holdSegments: [{start: 10:03:00, end: 10:04:05}] │
├──────────────────────────────────────────────────────────────────────────────┤
│ CallLeg #2 (Agent 1006 — consult only, no customer) │
│ jtapiId: 167892 │
│ extension: 1006 │
│ startTime: 10:03:05 │
│ endTime: 10:04:05 │
│ duration: 60 seconds │
│ callType: CONSULT │
│ consultSource: 1005 │
│ consultResult: CANCELLED │
│ status: COMPLETED │
└──────────────────────────────────────────────────────────────────────────────┘
KEY DISTINCTION FROM TRANSFER:
- CONSULT: Agent 1005 puts original call ON HOLD before dialing 1006
- TRANSFER (blind): Agent 1005 does NOT put call on hold; immediately transfers
- The hold segment is the key discriminator
```
21.4.4 CONFERENCE Call (Three+ Parties)
EVENT SEQUENCE: CONFERENCE CALL
═══════════════════════════════════════════════════════════════════════════════
Customer ──► Agent 1005 ──► Consult 1006 ──► Conference all three
JTAPI Events (single globalCallId = 167893):
┌────┬────────────────────────────┬──────────┬────────────┬─────────────────────────┐
│ # │ Event │ Time │ Extension │ Notes │
├────┼────────────────────────────┼──────────┼────────────┼─────────────────────────┤
│ 1 │ CallActiveEv │ 11:00:00 │ — │ Call created │
│ 2 │ CallCtlTermConnTalkingEv │ 11:00:05 │ 1005 │ Agent 1005 answers │
│ │ │ │ │ │
│ 3 │ CallCtlTermConnHeldEv │ 11:02:00 │ 1005 │ 1005 holds │
│ 4 │ CallCtlConnInitiatedEv │ 11:02:01 │ 1006 │ Initiate consult │
│ 5 │ CallCtlConnDialingEv │ 11:02:01 │ 1006 │ Dialing 1006 │
│ 6 │ CallCtlTermConnTalkingEv │ 11:02:05 │ 1006 │ 1006 answers │
│ │ │ │ │ │
│ 7 │ CallCtlConferencedEv │ 11:02:30 │ — │ CONFERENCE FORMED │
│ │ │ │ │ All 3 parties joined │
│ │ │ │ │ │
│ │ [CONFERENCE — 5 min] │ │ │ │
│ │ │ │ │ │
│ 8 │ ConnDisconnectedEv │ 11:07:30 │ 8001 │ Caller hangs up │
│ 9 │ CallCtlTermConnDroppedEv │ 11:07:30 │ 1005 │ 1005 dropped │
│ 10 │ CallCtlTermConnDroppedEv │ 11:07:30 │ 1006 │ 1006 dropped │
│ 11 │ CallInvalidEv │ 11:07:30 │ — │ Call destroyed │
└────┴────────────────────────────┴──────────┴────────────┴─────────────────────────┘
CORRELATION RESULT:
┌──────────────────────────────────────────────────────────────────────────────┐
│ CallLeg #1 (Agent 1005) │
│ jtapiId: 167893 │
│ extension: 1005 │
│ startTime: 11:00:05 │
│ endTime: 11:07:30 │
│ duration: 445 seconds │
│ callType: CONFERENCE │
│ conferenceId: 167893_CONF │
│ participants: [1005, 1006, 8001] │
│ holdSegments: [{start: 11:02:00, end: 11:02:30}] │
├──────────────────────────────────────────────────────────────────────────────┤
│ CallLeg #2 (Agent 1006) │
│ jtapiId: 167893 │
│ extension: 1006 │
│ startTime: 11:02:05 │
│ endTime: 11:07:30 │
│ duration: 325 seconds │
│ callType: CONFERENCE │
│ conferenceId: 167893_CONF │
│ participants: [1005, 1006, 8001] │
│ holdSegments: [] │
└──────────────────────────────────────────────────────────────────────────────┘
CONFERENCE DETECTION LOGIC:
The correlation engine detects CONFERENCE (not CONSULT) because:
1. CallCtlConferencedEv is present in the event stream
2. Multiple agents remain active simultaneously after the conference event
3. All parties drop at the same time when caller hangs up
4. No single agent "completes" a transfer
RECORDING IMPLICATION:
- Media Server receives TWO BiB streams (one from 1005, one from 1006)
- Mixer must combine both agent streams with caller audio
- Result: 3+ channel mixed recording or separate leg recordings
```
21.4.5 OUTBOUND Call (Agent Initiates)
EVENT SEQUENCE: OUTBOUND CALL
═══════════════════════════════════════════════════════════════════════════════
Agent 1005 ──► Dials customer +1234567890
JTAPI Events (single globalCallId = 167894):
┌────┬────────────────────────────┬──────────┬────────────┬─────────────────────────┐
│ # │ Event │ Time │ Extension │ Notes │
├────┼────────────────────────────┼──────────┼────────────┼─────────────────────────┤
│ 1 │ CallActiveEv │ 14:00:00 │ — │ Call created │
│ 2 │ CallCtlConnInitiatedEv │ 14:00:00 │ 1005 │ Agent initiates call │
│ 3 │ CallCtlConnDialingEv │ 14:00:00 │ +1234... │ Dialing customer │
│ 4 │ CallCtlConnNetworkReachedEv│ 14:00:02 │ +1234... │ Network reached │
│ 5 │ CallCtlConnNetworkAlertingEv│14:00:03 │ +1234... │ Remote ringing │
│ 6 │ CallCtlTermConnRingingEv │ 14:00:03 │ 1005 │ Agent phone ringing │
│ 7 │ CallCtlConnEstablishedEv │ 14:00:08 │ +1234... │ CUSTOMER ANSWERS │
│ 8 │ CallCtlTermConnTalkingEv │ 14:00:08 │ 1005 │ AGENT CONNECTED │
│ │ │ │ │ │
│ │ [TALKING — 4 min] │ │ │ │
│ │ │ │ │ │
│ 9 │ ConnDisconnectedEv │ 14:04:08 │ +1234... │ Customer/Agent hangs up │
│ 10 │ CallCtlTermConnDroppedEv │ 14:04:08 │ 1005 │ Agent dropped │
│ 11 │ CallInvalidEv │ 14:04:08 │ — │ Call destroyed │
└────┴────────────────────────────┴──────────┴────────────┴─────────────────────────┘
CORRELATION RESULT:
┌──────────────────────────────────────────────────────────────────────────────┐
│ CallLeg #1 (Outbound) │
│ jtapiId: 167894 │
│ extension: 1005 │
│ startTime: 14:00:08 │
│ endTime: 14:04:08 │
│ duration: 240 seconds │
│ callType: OUTBOUND │
│ customerNumber: +1234567890 │
│ direction: OUTBOUND │
│ status: COMPLETED │
└──────────────────────────────────────────────────────────────────────────────┘
OUTBOUND DETECTION:
- CallCtlConnInitiatedEv on agent extension (not DNIS)
- No DNIS/queue events
- CallCtlConnDialingEv points to external number
- CiscoCause may indicate outbound campaign
UCCE TCD DIFFERENCES:
- PeripheralCallType = 7, 17, 18, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 41
- Dialer_Detail table may have record
- Campaign ID and SkillTargetID populated
```
21.4.6 RONA (Redirect on No Answer)
EVENT SEQUENCE: RONA
═══════════════════════════════════════════════════════════════════════════════
Customer ──► Agent 1005 (doesn't answer) ──► Agent 1006
JTAPI Events (globalCallId = 167895):
┌────┬────────────────────────────┬──────────┬────────────┬─────────────────────────┐
│ # │ Event │ Time │ Extension │ Notes │
├────┼────────────────────────────┼──────────┼────────────┼─────────────────────────┤
│ 1 │ CallActiveEv │ 15:00:00 │ — │ Call created │
│ 2 │ CallCtlTermConnTalkingEv │ 15:00:05 │ 1005 │ Routed to 1005 │
│ 3 │ CallCtlTermConnRingingEv │ 15:00:05 │ 1005 │ 1005 ringing │
│ │ │ │ │ │
│ │ [RINGING — 12 seconds] │ │ │ UCCE RONA timeout=12s │
│ │ │ │ │ │
│ 4 │ CallCtlTermConnDroppedEv │ 15:00:17 │ 1005 │ RONA! 1005 dropped │
│ 5 │ CallCtlConnDisconnectedEv │ 15:00:17 │ 1005 │ Connection cleared │
│ 6 │ CallCtlTermConnRingingEv │ 15:00:18 │ 1006 │ Rerouted to 1006 │
│ 7 │ CallCtlTermConnTalkingEv │ 15:00:22 │ 1006 │ Agent 1006 ANSWERS │
│ │ │ │ │ │
│ 8 │ ConnDisconnectedEv │ 15:05:22 │ 8001 │ Caller hangs up │
│ 9 │ CallCtlTermConnDroppedEv │ 15:05:22 │ 1006 │ 1006 dropped │
└────┴────────────────────────────┴──────────┴────────────┴─────────────────────────┘
CORRELATION RESULT:
┌──────────────────────────────────────────────────────────────────────────────┐
│ CallLeg #1 (Agent 1005 — RONA) │
│ jtapiId: 167895 │
│ extension: 1005 │
│ startTime: 15:00:05 │
│ endTime: 15:00:17 │
│ duration: 12 seconds │
│ callType: RONA │
│ ronaReason: NO_ANSWER │
│ status: COMPLETED │
│ recording: NO (BiB never fully activated) │
├──────────────────────────────────────────────────────────────────────────────┤
│ CallLeg #2 (Agent 1006 — successful) │
│ jtapiId: 167895 │
│ extension: 1006 │
│ startTime: 15:00:22 │
│ endTime: 15:05:22 │
│ duration: 300 seconds │
│ callType: SIMPLE │
│ status: COMPLETED │
└──────────────────────────────────────────────────────────────────────────────┘
RONA DETECTION:
- Agent extension has RingingEv but NO TalkingEv
- Connection dropped within RONA timeout (typically 10-20s)
- CiscoCause = CAUSE_CALL_REJECTED or CAUSE_NO_USER_RESPONSE
- UCCE TCD: CallResult = 2 (Abandoned), AgentSkillTargetID may be null
CRITICAL: RONA legs with no TalkingEv are still recorded in jtapi_event
but may produce CallLegs with duration < threshold. CX filters these
based on config (e.g., ignore legs < 5 seconds).
```
21.4.7 Multi-Leg Call Summary Matrix
|
Call Type |
# of Legs |
Leg Structure |
Key Discriminator |
Hold Used? |
|---|---|---|---|---|
|
SIMPLE |
1 |
One agent, one caller |
Single extension |
No |
|
TRANSFER |
2+ |
Agent A → Agent B |
Agent A drops before call ends |
No (blind) |
|
CONSULT |
2+ |
Agent A consults B, then transfer or cancel |
Hold + consult events |
Yes |
|
CONFERENCE |
2+ |
Multiple agents + caller simultaneously |
CallCtlConferencedEv |
Yes |
|
OUTBOUND |
1 |
Agent dials customer |
Initiated by agent extension |
No |
|
RONA |
1 (failed) + 1 (success) |
No answer → reroute |
Ringing without Talking |
No |
|
CONSULT→TRANSFER |
2 |
Consult then complete transfer |
Hold + transfer completion |
Yes |
|
CONSULT→CANCEL |
2 |
Consult then back to original |
Hold + consult disconnect |
Yes |
|
CONSULT→CONFERENCE |
2+ |
Consult then conference all |
Hold + conference event |
Yes |
---
21.5 ConversationService — Post-Call Enrichment
21.5.1 Enrichment Flow
[CallLegs Table] ──► [ConversationService] ──► [UCCE/UCCX DB] ──► [CCM]
│
│ 1. Poll legs where cim_sent = false
│ AND end_time IS NOT NULL
│ AND status = 'COMPLETED'
│
│ 2. For each leg:
│ a. Determine query type (UCCE vs UCCX)
│ b. Build SQL with jtapiId + time window
│ c. Execute query
│ d. Map result to CallLeg fields
│ e. Build CIM messages
│ f. Send to CCM
│ g. Mark cim_sent = true
21.5.2 UCCE Enrichment — Detailed Mapping
public void enrichFromUCCE(CallLeg leg) {
String sql = buildUCCEQuery(leg.getJtapiId(), leg.getStartTime(), leg.getEndTime());
try (Connection conn = ucceDataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ResultSet rs = ps.executeQuery();
if (rs.next()) {
// Direct field mapping
leg.setServiceIdentifier(rs.getString("DigitsDialed"));
leg.setCustomerIdentifier(rs.getString("ANI"));
leg.setAgentId(rs.getString("AgentID"));
leg.setAgentName(rs.getString("AgentName"));
leg.setWrapUpCode(rs.getString("WrapUps"));
leg.setExtension(rs.getString("extension"));
// Derived fields
String callResult = rs.getString("callResult");
leg.setCallResult(mapCallResult(callResult));
// Duration validation
int tcdTalkTime = rs.getInt("TalkTime");
int calculatedDuration = (int) Duration.between(
leg.getStartTime(), leg.getEndTime()).getSeconds();
if (Math.abs(tcdTalkTime - calculatedDuration) > 5) {
log.warn("Duration mismatch: TCD={}, JTAPI={} for leg {}",
tcdTalkTime, calculatedDuration, leg.getId());
leg.setDurationSeconds(tcdTalkTime); // Trust TCD
} else {
leg.setDurationSeconds(calculatedDuration);
}
// Hold segments from TCD
int holdTime = rs.getInt("HoldTime");
if (holdTime > 0 && leg.getHoldSegments().isEmpty()) {
// TCD reports hold but JTAPI missed it — create synthetic segment
leg.addSyntheticHoldSegment(holdTime);
}
} else {
// No TCD record found — possible RONA or system error
leg.setStatus("ENRICHMENT_FAILED");
leg.setCorrelationAttempts(leg.getCorrelationAttempts() + 1);
if (leg.getCorrelationAttempts() >= MAX_RETRY) {
leg.setStatus("ENRICHMENT_MAX_RETRY");
// Send CIM with partial data
sendPartialCim(leg);
}
}
} catch (SQLException e) {
log.error("UCCE query failed for leg {}", leg.getId(), e);
leg.setStatus("ENRICHMENT_ERROR");
}
}
21.5.3 CIM Message Construction
public List<CimMessage> buildCimMessages(CallLeg leg) {
List<CimMessage> messages = new ArrayList<>();
// CALL_LEG_STARTED (if not already sent)
if (!leg.isStartedCimSent()) {
messages.add(CimMessage.builder()
.intent("CALL_LEG_STARTED")
.activityType("VOICE_CALL")
.channelType("VOICE")
.channelProvider("CISCO")
.body(Map.of(
"legId", leg.getId(),
"jtapiId", leg.getJtapiId(),
"agentExtension", leg.getAgentExtension(),
"agentId", leg.getAgentId(),
"agentName", leg.getAgentName(),
"startTime", leg.getStartTime().toString(),
"callType", leg.getCallType().name(),
"serviceIdentifier", orEmpty(leg.getServiceIdentifier()),
"customerIdentifier", orEmpty(leg.getCustomerIdentifier())
))
.build());
}
// CALL_LEG_ENDED
messages.add(CimMessage.builder()
.intent("CALL_LEG_ENDED")
.activityType("VOICE_CALL")
.channelType("VOICE")
.body(Map.of(
"legId", leg.getId(),
"jtapiId", leg.getJtapiId(),
"agentExtension", leg.getAgentExtension(),
"endTime", leg.getEndTime().toString(),
"durationSeconds", leg.getDurationSeconds(),
"callType", leg.getCallType().name(),
"holdSegments", leg.getHoldSegments()
))
.build());
// WRAPUP (if wrap-up code present)
if (leg.getWrapUpCode() != null && !leg.getWrapUpCode().isEmpty()) {
messages.add(CimMessage.builder()
.intent("WRAPUP")
.activityType("WRAPUP")
.channelType("VOICE")
.body(Map.of(
"legId", leg.getId(),
"jtapiId", leg.getJtapiId(),
"agentExtension", leg.getAgentExtension(),
"wrapUpCode", leg.getWrapUpCode(),
"wrapUpData", orEmpty(leg.getWrapUpData()),
"timestamp", leg.getEndTime().toString()
))
.build());
}
return messages;
}
21.5.4 CCM Processing of CIM Messages
[CCM receives CIM]
│
├─► CALL_LEG_STARTED
│ │
│ │ 1. Create or update Conversation
│ │ 2. Add VOICE_CALL activity to timeline
│ │ 3. Link to customer via customerIdentifier (ANI)
│ │ 4. Set conversation state = ACTIVE
│ │ 5. Assign to agent via agentId
│ ▼
│ [Conversation Timeline]
│ "Voice call started at 09:23:45"
│ "Agent: John Smith (1005)"
│
├─► CALL_LEG_ENDED
│ │
│ │ 1. Update activity with endTime, duration
│ │ 2. If all legs ended, set conversation state = CLOSED
│ │ 3. Trigger post-call workflows
│ ▼
│ [Conversation Timeline]
│ "Voice call ended at 09:28:45 (5:00)"
│
└─► WRAPUP
│
│ 1. Add WRAPUP activity to timeline
│ 2. Update conversation classification
│ 3. Trigger analytics (sentiment, categorization)
▼
[Conversation Timeline]
"Wrap-up: RESOLVED"
---
21.6 Error Scenarios & Recovery
21.6.1 JTAPI Provider Disconnect
SCENARIO: JTAPI Provider loses connection to CUCM
[CUCM JTAPI Service Restart]
│
│ TCP connection dropped
▼
[JtapiListenerService]
│
│ 1. Detects ProviderOutOfServiceEv
│ 2. Stops processing events
│ 3. Marks all IN_PROGRESS legs as STALE
│ 4. Starts reconnection timer (30s interval)
│
├─► Reconnection successful:
│ │
│ │ 1. Receives ProviderInServiceEv
│ │ 2. Resubscribes to all monitored terminals
│ │ 3. Resumes normal processing
│ ▼
│ [Back to normal]
│
└─► Reconnection fails after 10 attempts:
│
│ 1. Alerts operations team
│ 2. All STALE legs marked FAILED
│ 3. Partial CIM sent to CCM
▼
[Manual intervention required]
RECOVERY: When JTAPI reconnects, it does NOT replay missed events.
The CorrelationService relies on the UCCE/UCCX database query to
backfill any calls that occurred during the outage.
```
21.6.2 Event Sequence Gaps
SCENARIO: Events lost or out of order
Problem: CallCtlTermConnTalkingEv arrives AFTER ConnDisconnectedEv
JTAPI Events (out of order):
┌────┬────────────────────────────┬──────────┬─────────────────┐
│ # │ Event │ Time │ Issue │
├────┼────────────────────────────┼──────────┼─────────────────┤
│ 1 │ ConnDisconnectedEv │ 09:28:45 │ Arrived first! │
│ 2 │ CallCtlTermConnTalkingEv │ 09:23:45 │ Actual order │
└────┴────────────────────────────┴──────────┴─────────────────┘
CORRELATION ENGINE HANDLING:
1. Events are ALWAYS sorted by timestamp before processing
2. If TalkingEv timestamp < DisconnectEv timestamp, correct order is enforced
3. If timestamps are identical (same millisecond), event type priority used:
- TALKING (start) takes precedence over DISCONNECT (end)
- RINGING takes precedence over TALKING
EDGE CASE: If no TalkingEv exists (lost event):
- CorrelationService checks for RingingEv as fallback start
- If no RingingEv either, leg created with earliest event timestamp
- Duration marked as ESTIMATED
- Flag: startEventMissing = true
```
21.6.3 Duplicate Events
SCENARIO: Same JTAPI event received twice
Causes:
1. JTAPI Provider retransmission
2. Network glitch
3. CUCM failover event duplication
DETECTION:
- Each event has unique {globalCallId + eventType + timestamp + extension}
- CorrelationService maintains dedup window (last 5 minutes)
- INSERT with ON CONFLICT (event_hash) DO NOTHING
TABLE:
CREATE TABLE jtapi_event (
...,
event_hash VARCHAR(64) UNIQUE, -- SHA256 of key fields
...
);
```
21.6.4 Clock Skew Between CUCM and CX
SCENARIO: CUCM clock is 30 seconds ahead of CX server
Impact:
- JTAPI event timestamps are from CUCM
- CX server uses its own clock for DB inserts
- Duration calculations may be incorrect
MITIGATION:
1. NTP synchronization is REQUIRED on both systems
2. CorrelationService adjusts timestamps:
- If future-dated event detected, cap to now()
- Log warning if skew > 5 seconds
3. Duration uses: endTime - startTime (both from JTAPI)
Never uses CX server clock for duration
```
21.6.5 UCCE TCD Query Returns No Results
SCENARIO: Call completed but no TCD record found
Possible Causes:
1. PG delay — TCD not yet written to AW (can be 5-30 min behind)
2. Call was RONA/abandoned — no agent, no TCD agent record
3. ICM script bypassed normal routing
4. Database replication lag
CORRELATION ENGINE RETRY:
┌─────────────┬────────────────────────────────────────────────────────┐
│ Attempt # │ Action │
├─────────────┼────────────────────────────────────────────────────────┤
│ 1 (immediate)│ Query UCCE — if no result, mark PENDING_ENRICHMENT │
│ 2 (+5 min) │ Re-query UCCE │
│ 3 (+15 min) │ Re-query UCCE │
│ 4 (+30 min) │ Re-query UCCE │
│ 5 (+60 min) │ Final attempt — if still no result: │
│ │ - Mark ENRICHMENT_FAILED │
│ │ - Send partial CIM to CCM (JTAPI data only) │
│ │ - Log for manual investigation │
└─────────────┴────────────────────────────────────────────────────────┘
PARTIAL CIM (when TCD missing):
{
"intent": "CALL_LEG_ENDED",
"legId": 12345,
"agentExtension": "1005",
"startTime": "2024-01-15T09:23:45Z",
"endTime": "2024-01-15T09:28:45Z",
"durationSeconds": 300,
"enrichmentStatus": "PARTIAL",
"note": "TCD record not found in UCCE AW database"
}
```
---
21.7 Real-Time Event Flow Diagram
TIME: T+0 seconds (Customer dials)
═══════════════════════════════════════════════════════════════════════════════
[PSTN] ──SIP INVITE──► [CUBE] ──SIP──► [CUCM]
│
│ Route to DNIS 8001
▼
[CVP/UCCX]
│
│ Route to agent
▼
[CUCM]
│
│ SIP INVITE to Agent Phone
▼
TIME: T+2 seconds (Agent phone rings)
═══════════════════════════════════════════════════════════════════════════════
[Agent Phone]
│
│ Ringing
│ BiB fork initializes
▼
[CUCM]
│
│ JTAPI: CallCtlTermConnRingingEv
│ { globalCallId: 167890, extension: 1005 }
▼
[Cisco Connector]
│
│ INSERT INTO jtapi_event (...)
│ Marked: processed = false
▼
[PostgreSQL]
TIME: T+5 seconds (Agent answers)
═══════════════════════════════════════════════════════════════════════════════
[Agent Phone]
│
│ Off-hook
│ BiB fork active → Media Server
▼
[CUCM]
│
├─► JTAPI: CallCtlTermConnTalkingEv
│ { globalCallId: 167890, extension: 1005 }
│
├─► SIP INVITE (BiB) ──► [Media Server]
│ │
│ │ Accepts, starts recording
│ │ File: /streams/167890_20240115.wav
│ ▼
│ [FreeSWITCH]
│
▼
[Cisco Connector]
│
│ INSERT INTO jtapi_event
│
├─► Finesse publishes XMPP:
│ <Dialog><state>ACTIVE</state></Dialog>
│
▼
[Finesse] ──XMPP/BOSH──► [AgentDesk]
│
│ Updates UI: "On Call"
│ Shows customer info (ANI)
▼
[Angular UI]
TIME: T+30 seconds (Correlation runs)
═══════════════════════════════════════════════════════════════════════════════
[CorrelationService — scheduled job]
│
│ SELECT * FROM jtapi_event WHERE processed = false
│
│ Groups by globalCallId = 167890:
│ Event 1: RingingEv 09:23:42
│ Event 2: TalkingEv 09:23:45 ← START
│
│ Detects: single extension, no hold, no transfer
│ CallType = SIMPLE
│
│ Creates CallLeg:
│ jtapiId: 167890
│ extension: 1005
│ startTime: 09:23:45
│ endTime: null (not yet ended)
│ status: IN_PROGRESS
│
│ UPDATE jtapi_event SET processed = true
▼
[call_leg table]
INSERT INTO call_leg (...)
TIME: T+305 seconds (Call ends)
═══════════════════════════════════════════════════════════════════════════════
[Customer hangs up]
│
▼
[CUCM]
│
├─► JTAPI: ConnDisconnectedEv
│ { globalCallId: 167890, extension: 8001 }
│
├─► JTAPI: CallCtlTermConnDroppedEv
│ { globalCallId: 167890, extension: 1005 }
│
├─► BiB BYE ──► [Media Server]
│ │
│ │ Stops recording
│ │ File closed
│ │ RECORD_STOP event
│ │ POST /mixer/sip-data
│ ▼
│ [Mixer Queue]
│
├─► Finesse: Dialog state = DROPPED
│
▼
[Cisco Connector]
│
│ INSERT INTO jtapi_event (2 new events)
│
TIME: T+330 seconds (Correlation picks up end events)
═══════════════════════════════════════════════════════════════════════════════
[CorrelationService]
│
│ Finds unprocessed events for globalCallId 167890:
│ Event 3: ConnDisconnectedEv 09:28:45
│ Event 4: CallCtlTermConnDroppedEv 09:28:45
│
│ Matches existing CallLeg (jtapiId: 167890, ext: 1005)
│ Sets endTime = 09:28:45
│ Sets duration = 300 seconds
│ Sets status = COMPLETED
│
│ UPDATE jtapi_event SET processed = true
▼
[call_leg table]
UPDATE call_leg SET end_time = ..., duration = 300, status = 'COMPLETED'
TIME: T+360 seconds (Enrichment runs)
═══════════════════════════════════════════════════════════════════════════════
[ConversationService — scheduled job]
│
│ SELECT * FROM call_leg
│ WHERE cim_sent = false
│ AND status = 'COMPLETED'
│
│ Finds CallLeg id=12345 (jtapiId: 167890)
│
│ Executes UCCE query:
│ SELECT tcd.*, a.EnterpriseName
│ FROM Termination_Call_Detail tcd
│ LEFT JOIN Agent a ON tcd.AgentSkillTargetID = a.SkillTargetID
│ WHERE tcd.PeripheralCallKey = '167890'
│
│ Result:
│ ANI: +18005550199
│ DigitsDialed: 8001
│ WrapupData: RESOLVED
│ AgentSkillTargetID: 5001
│ EnterpriseName: John Smith
│ TalkTime: 300
│
│ Enriches CallLeg:
│ serviceIdentifier = 8001
│ customerIdentifier = +18005550199
│ agentId = 5001
│ agentName = John Smith
│ wrapUpCode = RESOLVED
│
│ Builds CIM messages:
│ 1. CALL_LEG_STARTED (if not sent)
│ 2. CALL_LEG_ENDED
│ 3. WRAPUP
│
│ POST /conversation-manager/activities/third-party
│ Authorization: Bearer {token}
│
│ Response: 200 OK
│
│ UPDATE call_leg SET cim_sent = true
▼
[CCM Activities Timeline]
Shows: Call started, Agent assigned, Wrap-up code, Duration
```
---
21.8 Finesse XMPP Event Deep Dive
21.8.1 XMPP Message Types
<!-- Agent State Change -->
<Update>
<User>
<state>RESERVED</state>
<stateChangeTime>2024-01-15T09:23:40.000Z</stateChangeTime>
</User>
</Update>
<!-- Dialog (Call) State Change -->
<Update>
<Dialog>
<dialogId>167890</dialogId>
<state>ALERTING</state>
<fromAddress>+18005550199</fromAddress>
<toAddress>1005</toAddress>
<uri>sip:+18005550199@expertflow.com</uri>
<secondaryId>8001</secondaryId>
</Dialog>
</Update>
<Update>
<Dialog>
<dialogId>167890</dialogId>
<state>ACTIVE</state>
<fromAddress>+18005550199</fromAddress>
<toAddress>1005</toAddress>
</Dialog>
</Update>
<Update>
<Dialog>
<dialogId>167890</dialogId>
<state>DROPPED</state>
<fromAddress>+18005550199</fromAddress>
<toAddress>1005</toAddress>
</Dialog>
</Update>
<!-- Wrap-up -->
<Update>
<Dialog>
<dialogId>167890</dialogId>
<state>WRAP_UP</state>
<wrapUpReason>Resolved</wrapUpReason>
</Dialog>
</Update>
```
21.8.2 Finesse-to-CX State Mapping
|
Finesse State |
CX MRD State |
When |
|---|---|---|
|
|
|
Agent logs out |
|
|
|
Agent goes not-ready |
|
|
|
Agent ready for calls |
|
|
|
Call offered to agent |
|
|
|
Agent on active call |
|
|
|
Post-call wrap-up |
|
|
|
Wrap-up auto-complete |
|
|
|
Call on hold |
21.8.3 AgentDesk cti.service.ts Processing
// Simplified flow of how AgentDesk processes Finesse events
class CtiService {
private finesseConnection: BoshConnection;
private agentState: AgentState = AgentState.LOGOUT;
connect(agentId: string, password: string): void {
this.finesseConnection = new BoshConnection({
url: 'https://finesse-server:8445/http-bind',
jid: ${agentId}@finesse-server,
password: password
});
this.finesseConnection.on('message', (msg: XmppMessage) => {
this.handleXmppMessage(msg);
});
}
private handleXmppMessage(msg: XmppMessage): void {
if (msg.update?.user) {
// Agent state change
const finesseState = msg.update.user.state;
const cxState = this.mapFinesseToCxState(finesseState);
this.updateAgentState(cxState);
}
if (msg.update?.dialog) {
// Dialog (call) event
const dialog = msg.update.dialog;
this.handleDialogEvent(dialog);
}
}
private handleDialogEvent(dialog: FinesseDialog): void {
switch (dialog.state) {
case 'ALERTING':
this.showIncomingCallPopup({
customerNumber: dialog.fromAddress,
serviceNumber: dialog.secondaryId,
dialogId: dialog.dialogId
});
break;
case 'ACTIVE':
this.activateCallControls();
this.triggerScreenPop(dialog.fromAddress);
break;
case 'DROPPED':
this.deactivateCallControls();
if (this.wrapUpRequired) {
this.showWrapUpPanel();
}
break;
case 'WRAP_UP':
this.submitWrapUp(dialog.dialogId, dialog.wrapUpReason);
break;
}
}
private updateAgentState(newState: AgentState): void {
this.agentState = newState;
// Update UI
this.stateSubject.next(newState);
// Notify Routing Engine (if state change is significant)
if (this.isSignificantStateChange(newState)) {
this.http.put(/voice/agents/${this.agentId}/state, {
state: newState,
timestamp: new Date().toISOString()
}).subscribe();
}
}
}
```
---
21.9 End-to-End Event Sequence for All Call Types
21.9.1 Complete SIMPLE Inbound Sequence
Time Source Event/Action CX Component
──── ────── ───────────── ────────────
T+0 Customer Dials 8001 —
T+1 CUCM Route to CVP/UCCX —
T+2 CVP/UCCX Routes to agent 1005 —
T+3 CUCM SIP INVITE to Agent Phone —
T+3 Agent Phone Rings, BiB initializes —
T+3 CUCM JTAPI: TermConnRingingEv JtapiListenerService
T+3 CUCM JTAPI: CallCtlConnAlertingEv JtapiListenerService
T+5 Agent Answers call —
T+5 CUCM JTAPI: TermConnTalkingEv JtapiListenerService
T+5 CUCM BiB fork RTP → Media Server —
T+5 Media Server Starts recording FreeSWITCH
T+5 Finesse XMPP: Dialog state=ACTIVE AgentDesk (cti.service)
T+5 AgentDesk Shows "On Call", screen pop Angular UI
T+5 AgentDesk POST /voice/agents/1005/state BUSY Routing Engine
T+30 CorrelationService Polls jtapi_event CorrelationService
T+30 CorrelationService Creates CallLeg (IN_PROGRESS) call_leg table
T+305 Customer Hangs up —
T+305 CUCM JTAPI: ConnDisconnectedEv JtapiListenerService
T+305 CUCM JTAPI: TermConnDroppedEv JtapiListenerService
T+305 CUCM BiB BYE → Media Server —
T+305 Media Server Stops recording, posts to Mixer FreeSWITCH → Mixer
T+305 Finesse XMPP: Dialog state=DROPPED AgentDesk
T+305 AgentDesk Shows wrap-up panel (if configured) Angular UI
T+305 Agent Selects wrap-up code —
T+330 CorrelationService Polls, finds end events CorrelationService
T+330 CorrelationService Updates CallLeg (COMPLETED) call_leg table
T+360 ConversationService Polls unsynced legs ConversationService
T+360 ConversationService Queries UCCE AW TCD JDBC
T+360 ConversationService Enriches CallLeg call_leg table
T+360 ConversationService Builds CIM, POST to CCM CCM REST API
T+360 CCM Creates activities Activities Timeline
T+360 CCM Conversation state = CLOSED Conversation
T+361 Mixer Processes recording Mixer Worker
T+361 Mixer Writes mixed file to NAS NAS/Storage
T+370 RecordingLinkSvc Polls CCM for voice activities Scheduled job
T+370 RecordingLinkSvc Finds recording, pushes URL CCM REST API
T+370 CCM Adds VOICE_RECORDING activity Activities Timeline
21.9.2 Complete TRANSFER Sequence
Time Source Event/Action CX Component
──── ────── ───────────── ────────────
T+0 Customer Dials 8001 —
T+5 Agent 1005 Answers —
T+5 CUCM JTAPI: TalkingEv (1005) JtapiListenerService
T+5 Media Server Starts recording leg 1 FreeSWITCH
T+120 Agent 1005 Presses Transfer, dials 1006 —
T+120 CUCM JTAPI: TermConnHeldEv (1005) JtapiListenerService
T+120 CUCM JTAPI: ConnInitiatedEv (1006) JtapiListenerService
T+122 CUCM JTAPI: TermConnRingingEv (1006) JtapiListenerService
T+125 Agent 1006 Answers consultation —
T+125 CUCM JTAPI: TalkingEv (1006) JtapiListenerService
T+125 Media Server Starts recording leg 2 FreeSWITCH
T+130 Agent 1005 Completes transfer —
T+130 CUCM JTAPI: TermConnDroppedEv (1005) JtapiListenerService
T+130 CUCM BiB BYE for 1005 —
T+130 Media Server Stops recording leg 1 FreeSWITCH
T+130 CUCM Customer now connected to 1006 —
T+400 Customer Hangs up —
T+400 CUCM JTAPI: ConnDisconnectedEv JtapiListenerService
T+400 CUCM JTAPI: TermConnDroppedEv (1006) JtapiListenerService
T+400 CUCM BiB BYE for 1006 —
T+400 Media Server Stops recording leg 2 FreeSWITCH
T+430 CorrelationService Processes all events CorrelationService
T+430 Creates TWO CallLegs call_leg table
T+430 Leg 1: 1005, start=T+5, end=T+130 —
T+430 Leg 2: 1006, start=T+125, end=T+400 —
T+460 ConversationService Enriches both legs ConversationService
T+460 Queries TCD twice (once per leg) JDBC
T+460 Sends 2× CALL_LEG_STARTED CCM
T+460 Sends 2× CALL_LEG_ENDED CCM
T+460 Sends 2× WRAPUP CCM
T+470 Mixer Processes 2 recordings Mixer
T+470 Mixes leg 1 + leg 2 —
T+470 OR produces separate files per leg NAS
---
21.10 Summary: Event Flow Decision Tree
[New JTAPI Event Received]
│
├─► Event Type?
│ │
│ ├─► CallCtlTermConnTalkingEv
│ │ │
│ │ ├─► First TalkingEv for this globalCallId?
│ │ │ ├─► YES → Create new CallLeg (status: IN_PROGRESS)
│ │ │ └─► NO → Update existing leg (resume from hold)
│ │ │
│ │ └─► Media Server already recording?
│ │ ├─► NO → Start new recording
│ │ └─► YES → Continue (resume)
│ │
│ ├─► ConnDisconnectedEv / CallCtlTermConnDroppedEv
│ │ │
│ │ ├─► Other extensions still active?
│ │ │ ├─► YES → Mark this leg COMPLETED, keep call active
│ │ │ └─► NO → Mark this leg COMPLETED, call ended
│ │ │
│ │ └─► Stop Media Server recording for this extension
│ │
│ ├─► CallCtlTermConnHeldEv
│ │ │
│ │ └─► Record hold segment start, pause recording marker
│ │
│ ├─► CallCtlConferencedEv
│ │ │
│ │ └─► Mark callType = CONFERENCE, link all agent legs
│ │
│ ├─► CallCtlConnInitiatedEv + agent extension
│ │ │
│ │ └─► Mark callType = OUTBOUND
│ │
│ └─► CallCtlTermConnRingingEv (no TalkingEv follows within RONA timeout)
│ │
│ └─► Mark callType = RONA
│
└─► [CorrelationService scheduled run]
│
├─► Group by globalCallId
├─► Sort by timestamp
├─► Detect call type from pattern
├─► Build legs per extension
├─► Calculate durations
├─► Mark events processed
└─► Save legs to call_leg table
[ConversationService scheduled run]
│
├─► Find legs: cim_sent = false AND status = COMPLETED
├─► For each leg:
│ ├─► Determine UCCE vs UCCX
│ ├─► Build SQL query with jtapiId
│ ├─► Execute query
│ ├─► If result found:
│ │ ├─► Enrich leg (ANI, wrap-up, agent name)
│ │ ├─► Build CIM messages
│ │ ├─► POST to CCM
│ │ └─► Mark cim_sent = true
│ └─► If no result:
│ ├─► Increment retry count
│ ├─► If retry < MAX: leave for next run
│ └─► If retry >= MAX: send partial CIM, mark failed
│
└─► [CCM receives CIM]
│
├─► CALL_LEG_STARTED → Create/update conversation, add activity
├─► CALL_LEG_ENDED → Update activity, close conversation if last leg
└─► WRAPUP → Add wrap-up activity, trigger analytics
```
---
## 22. Cisco Recording vs. EFCX Recording — Complete Architecture, Correlation & Component Analogy
This section explains the fundamental differences between Cisco-native recording and EFCX recording, how recordings are correlated with call events, the complete event flow, the unique identifiers used, and the distinct purpose of each recording-related component. Real-world analogies are provided for clarity.
---
22.1 The Core Difference — Cisco vs. EFCX Recording Philosophies
22.1.1 Conceptual Overview
|
Aspect |
Cisco-Integrated Recording |
EFCX-Native Recording |
|---|---|---|
|
Who records? |
Cisco CUCM forks audio via Built-in-Bridge (BiB) |
EFCX Media Server (FreeSWITCH) records directly |
|
Where does audio come from? |
Agent's IP Phone — the phone itself forks a copy of the audio |
SIP media path — the media server is in the RTP path |
|
Control plane |
CUCM JTAPI events drive the recording lifecycle |
SIP messages + EFCX internal events |
|
Call detail source |
UCCE AW |
EFCX internal database |
|
Correlation key |
|
|
|
Post-processing |
Mixer — async mixing of raw BiB files |
Real-time or near-real-time processing |
|
Recording delay |
30 seconds to 5 minutes (async mixer) |
Near-instant (in the media path) |
|
Agent awareness |
Agent does NOT know recording is happening |
Agent knows (UI indicator) |
|
Wrap-up handling |
From UCCE/UCCX database (post-call) |
From CCM activities (real-time) |
|
Storage |
NAS / shared storage |
EFCX storage or configured backend |
22.1.2 Real-World Analogy
Cisco Recording = Security Camera with External DVR
Imagine a bank with security cameras. The cameras (Cisco IP Phones) continuously stream video. A separate device (the DVR — our Media Server) records the stream. But the cameras also have a special feature: they can make a duplicate copy of the video and send it to a second DVR (the BiB fork to FreeSWITCH). The bank manager (CUCM) decides when to start and stop this duplicate copy based on motion sensors (JTAPI events). After hours, a technician (Mixer) takes all the raw footage from multiple cameras and combines them into one seamless video with timestamps. Finally, the combined video is filed in the vault (NAS) and linked to the incident report (CCM conversation).
EFCX Recording = Smartphone Screen Recording
When you start a video call on your smartphone and hit "record," the recording happens inside the app itself. The audio and video pass through the app, and the app saves a copy directly to your phone's storage. There's no external DVR — the app IS the recorder. You know recording is active because you see a red dot. When the call ends, the recording is immediately available.
---
22.2 How Cisco Recording Works — Complete Flow
22.2.1 The Four-Stage Recording Pipeline
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ CISCO RECORDING PIPELINE — 4 STAGES │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ STAGE 1: CAPTURE (Cisco Phone + CUCM + Media Server) │
│ ═══════════════════════════════════════════════════════ │
│ │
│ [Customer] ◄────RTP────► [CUCM] ◄────RTP────► [Agent Phone] │
│ │ │
│ │ BiB Fork (SPAN port equivalent) │
│ ▼ │
│ [Media Server — FreeSWITCH] │
│ │ │
│ │ • Receives SIP INVITE from CUCM for BiB leg │
│ │ • Answers and starts recording via `record.lua` │
│ │ • Writes raw WAV: `/streams/{gcid}_{timestamp}.wav` │
│ ▼ │
│ [Raw Recording File] │
│ │
│ STAGE 2: HANDOFF (FreeSWITCH → Mixer) │
│ ═══════════════════════════════════════ │
│ │
│ When call ends (BYE received): │
│ │
│ [FreeSWITCH — cx_hangup.lua] │
│ │ │
│ │ POST /mixer/sip-data │
│ │ { │
│ │ "callId": "167890", │
│ │ "filePath": "/streams/167890_20240115T092345.wav", │
│ │ "duration": 300, │
│ │ "agentExtension": "1005", │
│ │ "ani": "+18005550199", │
│ │ "startTime": "2024-01-15T09:23:45Z", │
│ │ "endTime": "2024-01-15T09:28:45Z" │
│ │ } │
│ ▼ │
│ [Mixer — Message Queue (RabbitMQ)] │
│ │
│ STAGE 3: POST-PROCESSING (Mixer) │
│ ═════════════════════════════════ │
│ │
│ [Mixer Worker] │
│ │ │
│ │ 1. Dequeue job │
│ │ 2. Read recording rules from DB (per-tenant, per-call-type) │
│ │ 3. Evaluate rules: │
│ │ • Should this recording be kept? (compliance check) │
│ │ • Mono or stereo? (agent + customer separated?) │
│ │ • What quality/bitrate? │
│ │ • Encrypt? │
│ │ 4. Process audio: │
│ │ • Mix multiple BiB legs into single file │
│ │ • OR split into separate files per leg │
│ │ • Apply normalization, trimming │
│ │ 5. Write final file: │
│ │ `/recordings/{tenant}/{date}/{recordingId}_mixed.wav` │
│ │ 6. Write metadata JSON: │
│ │ `/recordings/{tenant}/{date}/{recordingId}.json` │
│ ▼ │
│ [NAS / Shared Storage] │
│ │
│ STAGE 4: LINKAGE (Recording Link Activities → CCM) │
│ ══════════════════════════════════════════════════ │
│ │
│ [Recording Link Activities — Scheduled Job] │
│ │ │
│ │ Every N minutes: │
│ │ 1. Query CCM: GET /conversation-manager/activities/voice │
│ │ 2. For each completed voice leg: │
│ │ • Match by callId + agentExtension + time window │
│ │ 3. Build secure URL: │
│ │ `https://middleware/{callId}:{agent}:{ani}:{epoch}/recording-file` │
│ │ 4. POST to CCM: │
│ │ /conversation-manager/activities/third-party │
│ │ CIM: intent = "VOICE_RECORDING" │
│ │ { legId, voiceRecordingUrl, duration, fileSize } │
│ ▼ │
│ [CCM — Conversation Activities] │
│ │ │
│ │ • Adds VOICE_RECORDING activity to timeline │
│ │ • Supervisor can click URL to play │
│ │ • URL expires after configured time (e.g., 24 hours) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────┘
22.2.2 Stage 1: Capture — Deep Dive
[Agent Phone — Cisco 8861/8841/7841 with BiB]
│
│ Configuration on CUCM:
│ • Built-in Bridge = ENABLED
│ • Recording Media Source = Gateway Preferred
│ • Recording Profile = "CX_Recording_Profile"
│
│ When call is active:
│ ┌─────────────────────────────────────┐
│ │ Phone creates TWO RTP streams: │
│ │ │
│ │ Stream A: Agent ↔ Customer │
│ │ Stream B: Agent audio → CUCM │
│ │ (BiB fork) │
│ │ │
│ │ Stream A is the actual call. │
│ │ Stream B is a COPY for recording. │
│ └─────────────────────────────────────┘
│
│ BiB behavior by scenario:
│ • SIMPLE: One BiB stream per call
│ • TRANSFER: BiB follows agent — first agent's BiB stops, second starts
│ • CONFERENCE: All agents' BiB streams run simultaneously
│ • CONSULT: Two BiB streams briefly (consulting agent + consulted agent)
▼
[CUCM]
│
│ Receives BiB stream from phone
│ Forwards to Recording SIP Trunk
│
│ SIP INVITE for BiB leg:
│ From: <sip:1005@expertflow.com>
│ To: <sip:record@expertflow.com>
│ Call-ID: 167890_bib_1@cucm
│ X-Cisco-Recording-Type: automatic
│ X-Cisco-Call-ID: 167890
▼
[Media Server — FreeSWITCH]
│
│ Receives SIP INVITE on recording SIP profile
│
│ FreeSWITCH dialplan for recording:
│ <extension name="cisco_recording">
│ <condition field="destination_number" expression="^record$">
│ <action application="lua" data="record.lua"/>
│ </condition>
│ </extension>
│
│ record.lua actions:
│ 1. Extract GCID from SIP headers: X-Cisco-Call-ID
│ 2. Set recording path: /streams/{gcid}_{isoTimestamp}.wav
│ 3. Execute: session:recordFile(path, maxDuration)
│ 4. Log: "Recording started for GCID 167890"
▼
[Raw WAV File on Media Server]
│
│ File: /streams/167890_20240115T092345.wav
│ Format: WAV, 8kHz, 16-bit, mono
│ Content: Agent's audio ONLY (one side of conversation)
│ Size: ~240KB per minute
22.2.3 The BiB Recording Problem — Why Mixer is Needed
PROBLEM: BiB records ONE SIDE only
A standard BiB recording contains ONLY the agent's audio.
The customer's audio is on the main RTP stream, not the BiB fork.
SIMPLE Call Recording:
┌─────────────────────────────────────────────────────────────┐
│ Call: Customer (+1800...) ↔ Agent 1005 │
│ │
│ BiB Stream from Agent 1005: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [Agent Voice Only] │ │
│ │ "Thank you for calling, how can I help?" │ │
│ │ "Let me check that for you..." │ │
│ │ │ │
│ │ MISSING: Customer's side of conversation! │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
SOLUTION: Mixer combines multiple sources
The Mixer has access to:
1. Agent BiB recording (from Media Server)
2. Customer-side recording (if available — some deployments record both legs)
3. Call metadata (who talked when)
For most Cisco deployments, the solution is:
- Record agent BiB (primary)
- Customer audio is reconstructed from the same BiB if CUCM sends both directions
- OR: Two separate BiB streams are created (agent outbound + agent inbound)
- Mixer combines them into stereo or mono mixed file
NOTE: In many Cisco setups, CUCM actually sends BOTH audio directions
in the BiB stream (agent hears customer, customer's audio is present
in the RTP stream received by the phone). The exact behavior depends on:
- Phone model and firmware
- CUCM recording profile configuration
- "Recording Media Source" setting (Phone Preferred vs Gateway Preferred)
```
---
22.3 Unique Identifiers in Cisco — Complete Reference
22.3.1 Identifier Hierarchy
┌─────────────────────────────────────────────────────────────────────────────┐
│ CISCO IDENTIFIER HIERARCHY │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ LEVEL 1: CUCM Call Identifier │
│ ═══════════════════════════════════ │
│ │
│ globalCallId (GCID) │
│ ├─► Format: {CallManagerID}_{GlobalCallID} │
│ ├─► Example: "1_167890" │
│ ├─► Scope: Unique within CUCM cluster │
│ ├─► Source: JTAPI Call.getCallID() │
│ ├─► Persisted in: jtapi_event.call_id, call_leg.jtapi_id │
│ └─► Used for: Correlating JTAPI events to a single call │
│ │
│ LEVEL 2: UCCE Peripheral Call Key │
│ ══════════════════════════════════ │
│ │
│ PeripheralCallKey │
│ ├─► Format: Integer (e.g., 167890) │
│ ├─► Scope: Unique within UCCE peripheral (PG) │
│ ├─► Source: UCCE TCD table │
│ ├─► Maps to: globalCallId (numeric portion) │
│ ├─► Persisted in: Termination_Call_Detail.PeripheralCallKey │
│ └─► Used for: UCCE database query, correlating TCD to JTAPI events │
│ │
│ LEVEL 3: UCCX Contact ID │
│ ═════════════════════════ │
│ │
│ contactid │
│ ├─► Format: Integer (e.g., 167890) │
│ ├─► Scope: Unique within UCCX server │
│ ├─► Source: UCCX ContactCallDetail table │
│ ├─► Maps to: globalCallId (numeric portion) │
│ ├─► Persisted in: ContactCallDetail.contactid │
│ └─► Used for: UCCX database query │
│ │
│ LEVEL 4: Call Leg Identifier │
│ ══════════════════════════ │
│ │
│ call_leg.id │
│ ├─► Format: UUID or BIGSERIAL │
│ ├─► Scope: Unique within CX database │
│ ├─► Source: Generated by CorrelationService │
│ ├─► Used for: Internal CX reference, CIM messages │
│ └─► Maps to: One agent extension + one globalCallId │
│ │
│ LEVEL 5: Recording File Identifier │
│ ════════════════════════════════ │
│ │
│ recordingId │
│ ├─► Format: UUID (e.g., "rec-a1b2c3d4...") │
│ ├─► Scope: Unique globally │
│ ├─► Source: Generated by Mixer │
│ ├─► Used for: File naming, URL generation │
│ └─► Maps to: One or more call_leg records │
│ │
│ LEVEL 6: Conversation Identifier │
│ ══════════════════════════════ │
│ │
│ conversationId │
│ ├─► Format: UUID │
│ ├─► Scope: Unique within CCM │
│ ├─► Source: Generated by CCM when first CIM received │
│ ├─► Used for: CCM conversation timeline │
│ └─► Maps to: One or more call legs │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
22.3.2 Identifier Mapping Flow
[JTAPI Event]
│
│ globalCallId = "1_167890"
│
▼
[jtapi_event table]
│
│ call_id = "1_167890"
│
▼
[CorrelationService]
│
│ Extracts numeric portion: "167890"
│ Creates CallLeg with jtapi_id = "167890"
│
▼
[call_leg table]
│
│ id = 12345 (auto-generated)
│ jtapi_id = "167890"
│ agent_extension = "1005"
│
▼
[ConversationService]
│
│ Queries UCCE: WHERE PeripheralCallKey = '167890'
│ Queries UCCX: WHERE contactid = 167890
│
▼
[UCCE TCD / UCCX CCD]
│
│ Returns: ANI, wrap-up, agent ID, etc.
│
▼
[CCM CIM]
│
│ jtapiId = "167890"
│ legId = "12345"
│ conversationId = "conv-uuid" (generated by CCM)
│
▼
[Mixer]
│
│ Receives: callId = "167890" from FreeSWITCH
│ Generates: recordingId = "rec-uuid"
│ File: rec-uuid_mixed.wav
│
▼
[Recording Link Activities]
│
│ Matches: call_leg.jtapi_id (167890) ↔ recording metadata
│ URL: /{recordingId}/play
│
▼
[CCM Activities]
│
│ VOICE_RECORDING activity
│ Links to conversationId
22.3.3 Why So Many Identifiers?
|
Identifier |
Why It Exists |
Why Not Just One? |
|---|---|---|
|
|
CUCM needs a unique call ID |
CUCM cluster may have multiple CallManager nodes |
|
|
UCCE PG needs its own ID |
PG may see multiple peripherals; ID scoped to PG |
|
|
UCCX uses different schema |
UCCX and UCCE are separate products with different databases |
|
|
CX needs to track individual legs |
One call may have multiple agents (transfer, conference) |
|
|
Mixer needs UUID for files |
File systems need unique names; GCID may collide across days |
|
|
CCM groups all channels |
A conversation may include voice, chat, email — need unified view |
Analogy: A Hospital Patient
- globalCallId = Hospital admission number (unique for this visit)
- PeripheralCallKey = Insurance claim number (external system's ID)
- call_leg.id = Individual procedure ID (each test/surgery has its own record)
- recordingId = Medical imaging file ID (each X-ray/MRI has a file number)
- conversationId = Patient chart number (groups all visits and records for this person)
---
22.4 Recording Correlation — How Recordings Match to Calls
22.4.1 The Correlation Challenge
PROBLEM: Recording files and call events live in different systems
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ CUCM / JTAPI │ │ Media Server │ │ UCCE / UCCX │
│ (Call Events) │ │ (Recording Files) │ │ (Call Details) │
├─────────────────────┤ ├─────────────────────┤ ├─────────────────────┤
│ globalCallId: │ │ File: │ │ PeripheralCallKey: │
│ 167890 │ │ 167890_2024... │ │ 167890 │
│ Event: TalkingEv │ │ .wav │ │ ANI: +1800... │
│ Extension: 1005 │ ??? │ Size: 1.2MB │ ??? │ Wrap-up: RESOLVED │
│ Time: 09:23:45 │◄───────►│ Created: 09:28:45 │◄───────►│ TalkTime: 300s │
│ │ │ │ │ │
│ Lives in: │ │ Lives in: │ │ Lives in: │
│ PostgreSQL │ │ File system │ │ SQL Server / │
│ │ │ │ │ Informix │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
The challenge: How do we know that file "167890_20240115T092345.wav"
belongs to the call with globalCallId 167890, agent 1005, ANI +1800...?
```
22.4.2 Correlation Strategy — Three-Phase Matching
PHASE 1: GCID Match (Strongest)
═══════════════════════════════════════════════════════════════════════════════
The recording filename CONTAINS the GCID:
File: /streams/167890_20240115T092345.wav
GCID: 167890
This is BY DESIGN. The FreeSWITCH record.lua script extracts the GCID
from the SIP header X-Cisco-Call-ID and includes it in the filename.
Match confidence: 100% (filename contains exact GCID)
PHASE 2: Time Window Match (Validation)
═══════════════════════════════════════════════════════════════════════════════
Recording file timestamp must fall within the call's time window:
CallLeg:
startTime: 2024-01-15T09:23:45Z
endTime: 2024-01-15T09:28:45Z
Recording file:
created: 2024-01-15T09:23:45Z ← matches start
modified: 2024-01-15T09:28:45Z ← matches end
Validation rule:
recording.created >= callLeg.startTime - 5s
AND recording.modified <= callLeg.endTime + 5s
PHASE 3: Agent Extension Match (Multi-Leg Disambiguation)
═══════════════════════════════════════════════════════════════════════════════
For calls with multiple agents (transfer, conference), we need to know
WHICH recording belongs to WHICH agent.
For TRANSFER calls:
Leg 1: Agent 1005, start=09:23:45, end=09:32:20
Leg 2: Agent 1006, start=09:32:10, end=09:35:20
Recording 1: 167890_20240115T092345.wav, duration ~525s
→ But wait, this spans BOTH legs!
→ The BiB stream from Agent 1005 stops at 09:32:20
→ A NEW BiB stream starts for Agent 1006
Recording 1 actually: 167890_bib1.wav (agent 1005)
Recording 2 actually: 167890_bib2.wav (agent 1006)
The Mixer receives both and handles them appropriately.
For CONFERENCE calls:
Both agents' BiB streams run simultaneously
Mixer receives both and may create:
- One combined file (all parties)
- OR separate files per agent
```
22.4.3 Correlation Data Flow
[FreeSWITCH — Call Ends]
│
│ 1. Closes WAV file
│ 2. cx_hangup.lua executes:
│
│ POST https://mixer/api/sip-data
│ {
│ "callId": "167890", ← GCID (primary key)
│ "agentExtension": "1005", ← Agent DN
│ "filePath": "/streams/167890_...",
│ "startTime": "2024-01-15T09:23:45Z",
│ "endTime": "2024-01-15T09:28:45Z",
│ "duration": 300,
│ "ani": "+18005550199",
│ "dnis": "8001",
│ "recordingType": "AGENT_BIB"
│ }
▼
[Mixer — Receives SIP Data]
│
│ 3. Stores in mixer_job table:
│ {
│ jobId: "job-uuid",
│ callId: "167890",
│ agentExtension: "1005",
│ filePath: "/streams/167890_...",
│ status: "PENDING",
│ createdAt: "2024-01-15T09:28:46Z"
│ }
▼
[Mixer — Scheduled Processing]
│
│ 4. Worker picks up job
│ 5. Reads recording rules (tenant config)
│ 6. Processes audio
│ 7. Writes to NAS:
│ /recordings/{tenant}/2024-01-15/rec-uuid_mixed.wav
│
│ 8. Updates mixer_job:
│ status: "COMPLETED"
│ outputPath: "/recordings/.../rec-uuid_mixed.wav"
│ recordingId: "rec-uuid"
▼
[Recording Link Activities — Scheduled Job]
│
│ 9. Every 5 minutes:
│ a. Query CCM for voice activities in last hour
│ b. For each voice leg:
│ - Get callId (jtapiId from CIM)
│ - Query mixer_job by callId + agentExtension
│ - If match found:
│ * Build URL: https://middleware/rec-uuid/play
│ * POST CIM to CCM
│ c. Mark as linked
▼
[CCM — VOICE_RECORDING Activity]
│
│ 10. Activity appears on conversation timeline
│ 11. Supervisor clicks URL → Middleware serves file
---
22.5 Recording Events — Complete Event Sequence
22.5.1 Recording Event Timeline (SIMPLE Call)
Time Source Recording Event File State
──── ────── ─────────────── ──────────
T+0 FreeSWITCH SIP INVITE received (BiB) No file
T+0 FreeSWITCH record.lua starts File OPENED
T+0 FreeSWITCH Recording header written /streams/167890_...wav
T+5 FreeSWITCH Continuous RTP write File GROWING
T+30 FreeSWITCH Continuous RTP write File GROWING
T+60 FreeSWITCH Continuous RTP write File GROWING
... ... ... ...
T+300 Customer Hangs up —
T+300 FreeSWITCH BYE received —
T+300 FreeSWITCH Recording stops File CLOSED
T+300 FreeSWITCH FILE_CLOSE event fired File COMPLETE
T+300 FreeSWITCH cx_hangup.lua executes —
T+300 FreeSWITCH POST /mixer/sip-data —
T+301 Mixer Job queued (RabbitMQ) —
T+301 Mixer Job persisted to mixer_job table status=PENDING
T+330 Mixer Worker Job picked up by worker status=PROCESSING
T+331 Mixer Worker Reads recording rules —
T+332 Mixer Worker Reads WAV file from /streams/ —
T+333 Mixer Worker Applies audio processing —
T+335 Mixer Worker Writes mixed file to NAS /recordings/.../rec-uuid_mixed.wav
T+335 Mixer Worker Writes metadata JSON /recordings/.../rec-uuid.json
T+336 Mixer Worker Updates mixer_job status=COMPLETED
T+360 RecordingLinkSvc Polls CCM for voice activities —
T+361 RecordingLinkSvc Finds voice leg with jtapiId=167890 —
T+361 RecordingLinkSvc Queries mixer_job by callId Match found
T+362 RecordingLinkSvc Builds secure URL https://middleware/rec-uuid/play
T+362 RecordingLinkSvc POST CIM to CCM 200 OK
T+362 CCM VOICE_RECORDING activity created Timeline updated
22.5.2 Recording Events (TRANSFER Call — Two Legs)
Time Source Event Recording
──── ────── ───── ─────────
T+0 FreeSWITCH BiB INVITE for Agent 1005 File A OPENED
T+0 FreeSWITCH record.lua (1005) /streams/167890_...A.wav
T+5 FreeSWITCH Continuous write File A GROWING
... ... ... ...
T+120 Agent 1005 Presses Transfer —
T+120 Agent 1005 Call on hold —
T+125 Agent 1006 Answers consult —
T+125 FreeSWITCH BiB INVITE for Agent 1006 File B OPENED
T+125 FreeSWITCH record.lua (1006) /streams/167890_...B.wav
T+125 FreeSWITCH Now writing TWO files A + B GROWING
... ... ... ...
T+130 Agent 1005 Completes transfer —
T+130 FreeSWITCH BYE for 1005 BiB File A CLOSED
T+130 FreeSWITCH cx_hangup.lua for A POST /mixer/sip-data (A)
T+130 Mixer Job A queued —
... ... ... ...
T+400 Customer Hangs up —
T+400 FreeSWITCH BYE for 1006 BiB File B CLOSED
T+400 FreeSWITCH cx_hangup.lua for B POST /mixer/sip-data (B)
T+400 Mixer Job B queued —
T+430 Mixer Worker Processes Job A Output: rec-A_mixed.wav
T+431 Mixer Worker Processes Job B Output: rec-B_mixed.wav
T+460 RecordingLinkSvc Links both recordings to CCM 2 VOICE_RECORDING activities
---
22.6 Component Purposes — Complete Analogy
22.6.1 Component Map with Analogies
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ RECORDING COMPONENTS — PURPOSE & ANALOGY │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ COMPONENT 1: JTAPI Connector (JtapiListenerService) │ │
│ │ ═══════════════════════════════════════════════════ │ │
│ │ │ │
│ │ REAL ANALOGY: Court Stenographer │ │
│ │ │ │
│ │ A court stenographer sits in the courtroom and types EVERYTHING that happens. │ │
│ │ They don't make decisions — they just record. Every objection, every answer, │ │
│ │ every pause is captured in real-time. │ │
│ │ │ │
│ │ PURPOSE: │ │
│ │ • Connects to CUCM JTAPI Provider (TCP port 2748) │ │
│ │ • Subscribes to call events for ALL monitored extensions │ │
│ │ • Receives events in REAL-TIME as they happen │ │
│ │ • Persists EVERY event to jtapi_event table (no filtering) │ │
│ │ • Does NOT process or correlate — just stores │ │
│ │ │ │
│ │ INPUT: JTAPI events from CUCM │ │
│ │ OUTPUT: Raw events in PostgreSQL │ │
│ │ │ │
│ │ KEY CHARACTERISTICS: │ │
│ │ • Must be always running (daemon) │ │
│ │ • Handles reconnection if CUCM restarts │ │
│ │ • Event buffer for burst handling │ │
│ │ • No business logic — pure event capture │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ COMPONENT 2: Cisco Connector (CorrelationService + ConversationService) │ │
│ │ ══════════════════════════════════════════════════════════════════════ │ │
│ │ │ │
│ │ REAL ANALOGY: Court Clerk Who Organizes Case Files │ │
│ │ │ │
│ │ After the trial, a clerk takes the stenographer's transcript and organizes it │ │
│ │ into a proper case file. They cross-reference with evidence tags, attach │ │
│ │ police reports, and file it under the correct case number. │ │
│ │ │ │
│ │ PURPOSE: │ │
│ │ • Polls jtapi_event table for unprocessed events │ │
│ │ • CORRELATES events into CallLegs (start + end matching) │ │
│ │ • DETECTS call type (SIMPLE, TRANSFER, CONSULT, etc.) │ │
│ │ • QUERIES UCCE/UCCX database for enrichment (ANI, wrap-up, agent name) │ │
│ │ • BUILDS CIM messages │ │
│ │ • SENDS CIM to CCM via REST API │ │
│ │ │ │
│ │ INPUT: Raw jtapi_event rows, UCCE/UCCX SQL results │ │
│ │ OUTPUT: CallLeg records, CIM messages to CCM │ │
│ │ │ │
│ │ KEY CHARACTERISTICS: │ │
│ │ • Runs on SCHEDULE (every 30-60 seconds), NOT event-driven │ │
│ │ • Contains ALL business logic for call correlation │ │
│ │ • Handles UCCE vs UCCX query differences │ │
│ │ • Retry logic for failed enrichment │ │
│ │ • Converts Cisco data model to CX data model │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ COMPONENT 3: Media Server (FreeSWITCH) │ │
│ │ ══════════════════════════════════════ │ │
│ │ │ │
│ │ REAL ANALOGY: Security Camera DVR │ │
│ │ │ │
│ │ A DVR in a security system receives video feeds from cameras, records them │ │
│ │ to a hard drive, and can play them back on demand. It handles multiple │ │
│ │ cameras simultaneously and manages file storage. │ │
│ │ │ │
│ │ PURPOSE: │ │
│ │ • Receives SIP INVITE from CUCM for BiB recording legs │ │
│ │ • Answers SIP and establishes RTP receive path │ │
│ │ • Records audio to WAV files using `record.lua` │ │
│ │ • Manages multiple simultaneous recordings │ │
│ │ • When call ends, triggers `cx_hangup.lua` to notify Mixer │ │
│ │ • Serves recordings on demand (via HTTP or ESL) │ │
│ │ │ │
│ │ INPUT: SIP INVITE (BiB) from CUCM, RTP audio streams │ │
│ │ OUTPUT: Raw WAV files, HTTP POST to Mixer │ │
│ │ │ │
│ │ KEY CHARACTERISTICS: │ │
│ │ • Handles 100+ simultaneous recordings │ │
│ │ • Files written in real-time (streaming) │ │
│ │ • No processing — raw audio only │ │
│ │ • Lua scripts for call lifecycle hooks │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ COMPONENT 4: Mixer (Recording Mixer Service) │ │
│ │ ══════════════════════════════════════════════ │ │
│ │ │ │
│ │ REAL ANALOGY: Video Editor / Post-Production Studio │ │
│ │ │ │
│ │ After filming, a video editor takes raw footage from multiple cameras, │ │
│ │ synchronizes them, adds titles, applies color correction, and exports a │ │
│ │ final video file. This happens AFTER filming is complete. │ │
│ │ │ │
│ │ PURPOSE: │ │
│ │ • Receives recording jobs from Media Server (via HTTP or queue) │ │
│ │ • Reads recording rules (what to do with each recording) │ │
│ │ • Processes audio: mixing, splitting, normalizing, converting │ │
│ │ • Writes final processed files to NAS │ │
│ │ • Generates metadata JSON files │ │
│ │ • Updates job status in database │ │
│ │ │ │
│ │ INPUT: Raw WAV files, call metadata, recording rules │ │
│ │ OUTPUT: Mixed WAV files, metadata JSON, completed job records │ │
│ │ │ │
│ │ KEY CHARACTERISTICS: │ │
│ │ • ASYNC post-processing (happens after call ends) │ │
│ │ • Queue-based (RabbitMQ) for reliability │ │
│ │ • Rules-driven (different processing per tenant/call type) │ │
│ │ • Can mix multiple legs into single file │ │
│ │ • Can produce stereo (agent left, customer right) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ COMPONENT 5: Recording Middleware (File Server) │ │
│ │ ═══════════════════════════════════════════════ │ │
│ │ │ │
│ │ REAL ANALOGY: Digital Library / Streaming Server │ │
│ │ │ │
│ │ A digital library stores all the finished videos and serves them to authorized │ │
│ │ users when requested. It handles authentication, bandwidth, and access control.│ │
│ │ │ │
│ │ PURPOSE: │ │
│ │ • Serves recording files on demand via HTTP(S) │ │
│ │ • Authenticates requests (validates tokens/session) │ │
│ │ • Streams audio to browser-based players │ │
│ │ • Generates time-limited secure URLs │ │
│ │ • Logs access for compliance │ │
│ │ │ │
│ │ INPUT: HTTP GET request with token │ │
│ │ OUTPUT: Audio stream (WAV/MP3) │ │
│ │ │ │
│ │ KEY CHARACTERISTICS: │ │
│ │ • Stateless — just serves files │ │
│ │ • URL expiration (e.g., 24 hours) │ │
│ │ • Range request support for seeking │ │
│ │ • Bandwidth throttling option │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ COMPONENT 6: Recording Link Activities (Scheduled Job) │ │
│ │ ══════════════════════════════════════════════════════ │ │
│ │ │ │
│ │ REAL ANALOGY: Mailroom Clerk Who Delivers Files │ │
│ │ │ │
│ │ A mailroom clerk periodically checks for new files, looks up who they belong │ │
│ │ to, and delivers them to the right person's desk. They don't create the files │ │
│ │ — they just make sure they get to the right place. │ │
│ │ │ │
│ │ PURPOSE: │ │
│ │ • Scheduled job (runs every 5-15 minutes) │ │
│ │ • Queries CCM for voice call activities that don't have recordings yet │ │
│ │ • Queries Mixer database for completed recordings │ │
│ │ • MATCHES voice legs to recording files (by GCID + time) │ │
│ │ • Builds secure playback URL │ │
│ │ • Pushes VOICE_RECORDING CIM to CCM │ │
│ │ │ │
│ │ INPUT: CCM voice activities, Mixer completed jobs │ │
│ │ OUTPUT: VOICE_RECORDING CIM messages to CCM │ │
│ │ │ │
│ │ KEY CHARACTERISTICS: │ │
│ │ • Bridge between recording system and conversation system │ │
│ │ • Idempotent (can run multiple times safely) │ │
│ │ • Handles missed links on retry │ │
│ │ • Configurable matching time window │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ COMPONENT 7: QM Connector (Quality Management Connector) │ │
│ │ ════════════════════════════════════════════════════════ │ │
│ │ │ │
│ │ REAL ANALOGY: External Audit Firm │ │
│ │ │ │
│ │ An external audit firm receives copies of important documents, evaluates them │ │
│ │ against standards, and produces compliance reports. They work independently │ │
│ │ of the main organization. │ │
│ │ │ │
│ │ PURPOSE: │ │
│ │ • OPTIONAL component for QM integration (e.g., Verint, NICE, Calabrio) │ │
│ │ • Receives recording metadata from Mixer │ │
│ │ • Formats data for external QM system │ │
│ │ • Pushes recordings/metadata to QM platform │ │
│ │ • Receives evaluation results from QM (scores, tags) │ │
│ │ • Can sync QM evaluations back to CCM │ │
│ │ │ │
│ │ INPUT: Recording metadata from Mixer │ │
│ │ OUTPUT: QM-formatted data, evaluation results │ │
│ │ │ │
│ │ KEY CHARACTERISTICS: │ │
│ │ • Optional — only deployed if QM integration required │ │
│ │ • Adapter pattern (different adapter per QM vendor) │ │
│ │ • Async batch processing │ │
│ │ • Handles QM-specific data formats │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────┘
22.6.2 Component Interaction Summary
|
Component |
Receives From |
Sends To |
When |
Key Data |
|---|---|---|---|---|
|
JTAPI Connector |
CUCM JTAPI Provider |
PostgreSQL (jtapi_event) |
Real-time |
JTAPI events |
|
Cisco Connector |
PostgreSQL, UCCE/UCCX DB |
CCM (CIM) |
Scheduled (30-60s) |
CallLegs, CIM |
|
Media Server |
CUCM (SIP BiB) |
NAS (raw WAV), Mixer (HTTP) |
Real-time |
Raw recordings |
|
Mixer |
Media Server (HTTP), NAS |
NAS (mixed files), DB |
Async (post-call) |
Mixed recordings |
|
Middleware |
NAS |
Browser (HTTP stream) |
On-demand |
Audio stream |
|
Recording Link Activities |
CCM, Mixer DB |
CCM (CIM) |
Scheduled (5-15m) |
Recording URLs |
|
QM Connector |
Mixer |
External QM system |
Scheduled |
QM-formatted data |
---
22.7 Recording vs. EFCX Recording — Side-by-Side Comparison
22.7.1 Architecture Comparison Diagram
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ CISCO-INTEGRATED RECORDING vs EFCX-NATIVE RECORDING │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ CISCO-INTEGRATED (This Document's Focus) │
│ ════════════════════════════════════════ │
│ │
│ Customer ──RTP──► CUCM ──RTP──► Agent Phone │
│ │ │
│ │ BiB Fork (phone copies audio) │
│ ▼ │
│ [Media Server — FreeSWITCH] │
│ │ │
│ │ Records raw WAV │
│ ▼ │
│ [Mixer — Post-processes] │
│ │ │
│ │ Mixes, splits, applies rules │
│ ▼ │
│ [NAS / Storage] │
│ │ │
│ │ File: {recordingId}_mixed.wav │
│ ▼ │
│ [Middleware] ◄── GET ── Supervisor │
│ │
│ Characteristics: │
│ • Recording controlled by CUCM (Cisco is the "boss") │
│ • Agent phone is the SOURCE of recording audio │
│ • Post-processing REQUIRED (async mixer) │
│ • Delay: 30s to 5 minutes before recording available │
│ • Correlation via GCID (Cisco identifier) │
│ • Wrap-up from UCCE/UCCX database │
│ │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ EFCX-NATIVE RECORDING (For Reference) │
│ ═════════════════════════════════════ │
│ │
│ Customer ──RTP──► [EFCX Media Server] ──RTP──► Agent Browser (SIP.js/WebRTC) │
│ │ │
│ │ Records directly in media path │
│ ▼ │
│ [EFCX Storage] │
│ │ │
│ │ File: {channelId}.wav │
│ ▼ │
│ [CCM] ◄── Direct integration ── Activity created immediately │
│ │
│ Characteristics: │
│ • Recording controlled by EFCX (EFCX is the "boss") │
│ • Media server is IN the audio path (not a fork) │
│ • No post-processing needed (real-time) │
│ • Delay: Near-instant (seconds) │
│ • Correlation via channelId (EFCX identifier) │
│ • Wrap-up from CCM activities │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────┘
22.7.2 Detailed Comparison Table
|
Aspect |
Cisco-Integrated |
EFCX-Native |
|---|---|---|
|
Recording trigger |
CUCM JTAPI events |
EFCX internal state machine |
|
Audio source |
Agent phone BiB fork |
Media server in RTP path |
|
Who decides to record? |
Cisco CUCM (admin-configured) |
EFCX application logic |
|
Recording start |
When CUCM establishes BiB |
When agent answers in EFCX |
|
Recording stop |
When CUCM tears down BiB |
When call ends in EFCX |
|
Raw file location |
Media Server |
EFCX Media Server storage |
|
File format |
WAV, 8kHz, mono |
WAV or MP3, configurable |
|
Post-processing |
Required (Mixer) |
Optional/minimal |
|
Mixing needed? |
Yes — BiB is one-sided |
No — media server has both sides |
|
Availability delay |
30s–5min |
1–10 seconds |
|
Correlation key |
|
|
|
Enrichment source |
UCCE AW / UCCX |
EFCX internal database |
|
Wrap-up source |
UCCE |
CCM wrap-up activity |
|
Agent awareness |
Silent (agent doesn't know) |
Visible (UI indicator) |
|
Pause/resume |
Controlled by CUCM |
Controlled by agent UI |
|
Multi-leg handling |
Mixer combines multiple BiB files |
Single continuous file |
|
Transfer recording |
Two separate files (per agent) |
One file (media server handles) |
|
Conference recording |
Multiple BiB files mixed |
Single file with all parties |
|
Storage path |
NAS: |
EFCX storage backend |
|
URL generation |
Recording Link Activities (scheduled) |
Real-time on call end |
|
QM integration |
QM Connector (optional) |
Built-in or adapter |
|
Compliance |
Cisco-certified (if using Cisco recorder) |
EFCX-managed |
|
Encryption |
At-rest (NAS), in-transit (HTTPS) |
Configurable |
|
Redundancy |
Mixer HA, NAS replication |
Media server clustering |
---
22.8 Real-World Example: Complete Recording Lifecycle
22.8.1 Scenario: Customer Calls, Talks to Agent, Call Recorded
THE STORY: Alice calls ExpertFlow support at 1-800-555-0199
CHARACTERS:
• Alice (Customer) — Calling from +1-555-123-4567
• Bob (Agent) — Extension 1005, Employee ID 5001
• The Phone — Cisco 8861 with Built-in Bridge enabled
• CUCM — Call manager
• FreeSWITCH — Media server
• Mixer — Recording post-processor
• CCM — Conversation manager
═══════════════════════════════════════════════════════════════════════════════
ACT 1: THE CALL BEGINS (1:23 PM)
═══════════════════════════════════════════════════════════════════════════════
Alice: "I need help with my account."
[Alice's Phone] ──► [PSTN] ──► [CUBE] ──► [CUCM]
│
│ CUCM: "Route 1-800-555-0199 to Support queue"
▼
[CVP/UCCX]
│
│ "Thank you for calling ExpertFlow..."
│ "Your call is being transferred to an agent."
▼
[CUCM]
│
│ SIP INVITE to Bob's phone (SEP123456789ABC)
▼
[Bob's Phone]
│
│ Ring! Ring! Ring!
│
│ INTERNAL: BiB activates
│ "I'm ready to fork audio when call is answered"
▼
[Bob]
│
│ picks up handset
│ "Thank you for calling ExpertFlow, this is Bob. How can I help?"
▼
[CUCM]
│
├─► Media path: Alice ↔ Bob established
├─► JTAPI: CallCtlTermConnTalkingEv
│ { globalCallId: "1_167890", extension: "1005" }
│
├─► BiB fork: Bob's phone sends COPY of audio to CUCM
│
└─► CUCM forwards BiB to Recording SIP Trunk
SIP INVITE:
To: <sip:record@expertflow.com>
X-Cisco-Call-ID: 167890
X-Cisco-Recording-Type: automatic
From: <sip:1005@expertflow.com>
▼
[FreeSWITCH]
│
│ record.lua executes:
│ "Recording started for GCID 167890"
│ File: /streams/167890_20240115T132345.wav
│
│ Alice: "I can't log in."
│ Bob: "Let me check your account..."
│
│ [Audio streaming to file in real-time]
═══════════════════════════════════════════════════════════════════════════════
ACT 2: JTAPI EVENTS ARE CAPTURED (1:23:45 PM)
═══════════════════════════════════════════════════════════════════════════════
[Cisco Connector — JtapiListenerService]
│
│ Receives CallCtlTermConnTalkingEv
│
│ INSERT INTO jtapi_event:
│ call_id: "1_167890"
│ global_call_id: "167890"
│ event_type: 107
│ event_name: "CallCtlTermConnTalkingEv"
│ agent_extension: "1005"
│ terminal_name: "SEP123456789ABC"
│ timestamp: "2024-01-15T13:23:45.123Z"
│ processed: false
▼
[PostgreSQL]
│
│ Row saved. Waiting for CorrelationService.
═══════════════════════════════════════════════════════════════════════════════
ACT 3: THE CALL CONTINUES (1:23:45 - 1:28:45 PM)
═══════════════════════════════════════════════════════════════════════════════
Alice: "I reset my password but it still doesn't work."
Bob: "I see the issue. Your account was locked after too many attempts.
I've unlocked it for you. Try logging in now."
Alice: "It works! Thank you so much, Bob."
Bob: "You're welcome, Alice. Have a great day!"
[FreeSWITCH]
│
│ Continuously writing audio to:
│ /streams/167890_20240115T132345.wav
│
│ File size growing: 100KB → 500KB → 1MB → 1.5MB
═══════════════════════════════════════════════════════════════════════════════
ACT 4: CALL ENDS (1:28:45 PM)
═══════════════════════════════════════════════════════════════════════════════
Alice: hangs up
[CUCM]
│
├─► Media path torn down
├─► JTAPI: ConnDisconnectedEv
├─► JTAPI: CallCtlTermConnDroppedEv (extension 1005)
│
└─► BiB BYE sent to FreeSWITCH
▼
[FreeSWITCH]
│
│ 1. Receives BYE for BiB leg
│ 2. Stops recording
│ 3. Closes WAV file
│ Final size: 1.5MB (5 minutes × 300KB/min)
│
│ 4. cx_hangup.lua executes:
│
│ POST https://mixer/api/sip-data
│ {
│ "callId": "167890",
│ "agentExtension": "1005",
│ "filePath": "/streams/167890_20240115T132345.wav",
│ "startTime": "2024-01-15T13:23:45Z",
│ "endTime": "2024-01-15T13:28:45Z",
│ "duration": 300,
│ "ani": "+15551234567",
│ "dnis": "18005550199"
│ }
│
│ 5. Response: 202 Accepted
▼
[Mixer]
│
│ Job queued: job-uuid-1234
│ status: PENDING
═══════════════════════════════════════════════════════════════════════════════
ACT 5: CORRELATION (1:29:15 PM — 30 seconds after call)
═══════════════════════════════════════════════════════════════════════════════
[Cisco Connector — CorrelationService]
│
│ Scheduled job runs:
│
│ SELECT * FROM jtapi_event WHERE processed = false;
│
│ Finds events for globalCallId = 167890:
│ Event 1: TalkingEv 13:23:45 ext=1005
│ Event 2: DisconnectEv 13:28:45 ext=8001 (Alice's side)
│ Event 3: DroppedEv 13:28:45 ext=1005
│
│ Analysis:
│ - Single extension (1005)
│ - No hold events
│ - No transfer events
│ - CallType = SIMPLE
│
│ Creates CallLeg:
│ id: 12345
│ jtapiId: "167890"
│ agentExtension: "1005"
│ startTime: 2024-01-15T13:23:45Z
│ endTime: 2024-01-15T13:28:45Z
│ duration: 300
│ callType: SIMPLE
│ status: COMPLETED
│
│ UPDATE jtapi_event SET processed = true;
▼
[call_leg table]
│
│ CallLeg saved.
═══════════════════════════════════════════════════════════════════════════════
ACT 6: ENRICHMENT (1:30:00 PM — 1 minute after call)
═══════════════════════════════════════════════════════════════════════════════
[Cisco Connector — ConversationService]
│
│ Scheduled job runs:
│
│ SELECT * FROM call_leg WHERE cim_sent = false AND status = 'COMPLETED';
│
│ Finds CallLeg 12345 (jtapiId: 167890)
│
│ Queries UCCE AW database:
│
│ SELECT tcd.ANI, tcd.DigitsDialed, tcd.WrapupData,
│ tcd.AgentSkillTargetID, a.EnterpriseName
│ FROM Termination_Call_Detail tcd
│ LEFT JOIN Agent a ON tcd.AgentSkillTargetID = a.SkillTargetID
│ WHERE tcd.PeripheralCallKey = '167890'
│
│ Result:
│ ANI: "+15551234567"
│ DigitsDialed: "18005550199"
│ WrapupData: "RESOLVED"
│ AgentSkillTargetID: 5001
│ EnterpriseName: "Bob Smith"
│
│ Enriches CallLeg:
│ serviceIdentifier: "18005550199"
│ customerIdentifier: "+15551234567"
│ agentId: "5001"
│ agentName: "Bob Smith"
│ wrapUpCode: "RESOLVED"
│
│ Builds CIM messages:
│
│ 1. CALL_LEG_STARTED:
│ { legId: 12345, jtapiId: "167890", agentExtension: "1005",
│ agentName: "Bob Smith", startTime: "...", callType: "SIMPLE",
│ customerIdentifier: "+15551234567", serviceIdentifier: "18005550199" }
│
│ 2. CALL_LEG_ENDED:
│ { legId: 12345, endTime: "...", duration: 300 }
│
│ 3. WRAPUP:
│ { legId: 12345, wrapUpCode: "RESOLVED" }
│
│ POST https://ccm/conversation-manager/activities/third-party
│ Authorization: Bearer {token}
│
│ Response: 200 OK
│
│ UPDATE call_leg SET cim_sent = true;
▼
[CCM]
│
│ Conversation created (or updated):
│ conversationId: "conv-abc-123"
│ customer: Alice (+15551234567)
│
│ Activities added:
│ 1. VOICE_CALL_STARTED (1:23 PM)
│ 2. VOICE_CALL_ENDED (1:28 PM, 5:00 duration)
│ 3. WRAPUP: RESOLVED
│
│ Supervisor can see: "Bob handled Alice's call, resolved login issue"
═══════════════════════════════════════════════════════════════════════════════
ACT 7: MIXER PROCESSING (1:32:00 PM — 3 minutes after call)
═══════════════════════════════════════════════════════════════════════════════
[Mixer Worker]
│
│ Picks up job: job-uuid-1234
│
│ Reads recording rules for tenant:
│ - Format: WAV, stereo
│ - Quality: 16kHz
│ - Encryption: AES-256
│ - Retention: 7 years
│
│ Processing:
│ 1. Reads: /streams/167890_20240115T132345.wav
│ 2. Normalizes audio levels
│ 3. Converts to stereo (agent left, customer right)
│ 4. Encrypts with tenant key
│ 5. Writes:
│ /recordings/expertflow/2024-01-15/rec-uuid-5678_mixed.wav
│ 6. Writes metadata:
│ /recordings/expertflow/2024-01-15/rec-uuid-5678.json
│ {
│ "recordingId": "rec-uuid-5678",
│ "callId": "167890",
│ "agentExtension": "1005",
│ "agentName": "Bob Smith",
│ "customerNumber": "+15551234567",
│ "serviceNumber": "18005550199",
│ "startTime": "2024-01-15T13:23:45Z",
│ "endTime": "2024-01-15T13:28:45Z",
│ "duration": 300,
│ "fileSize": 2400000,
│ "format": "WAV",
│ "channels": 2,
│ "sampleRate": 16000
│ }
│
│ Updates mixer_job:
│ status: COMPLETED
│ recordingId: "rec-uuid-5678"
═══════════════════════════════════════════════════════════════════════════════
ACT 8: RECORDING LINKED TO CONVERSATION (1:35:00 PM)
═══════════════════════════════════════════════════════════════════════════════
[Recording Link Activities — Scheduled Job]
│
│ Runs every 5 minutes.
│
│ 1. Queries CCM:
│ GET /conversation-manager/activities/voice
│ ?startTime=2024-01-15T13:00:00Z&endTime=2024-01-15T14:00:00Z
│
│ 2. Finds voice leg:
│ legId: 12345, jtapiId: "167890", agentExtension: "1005"
│
│ 3. Queries Mixer DB:
│ SELECT * FROM mixer_job
│ WHERE call_id = '167890' AND agent_extension = '1005'
│ AND status = 'COMPLETED'
│
│ Result: recordingId = "rec-uuid-5678"
│
│ 4. Builds URL:
│ https://middleware.expertflow.com/rec-uuid-5678/play
│ ?token=signed-jwt-expires-24h
│
│ 5. POST to CCM:
│ /conversation-manager/activities/third-party
│ {
│ "intent": "VOICE_RECORDING",
│ "legId": 12345,
│ "voiceRecordingUrl": "https://middleware.../rec-uuid-5678/play?token=...",
│ "duration": 300,
│ "fileSize": 2400000,
│ "recordingId": "rec-uuid-5678"
│ }
│
│ 6. Response: 200 OK
▼
[CCM — Conversation Timeline]
│
│ New activity added:
│ "Voice Recording"
│ Duration: 5:00
│ [Play Button] → opens middleware URL
│
│ Supervisor Sarah clicks play...
│ Audio streams from Middleware
│ "Thank you for calling ExpertFlow, this is Bob..."
═══════════════════════════════════════════════════════════════════════════════
ACT 9: QM CONNECTOR (Optional — Next Day)
═══════════════════════════════════════════════════════════════════════════════
[QM Connector — Scheduled Job]
│
│ Runs nightly at 2 AM.
│
│ 1. Queries Mixer DB for yesterday's recordings
│ 2. Formats data for Verint/NICE/Calabrio:
│ {
│ "callId": "167890",
│ "agentId": "5001",
│ "agentName": "Bob Smith",
│ "customerPhone": "+15551234567",
│ "callDate": "2024-01-15",
│ "duration": 300,
│ "wrapUpCode": "RESOLVED",
│ "recordingPath": "/recordings/.../rec-uuid-5678_mixed.wav"
│ }
│ 3. Pushes to QM system API
│ 4. QM system evaluates call (automated scoring)
│ 5. QM evaluation results synced back to CCM
│
│ Result: Bob's call scored 95/100. Flagged for "Excellent empathy."
═══════════════════════════════════════════════════════════════════════════════
THE END
═══════════════════════════════════════════════════════════════════════════════
Alice's problem is solved.
Bob gets a good QM score.
The recording is safely stored for 7 years.
Everything is documented in CCM.
```
---
22.9 Summary: Recording Component Responsibilities
|
Component |
One-Line Purpose |
What It Actually Does |
What It Does NOT Do |
|---|---|---|---|
|
JTAPI Connector |
Listens to CUCM |
Captures every JTAPI event into database |
Does not correlate, query, or send to CCM |
|
Cisco Connector |
Connects Cisco to CX |
Correlates events, queries UCCE/UCCX, sends CIM to CCM |
Does not record audio or process files |
|
Media Server |
Records audio |
Receives BiB stream, writes raw WAV |
Does not mix, correlate, or serve files |
|
Mixer |
Post-processes recordings |
Reads raw files, applies rules, writes mixed files |
Does not capture audio or serve to users |
|
Middleware |
Serves recordings |
Authenticates and streams audio on demand |
Does not process or create files |
|
Recording Link Activities |
Links recordings to conversations |
Matches recordings to CCM voice legs, pushes URLs |
Does not record, mix, or serve |
|
QM Connector |
Integrates with QM |
Formats and pushes data to external QM systems |
Does not record or serve recordings |