CX Voice Recording + Cisco — Complete Architecture Guide

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

  1. Executive Summary

  2. System Architecture Overview

  3. Component Definitions

  4. Cisco Integration Layer

  5. Complete Normal Inbound Call Flow

  6. Complete Normal Outbound Call Flow

  7. Complete RONA Flow

  8. Transfer, Consult & Conference Flows

  9. JTAPI Event Processing Deep Dive

  10. Recording Architecture

  11. Recording Middleware & Link Activities

  12. Call Failure Scenarios

  13. Key Source Files Reference

  14. Data Models & Entity Relationships

  15. Configuration & Environment Variables

  16. Event Chain Summary

  17. Cisco-Side Configuration Reference

  18. 17.1 CUCM Configuration Requirements

  19. 17.2 FreeSWITCH Recorder Configuration

  20. 17.3 CX Unified Admin — Cisco Voice Channel

  21. 17.4 AgentDesk Environment Configurations

  22. 17.5 Cisco Elements for CX SIP Proxy

  23. 17.6 ESL Configuration for Pause/Resume Recording

  24. 17.7 Recording File Encryption/Decryption

  25. 17.8 High Availability (HA) Deployment

  26. 17.9 Generic Connector JTAPI Commands

  27. 17.10 Understanding HA vs Non-HA

  28. 17.11 Pause/Resume EFCX Call Recording

  29. 17.12 Screen Recording for Cisco

  30. 17.13 BiB Compatible Cisco Phones

  31. 17.14 Solution Prerequisites & Hardware Sizing

  32. 17.15 Port Utilisation

  33. 17.16 Eleveo Recording Middleware Reference

  34. 17.17 Quality Management (QM) Deployment

---

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

vcApi.lua, hangup_event.lua, send_message.lua, cx_hangup.lua, set_recording_name.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

ConnConnectedEv

Connected

connConnectedHandler

A connection is established

CallCtlTermConnHeldEv

Held

connHeldHandler

Agent puts call on hold

CallCtlTermConnTalkingEv

Talking

connResumedHandler

Agent resumes call

CiscoTransferStartEv

Transfer Start

ciscoTransferStartHandler

Call transfer initiated

CiscoConsultCallActiveEv

Consult Active

ciscoConsultCallHandler

Consult call created

CiscoConferenceStartEv

Conference Start

ciscoConferenceStartHandler

Conference begins

CiscoConferenceEndEv

Conference End

ciscoConferenceEndHandler

Conference ends

ConnDisconnectedEv

Disconnected

connDisconnectedHandler

Call disconnected

CallObservationEndedEv

Observation Ended

callObservationEndedEvHandler

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:

  1. Fetches unprocessed JTAPI events from the database

  2. Deduplicates events (skips duplicate CALL_CONNECTED, CALL_DISCONNECTED)

  3. Groups events by globalCallId

  4. Validates event groups (ensures equal CONNECTED and DISCONNECTED counts)

  5. Detects fake legs (first event = CALL_TRANSFERRED followed by CALL_CONNECTED)

  6. Collects events of one call (links consult legs to parent legs)

  7. Detects call type: SIMPLE, DIRECT_TRANSFER, CONSULT, CONSULT_TRANSFER, CONFERENCE

  8. Builds CallLeg objects with start/end times, reasons, and durations

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

  1. Fetches unsynced CallLegs

  2. Groups legs by callId

  3. Fetches Cisco TCD (Termination Call Detail) or CCD (Contact Call Detail) records from Cisco database

  4. Matches Cisco records to CallLegs by closest end-time

  5. Enriches CallLegs with:

  6. serviceIdentifier

  7. customerIdentifier

  8. agentName

  9. agentId

  10. wrapUps

  11. Builds CIM messages: CALL_LEG_STARTED, CALL_LEG_ENDED, WRAPUP

  12. 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 Controller.exe → SFTP

Conversational IVR

NLU bot

Media Server integrates ASR/NLU for IVR

Sentiment Analysis

Analytics engine

Fork to real-time sentiment analysis service

---

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>

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

JtapiListenerService.java

cisco-connector/src/main/java/.../service/

Main JTAPI event listener, persists events

CorrelationService.java

cisco-connector/src/main/java/.../service/

Correlates JTAPI events into CallLegs

ConversationService.java

cisco-connector/src/main/java/.../service/

Fetches Cisco TCD/CCD, builds CIM messages

CiscoRepository.java

cisco-connector/src/main/java/.../repository/

JDBC queries for Cisco DB

JtapiAddressObserver.java

cisco-connector/src/main/java/.../observer/

JTAPI address-level observer

JtapiProviderObserver.java

cisco-connector/src/main/java/.../observer/

JTAPI provider state observer

13.2 JTAPI Connector (Legacy)

File

Path

Purpose

JtapiConnector.java

jtapi-connector/src/main/java/.../observer/

Singleton JTAPI provider connection

JtapiCallObserver.java

jtapi-connector/src/main/java/.../observer/

Call-level event observer

CallService.java

jtapi-connector/src/main/java/.../services/

Call correlation and database operations

13.3 Voice Connector (ecx_generic_connector)

File

Path

Purpose

VcService.java

ecx_generic_connector/src/main/java/.../services/

Core voice connector logic

InboundController.java

ecx_generic_connector/src/main/java/.../controllers/

Receives FreeSWITCH HTTP requests

OutboundController.java

ecx_generic_connector/src/main/java/.../controllers/

Receives CCM CIM messages

13.4 FreeSWITCH Scripts

File

Path

Purpose

vcApi.lua

freeswitch-scripts/

Main inbound/rona/agent request script

set_recording_name.lua

freeswitch-scripts/

Sets recording filename based on call type

cx_hangup.lua

freeswitch-scripts/

Hangup hook processing

cx_env{DN}.lua

freeswitch-scripts/

Per-tenant environment variables

13.5 Recording Middleware

File

Path

Purpose

RecordingController.java

recording-middleware/src/main/java/.../controllers/

REST API for recording files

EfswitchRecordingService.java

recording-middleware/src/main/java/.../services/

EFSwitch backend implementation

EleveoRecordingService.java

recording-middleware/src/main/java/.../services/

Eleveo backend implementation

File

Path

Purpose

EfswitchLinkService.java

recording-link-activities/src/main/java/.../services/

Pushes EFSwitch recording links

EleveoLinkService.java

recording-link-activities/src/main/java/.../services/

Pushes Eleveo recording links

Helper.java

recording-link-activities/src/main/java/.../services/

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

CUCM_IP

Cisco Unified Communications Manager IP address

CUCM_APPLICATION_USER_NAME

JTAPI application user

CUCM_APPLICATION_USER_PASSWORD

JTAPI application user password

JTAPI_HA_MODE

Enable/disable HA with Consul locks

CONSUL_URL

Consul server URL (if HA enabled)

EFCX_FQDN

ExpertFlow CCM API base URL

EFCX_CALLBACK_URL

Callback URL for ExpertFlow

15.2 JTAPI Connector (Legacy)

Variable

Description

CUCM_IP

CUCM IP address

CUCM_APPLICATION_USER_NAME

JTAPI user

CUCM_APPLICATION_USER_PASSWORD

JTAPI password

JTAPI_HA_MODE

HA mode flag

CONSUL_URL

Consul URL

15.3 Recording Middleware

Property

Description

recording.backend

EFSWITCH or ELEVEO

efswitch.encryption-enabled

Enable AES encryption for files

efswitch.cache-path

Temporary decryption cache path

eleveo.eleveo-fqdn

Eleveo server URL

eleveo.eleveo-admin

Eleveo admin username

eleveo.eleveo-admin-password

Eleveo admin password

Property

Description

recording.backend

EFSWITCH or ELEVEO

global.retrieval-interval

Default lookback interval in days

global.middleware-api

Recording Middleware base URL

global.third-party-activities-api

CCM Third Party Activities API

global.voice-activities-api

CCM Voice Activities API

global.auth-enabled

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

ASSIGN_RESOURCE_REQUESTED

Connector → CCM

Request agent assignment

CANCEL_RESOURCE_REQUESTED

Connector → CCM

Cancel agent request (RONA)

AGENT_RESERVED

CCM → Connector

Agent reserved notification

CALL_ALERTING

Agent Desktop → CCM

Call is ringing on agent

CALL_LEG_STARTED

Agent Desktop → CCM

Agent/customer answered

CALL_LEG_ENDED

Agent Desktop → CCM

Call ended

CALL_HOLD

Cisco Connector → CCM

Call was put on hold

CALL_RESUME

Cisco Connector → CCM

Call was resumed

WRAPUP

Cisco Connector → CCM

Wrap-up codes from Cisco

VOICE_RECORDING

Link Activities → CCM

Recording URL for activity

END_CHAT

Connector → CCM

End conversation

DELIVERY_NOTIFICATION

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
  1. Create a Recording Profile in CUCM:

  2. Navigate to Device → Device Settings → Recording Profile

  3. Create a new profile and associate it with your Recording Destination Address (the DN or Route Point where recordings are sent)

  4. Create a SIP Profile:

  5. Enable the PING Option (helps in troubleshooting trunk health)

  6. Configure appropriate codec preferences

  7. Create a SIP Security Profile:

  8. Set Incoming Transport to TCP + UDP

  9. Set Outgoing Transport to TCP

  10. Enable Built-in-Bridge (BiB) on all agent phones that will be recorded:

  11. Navigate to Device → Phone → [Select Phone] → Device Configuration

  12. Set Built-in-Bridge to On

  13. Link Recording Profile to Phones:

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

5060

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:

  • 5060 TCP (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
  1. Place the record.lua script in the FreeSWITCH scripts directory:

  2. Source package: /usr/local/freeswitch/scripts/

  3. Package install: /usr/share/freeswitch/scripts/

  4. Update the IP addresses in record.lua to 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"
  1. Create the recording directories:

mkdir -p /var/vrs/recordings/cucmRecording/streams
mkdir -p /var/vrs/recordings/cucmRecording/sessions
chmod 777 -R /var/vrs/
  1. 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/:

  1. Enable 3PCC (Third Party Call Control):

<param name="enable-3pcc" value="true"/>
  1. Parse all INVITE headers (add to internal.xml):

<param name="parse-all-invite-headers" value="true"/>
  1. 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_CC channel type; for earlier versions use VOICE

17.3.2 Channel Provider Configuration (Unified Admin)
  1. Navigate to Unified Admin → Channel Management → Channel Providers

  2. Add a provider for CISCO_CC (CX4.2+) or VOICE (earlier) channel type:

  3. The provider webhook is not required for Cisco integration

  4. 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 CISCO_SERVICE_IDENTIFIER in AgentDesk ConfigMap

Bot

Select configured bot

Channel Connector

Select the connector created above

Channel Mode

HYBRID

Activity Timeout

Must be > Cisco call timeout

Routing Mode

EXTERNAL

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

ManagedByRoutingEngine

false

MaxTaskRequest

1 (per agent)

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:

  1. Incoming Dial-Peer Configuration:

  2. Set the destination IP address of the SIP Proxy in the session target ipv4 field

  3. Trusted List:

  4. Add the CX SIP Proxy server IP(s) to the trusted list for security

CVP (Cisco Voice Portal) Settings:

  1. SIP Proxy Group Assignment:

  2. Create a group containing CVP IP addresses

  3. Map this group to a Dial Number (DN) on the SIP Proxy from CUBE

  4. CVP OAMP Configuration:

  5. Configure SIP Proxy IP for specific labels: 45199, 9191, 9292

  6. These correspond to EF SIP Proxy configurations

  7. VVB (Cisco Voice Browser) Integration:

  8. On SIP Proxy, assign a group containing VVB IP addresses to the labels above

  9. CUCM Integration:

  10. In CVP OAMP, set SIP Proxy IP against CUCM Extension Numbers

  11. On SIP Proxy, assign a group containing CUCM IP addresses for call routing

17.5.2 Outbound Call Configuration

Manual Outbound Calls:

  1. CUCM SIP Trunk:

  2. Create a SIP Trunk on CUCM connecting to the SIP Proxy

  3. SIP Proxy Group & Pattern:

  4. Create a group containing CUBE IP addresses

  5. Map to specific dialing prefixes (e.g., 96***********)

Dialer Connected Calls:

  1. Cisco Dialer Registry:

  2. Set SIPServerAddress to the SIP Proxy Server IP

  3. SIP Proxy Group & Pattern:

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

recording_filename=${recording_filename}

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

Hello#<client_extension>

Agent Login

agent_state

Login#<agent_login_Id>

<password>,<extension>

Mobile Agent Login

agent_state

MobileAgentLogin#<Id>

<password>,<extension>,<phone>,<connection_mode>

Agent Logout

agent_state

Logout#<agent_login_Id>

Make Ready

agent_state

MakeReady#<agent_login_Id>

Make Not Ready

agent_state

MakeNotReady#<agent_login_Id>

Make Not Ready with Reason

agent_state

MakeNotReadyWithReason#<Id>

<reason_code>

Make Call

agent_call

MakeCall#<agent_login_Id>

<Other_Party_Extension>

Answer Call

agent_call

AnswerCall#<agent_login_Id>

<Dialog_ID>

Hold Call

agent_call

HoldCall#<agent_login_Id>

<Dialog_ID>

Retrieve Call

agent_call

RetrieveCall#<agent_login_Id>

<Dialog_ID>

Release Call

agent_call

ReleaseCall#<agent_login_Id>

<Dialog_ID>

Consult/Transfer Initiation

agent_call

ConsultCall#<agent_login_Id>

<Other_Party>,<Dialog_ID>

Complete Transfer

agent_call

TransferCall#<agent_login_Id>

<agent_extension>,<Dialog_ID>

Complete Conference

agent_call

Conferencecall#<agent_login_Id>

<agent_extension>,<Dialog_ID>

Wrap-up Code

agent_dialog

wrapup#<agent_login_Id>

<Wrapup_Reason>,<dialog_id>

17.9.2 GC Output Events (relevant to CX)

Code

Output

Description

O-001

Agent State

<agentId>#State#<state>#<reasonCode>#<name>

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

DialogState#<state>#<dialogId>#<agentId>

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 cisco-connector instance

Two cisco-connector instances (e.g., Node-A, Node-B)

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

JTAPI_HA_MODE=false (or absent)

JTAPI_HA_MODE=true + CONSUL_URL=<consul-server>

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

finesseURLForAgent: "https://finesse1"

finesseURLForAgent: "https://finesse1" + SECONDARY_FINESSE_URL: "https://finesse2"

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

IS_FINESSE_HA_ENABLED=false

IS_FINESSE_HA_ENABLED=true

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 docker run or single-node compose

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

JTAPI_HA_MODE=false

JTAPI_HA_MODE=true<br>CONSUL_URL=http://consul:8500

Needs Consul server; both connector instances point to same Consul

Finesse (AgentDesk)

IS_FINESSE_HA_ENABLED=false<br>finesseURLForAgent=https://finesse1

IS_FINESSE_HA_ENABLED=true<br>SECONDARY_FINESSE_URL=https://finesse2

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

docker-compose up on one VM

docker swarm init + docker stack deploy

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

uuid_record <uuid> stop

Stops writing to the recording file

Resume

uuid_record <uuid> start <file>

Resumes writing to the same or new file

Mask via broadcast

uuid_broadcast <uuid> pause_recording

Sends internal pause signal

Resume via broadcast

uuid_broadcast <uuid> resume_recording

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:

  1. Detects when the agent is on an active call (via Finesse event API)

  2. Signals the local Controller.exe to start/stop screen capture

  3. Displays recording status to the agent

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

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:

  1. Navigate to Device → Phone

  2. Find the agent's phone

  3. Set Built In Bridge = On

  4. Set Recording Option = Automatic or Selective

  5. Set Recording Profile to the recording server SIP trunk

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

awdb (Admin Workstation) for UCCE; db_cra for UCCX

Authentication

SQL Server Authentication (username/password)

Permissions

Read access to Termination_Call_Detail, AgentConnectionDetail, ConsultLegDetail, Resource, DialingList

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

expertflow/cx-*

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 mergeHoldEndedLegs()

Eleveo splits recordings on conference

Middleware merges using mergeConferenceLegs()

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
  1. EF CX deployed with Cisco voice channel configured

  2. Recording backend configured (EFSwitch or Eleveo)

  3. CISCO_CC channels created in Unified Admin for both Inbound and Outbound

  4. Outbound service identifier set as UCCE_OB_SERVICE_IDENTIFIER in values.yaml

  5. Routing Mode = External

  6. Channel Model = HYBRID

  7. Keycloak user vrs created with all realm-management roles

17.17.3 QM-Connector Environment Variables

Variable

Description

Example

RECORDING_SERVER_FQDN

Recording Server IP

https://192.168.1.126

EFCX_FQDN

CX FQDN

https://efcx-qm.expertflow.com

CALL_BACK_URL

Webhook for conversation creation

http://webhook.site/...

UCCE_ENGINE

UCCE DB engine

sqlserver

UCCE_IP

UCCE IP

192.168.1.72

UCCE_PORT

UCCE DB port

1433

UCCE_DATABASE

UCCE awdb name

uc126_awdb

UCCE_USERNAME

UCCE DB user

sa

UCCE_PASSWORD

UCCE DB password

Expertfllow464

UCCE_OB_SERVICE_IDENTIFIER

Outbound service identifier

8899

KEYCLOAK_REALM_NAME

Keycloak realm

expertflow

KEYCLOAK_CLIENT_ID

Keycloak client

cim

KEYCLOAK_CLIENT_SECRET

Keycloak client secret

ef61df80-...

KEYCLOAK_USERNAME

Keycloak service user

vrs

KEYCLOAK_PASSWORD

Keycloak service password

vrs

KEYCLOAK_AGENT_ROLE_NAME

Agent role name

agent

KEYCLOAK_AGENT_ROLE_ID

Agent role UUID

1903735d-...

DB_NAME

VRS database name

vrs

DB_USER

VRS DB user

sa

DB_PASSWORD

VRS DB password

Expertflow123

DB_ENGINE

VRS DB engine

sqlserver

DB_HOST

VRS DB host

192.168.1.126

DB_PORT

VRS DB port

1433

DB_DRIVER

JDBC driver

com.microsoft.sqlserver.jdbc.SQLServerDriver

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

C:\Users\ExpertFlow\Desktop\projects\cisco-connector

Cisco JTAPI integration, event correlation, CIM generation

jtapi-connector

C:\Users\ExpertFlow\Desktop\projects\jtapi-connector

Legacy JTAPI event listener

recording-middleware

C:\Users\ExpertFlow\Desktop\projects\recording-middleware

Recording file serving (EFSwitch/Eleveo)

recording-link-activities

C:\Users\ExpertFlow\Desktop\projects\recording-link-activities

Pushes recording links to CCM

ecx_generic_connector

C:\Users\ExpertFlow\Desktop\projects\ecx_generic_connector

Voice Connector (FreeSWITCH ↔ CCM bridge)

freeswitch-scripts

C:\Users\ExpertFlow\Desktop\projects\freeswitch-scripts

FreeSWITCH Lua scripts for call flows

conversation-manager

C:\Users\ExpertFlow\Desktop\projects\conversation-manager

CCM (Conversation Manager)

media-routing-engine

C:\Users\ExpertFlow\Desktop\projects\media-routing-engine

Task/queue/agent routing

front-end / unified-agent

C:\Users\ExpertFlow\Desktop\projects\...

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

LOGOUT

Agent not logged in

NOT_READY

NOT_READY

Agent logged in but not accepting calls

NOT_READY

READY

Agent available for inbound calls

READY

RESERVED

Agent selected for inbound call, ringing

RESERVED

RESERVED_OUTBOUND

Agent selected for outbound preview call

RESERVED

TALKING

Agent on active call

BUSY

WORK

After-call work (wrap-up)

NOT_READY

WORK_READY

After-call work, then auto-ready

NOT_READY → READY

HOLD

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

LOGOUT

NOT_READY

Agent logs in

AgentDesk → Finesse → CX

NOT_READY

READY

Agent clicks "Make Ready"

AgentDesk → Finesse → CX

READY

NOT_READY

Agent clicks "Make Not Ready"

AgentDesk → Finesse → CX

READY

RESERVED

Routing Engine assigns inbound task

Routing Engine → CCM → AgentDesk

READY

RESERVED

Routing Engine assigns outbound preview

Routing Engine → CCM → AgentDesk

RESERVED

BUSY

Agent answers call

sip-wrapper.js → Angular → CX

RESERVED

NOT_READY

RONA (no answer)

FreeSWITCH → Voice Connector → CCM → Routing Engine

BUSY

NOT_READY

Call ends + wrap-up required

sip-wrapper.js → Angular → CX

BUSY

READY

Call ends + auto-ready enabled

sip-wrapper.js → Angular → CX

BUSY

BUSY

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

CREATED

Conversation initialized, no active legs

ASSIGN_RESOURCE_REQUESTED received

First CALL_LEG_STARTED

ACTIVE

At least one leg is active

CALL_LEG_STARTED

All legs ended + wrap-up complete

CLOSED

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

INITIATED

ASSIGN_RESOURCE_REQUESTED

INITIATED

ALERTING

CALL_ALERTING

CALL_ALERTING

ALERTING

ACTIVE

CALL_LEG_STARTED

CALL_LEG_STARTED

ACTIVE

ACTIVE

CALL_HOLD

CALL_HOLD

ACTIVE

ACTIVE

CALL_RESUME

CALL_RESUME

ACTIVE

ENDED

CALL_LEG_ENDED

CALL_LEG_ENDED

ALERTING

ENDED

CALL_LEG_ENDED (RONA)

CALL_LEG_ENDED

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

QUEUED

Task in queue, waiting for agent

Queue timeout may trigger overflow/IVR

RESERVED

Agent selected, waiting for answer

RONA timeout → task revoked

ACTIVE

Agent answered, call in progress

Call timer, max call duration

COMPLETED

Call ended, work complete

REVOKED

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

QUEUED

Task created

Add to queue

QUEUED

RESERVED

Agent found

Send AGENT_RESERVED to CCM

RESERVED

ACTIVE

Agent answers

Update agent MRD to BUSY

RESERVED

ENDED

RONA / Cancel

Revoke task, set agent NOT_READY (RONA only)

ACTIVE

ENDED

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

onInvite (inbound)

ALERTING

ALERTING

["ANSWER"]

onTrying (outbound)

INITIATING

INITIATING

[]

onProgress (outbound)

INITIATED

INITIATED

[]

onAccept (outbound)

ACTIVE

ACTIVE

["HOLD", "TRANSFER", "CONFERENCE", "DROP"]

stateChange → Established

ACTIVE

ACTIVE

["HOLD", "TRANSFER", "CONFERENCE", "DROP"]

hold_call (re-INVITE)

HELD

HELD

["RETRIEVE", "TRANSFER", "DROP"]

retrieve_call (re-INVITE)

ACTIVE

ACTIVE

["HOLD", "TRANSFER", "CONFERENCE", "DROP"]

onBye / bye()

DROPPED

DROPPED

[]

onCancel (RONA)

DROPPED

DROPPED

[]

18.5.3 SIP.js Session States

Internally, SIP.js tracks session state:

Initial → Establishing → Established → Terminated

SIP.js State

Meaning

UI State

Initial

Session created, no INVITE sent

INITIATING

Establishing

INVITE sent, waiting for response

INITIATING / INITIATED

Established

200 OK received, media flowing

ACTIVE

Terminating

BYE sent, cleanup in progress

DROPPED

Terminated

Session closed

DROPPED

18.5.4 INVITE Header Processing

When an inbound INVITE arrives, sip-wrapper.js extracts these headers:

Header

Dialog Property

Example

X-Call-Id

dialog.id

"abc-123-call-id"

X-Customernumber

dialog.customerNumber

"+18005550199"

X-Queue

dialog.queueName

"Sales"

X-Calltype

dialog.callType

"INBOUND"

X-Destination-Number

dialog.serviceIdentifier

"8001"

X-Call-Variable0..9

dialog.callVariables

Campaign data

X-Call-Dropped-Custom-Reason

callEndReason

"NORMAL_CLEARING"

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

uuid

FreeSWITCH (auto)

Unique call identifier

call_direction

Dialplan

INBOUND / OUTBOUND

destination_number

INVITE

Dialed number (DN)

caller_id_number

INVITE

Calling number (ANI)

sip_h_X-Queue

vcApi.lua

Queue name for routing

sip_h_X-CallType

vcApi.lua

INBOUND / OUT / CONSULT

recording_filename

set_recording_name.lua

Output filename

record_path

Dialplan

Output directory

outside_call

Dialplan

Flag for external calls

api_hangup_hook

Dialplan

Post-hangup script

session_in_hangup_hook

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"
  }
}
{
  "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

cx_env{DN}.lua per tenant

Recording path

Per-tenant

/app/files/wav/{tenant}/{date}/

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:

  1. Receive forked media streams from CUCM via SIP trunk (BiB recordings)

  2. Write raw audio to disk as individual call leg files

  3. Generate call metadata (timestamps, call IDs, file paths)

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

record.lua, mixapi.lua

vcApi.lua, cx_hangup.lua

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

RECEIVED

Job received, not yet queued

QUEUED

Job in RabbitMQ queue

PROCESSING

Worker actively processing

RULES_PASSED

Recording rules evaluated, will record

SKIPPED

Recording rules say do not record

MIXING

Audio mixing in progress

SPLICING

Segment splicing in progress

COMPLETED

Final file written to storage

FAILED

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)

---

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

GET /mixer/jobs?status=FAILED

Check worker logs, retry job

Audio only on one channel

Wrong mix format

Check rule parameters

Set format: "stereo"

Hold music in recording

MOH not excluded

Check splicing config

Add exclude_hold: true to rules

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

Files not appearing

Storage mount issue

df -h on mixer container

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

awdb database with TCD records

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 (db_cra)

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

awdb (SQL Server, separate AW)

db_cra (Informix, embedded)

Call detail table

Termination_Call_Detail (TCD)

ContactCallDetail (CCD)

Agent detail table

Agent

Resource

Call correlation

PeripheralCallKey

contactid

Wrap-up data

WrapupData in TCD

callwrapupdata in ACD

Historical data

HDS (separate historical store)

Same db_cra

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

ANI

Calling number

customerIdentifier

DigitsDialed

Dialed number

serviceIdentifier

WrapupData

Wrap-up code

WRAPUP CIM

AgentSkillTargetID

Agent ID

agentId

PeripheralCallKey

JTAPI call ID

Matches jtapi_event.jtapiId

CallTerminatedDateTimeUTC

End time

Matches leg endTime

EnterpriseName

Agent name

agentName

InstrumentPortNumber

Extension

agentExtension

TalkTime

Talk duration seconds

Duration validation

HoldTime

Hold duration seconds

Recording segment calculation

PeripheralCallType

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

contactid

JTAPI call ID

Matches jtapiId

originatordn

Calling number

customerIdentifier

callednumber

Dialed number

serviceIdentifier

callwrapupdata

Wrap-up code

WRAPUP CIM

resourceid

Agent ID

agentId

resourcename

Agent name

agentName

extension

Extension

agentExtension

connecttime

Call connect time

Leg startTime validation

disconnecttime

Call disconnect time

Leg endTime

customvariable1-10

Custom call variables

Screen-pop data

sessionid

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

Select Skill Group

Determines queue name

DN → Skill Group → CX Queue

Queue Call

Call enters CX Routing Engine

ICM queues internally; CX also queues

Route to Agent

Agent reserved in CX

ICM selects agent → CUCM connects → CX notified

Wrap-up

Wrap-up code collected

ICM stores → UCCE DB → Cisco Connector → CCM

Offer Callback

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

awdb on AW (SQL Server)

db_cra on UCCX (Informix)

Call detail table

Termination_Call_Detail

ContactCallDetail

Agent table

Agent

Resource

Correlation key

PeripheralCallKey

contactid

Wrap-up field

WrapupData

callwrapupdata

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

callId, agentExtension, timestamp, globalCallId

CallCtlTermConnRingingEv

Agent phone ringing

Pre-allocate leg (optional)

callId, agentExtension, calledNumber

ConnDisconnectedEv

Any party hangs up

Mark CallLeg end

callId, timestamp, disconnectReason

CallCtlTermConnDroppedEv

Agent connection dropped

Finalize leg

callId, agentExtension, timestamp

CallCtlTermConnHeldEv

Agent presses hold

Record hold segment

callId, timestamp

CallCtlConferencedEv

Conference created

Detect multi-party

callId, conferenceController

CallCtlCallInvalidEv

Call aborted unexpectedly

Error logging

callId, errorCode

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

id

152347

Auto-increment

call_id

1_167890

CUCM call manager ID + global call ID

global_call_id

167890

Unique per call

event_type

107

JTAPI numeric event type

event_name

CallCtlTermConnTalkingEv

Human-readable event name

agent_extension

1005

Agent's DN

terminal_name

SEP123456789ABC

Phone MAC

timestamp

2024-01-15T09:23:45.123Z

Event time

cisco_cause

CAUSE_NORMAL_CLEARING

Disconnect reason

cisco_feature_reason

REASON_TRANSFER

Why state changed

call_state

ACTIVE

Call state at event time

connection_count

2

Number of connections

processed

false

Correlation flag

created_at

2024-01-15T09:23:45.200Z

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

LOGOUT

LOGOUT

Agent logs out

NOT_READY

NOT_READY

Agent goes not-ready

READY

READY

Agent ready for calls

RESERVED

RESERVED

Call offered to agent

TALKING

BUSY

Agent on active call

WORK

WORK

Post-call wrap-up

WORK_READY

WORKREADY

Wrap-up auto-complete

HOLD

BUSY (sub-state)

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 Termination_Call_Detail or UCCX ContactCallDetail

EFCX internal database

Correlation key

globalCallId (from CUCM) + PeripheralCallKey/contactid

conversationId + channelId

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?

globalCallId

CUCM needs a unique call ID

CUCM cluster may have multiple CallManager nodes

PeripheralCallKey

UCCE PG needs its own ID

PG may see multiple peripherals; ID scoped to PG

contactid

UCCX uses different schema

UCCX and UCCE are separate products with different databases

call_leg.id

CX needs to track individual legs

One call may have multiple agents (transfer, conference)

recordingId

Mixer needs UUID for files

File systems need unique names; GCID may collide across days

conversationId

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

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

globalCallId / PeripheralCallKey

conversationId / channelId

Enrichment source

UCCE AW / UCCX db_cra

EFCX internal database

Wrap-up source

UCCE WrapupData / UCCX callwrapupdata

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: /recordings/{tenant}/{date}/

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