Inbound Voice — Alerting, CCM, Accept, and Topic Subscription

Inbound Voice: Alerting, CCM, Accept, and Topic Subscription

This page is an add-on to Custom Agent Desk Developer Guide 2.0 section 4 (Push Mode Tasks). Voice ringing is SIP-driven. After the agent accepts, CCM starts the reserved voice task, and Agent Manager then drives the conversation UI through the same topicSubscription / onTopicData path used for chat.

Voice does not use a reserved taskRequest to ring the agent. The offer is FS JS newInboundCall (ALERTING). taskRequest with taskState.name = STARTED arrives after accept, and that is when the desk subscribes to the conversation topic.

End-to-end flow

SIP INVITE arrives at registered agent extension
        ↓
FS JS event: newInboundCall  (dialog.state = ALERTING)
        ↓
Custom desk shows ringing UI (Accept / Reject)
        ↓
POST CCM /message/receive   intent = CALL_ALERTING   (dialog payload)
        ↓
Agent clicks Accept  →  FS JS command: answerCall
        ↓
FS JS event: dialogState  (dialog.state = ACTIVE)
        ↓
POST CCM /message/receive   intent = CALL_LEG_STARTED   (accepted dialog payload)
        ↓
CCM / Routing Engine changes the voice task state to STARTED
        ↓
Socket.IO event: taskRequest  (taskState.name = STARTED)
        ↓
Custom desk emits: topicSubscription
        ↓
Agent Manager emits: onTopicData
        ↓
Custom desk updates conversation UI from onTopicData
        ↓
Call ends → FS JS dialogState (DROPPED)
        ↓
POST CCM /message/receive   intent = CALL_LEG_ENDED

CCM API

All three voice CIM intents use the same CCM endpoint:

POST {CCM_URL}/message/receive

Content-Type: application/json

The desk switches intent on the same CIM envelope: CALL_ALERTING while ringing, CALL_LEG_STARTED after accept, CALL_LEG_ENDED when the dialog is dropped.

Step 1 — Ringing / alerting (push to CCM)

When newInboundCall arrives with dialog.state = ALERTING, show the ringing card. At the same time POST a CIM message to CCM so CCM knows the agent is being alerted:

POST {CCM_URL}/message/receive

Set header.intent to CALL_ALERTING. Put the SIP dialog on body.dialog (callId = dialog.id, customer number, service identifier, and leg). Do not wait for a chat-style reserved taskRequest to ring the agent. For the voice MRD (CX VOICE), a reserved taskRequest is ignored for ringing.

Sample payloadintent: CALL_ALERTING

JSON
{
    "id": "50c79b2e-72c5-4882-ad10-ee6814d381bb",
    "header": {
        "channelData": {
            "channelCustomerIdentifier": "1002",
            "serviceIdentifier": "2555",
            "additionalAttributes": []
        },
        "customer": {
            "_id": "6a86a304d32c74310ca0b33f",
            "firstName": "1002",
            "phoneNumber": [],
            "voice": [
                "1002"
            ],
            "isAnonymous": true,
            "timezoneSource": "UNRESOLVED",
            "timezoneConfidence": "UNRESOLVED_TIMEZONE",
            "timezone": null,
            "region": "",
            "__v": 0
        },
        "language": {},
        "timestamp": 1787208634000,
        "securityInfo": {},
        "stamps": [],
        "intent": "CALL_ALERTING",
        "entities": {},
        "sender": {
            "id": "d07d020b-e7e8-4685-9b7e-3e8ba1ef8e16",
            "senderName": "regression1",
            "type": "AGENT",
            "additionalDetail": {}
        }
    },
    "body": {
        "type": "VOICE",
        "markdownText": null,
        "reasonCode": "INBOUND",
        "leg": "hbiq7ptr2ljpm2fjf0oc:2001:1002:1787208634000",
        "callId": "hbiq7ptr2ljpm2fjf0oc",
        "dialog": {
            "id": "hbiq7ptr2ljpm2fjf0oc",
            "ani": "1002",
            "customerNumber": "1002",
            "associatedDialogUri": null,
            "callbackNumber": null,
            "outboundClassification": null,
            "scheduledCallbackInfo": null,
            "isCallEnded": 0,
            "eventType": "PUT",
            "callType": "OTHER_IN",
            "queueName": "voice regression",
            "queueType": "NAME",
            "dialedNumber": "2555",
            "dnis": "2555",
            "serviceIdentifier": "2555",
            "secondaryId": null,
            "state": "ALERTING",
            "isCallAlreadyActive": false,
            "wrapUpReason": null,
            "wrapUpItems": null,
            "callEndReason": null,
            "fromAddress": "1002",
            "callVariables": {
                "CallVariable": []
            },
            "participants": [
                {
                    "actions": {
                        "action": [
                            "ANSWER"
                        ]
                    },
                    "mediaAddress": "2001",
                    "mediaAddressType": "SIP.js/0.21.2-CTI/Expertflow",
                    "startTime": null,
                    "alertingTime": "2026-08-20T06:50:34.985Z",
                    "state": "ALERTING",
                    "stateCause": null,
                    "stateChangeTime": "2026-08-20T06:50:34.985Z",
                    "mute": false
                }
            ],
            "mediaType": "audio",
            "channelType": "VOICE",
            "primaryDN": null
        }
    }
}

Step 2 — Accept (dialog to CCM, task → STARTED)

Send FS JS answerCall with the alerting dialogId (Developer Guide section 5.4). When the callback returns dialogState with dialog.state = ACTIVE, POST the accepted dialog to CCM again:

POST {CCM_URL}/message/receive

Set header.intent to CALL_LEG_STARTED, using the same dialog / leg / callId fields as alerting. CCM uses this accept message to move the voice task from reserved/alerting to STARTED.

Sample payloadintent: CALL_LEG_STARTED

JSON
{
    "id": "3ac3a79f-b603-410b-849b-7e70c1937e5e",
    "header": {
        "channelData": {
            "channelCustomerIdentifier": "1002",
            "serviceIdentifier": "2555",
            "additionalAttributes": []
        },
        "customer": {
            "_id": "6a86a304d32c74310ca0b33f",
            "firstName": "1002",
            "phoneNumber": [],
            "voice": [
                "1002"
            ],
            "isAnonymous": true,
            "timezoneSource": "UNRESOLVED",
            "timezoneConfidence": "UNRESOLVED_TIMEZONE",
            "timezone": null,
            "region": "",
            "__v": 0
        },
        "language": {},
        "timestamp": 1787208639000,
        "securityInfo": {},
        "stamps": [],
        "intent": "CALL_LEG_STARTED",
        "entities": {},
        "sender": {
            "id": "d07d020b-e7e8-4685-9b7e-3e8ba1ef8e16",
            "senderName": "regression1",
            "type": "AGENT",
            "additionalDetail": {}
        }
    },
    "body": {
        "type": "VOICE",
        "markdownText": null,
        "reasonCode": "INBOUND",
        "leg": "hbiq7ptr2ljpm2fjf0oc:2001:1002:1787208634000",
        "callId": "hbiq7ptr2ljpm2fjf0oc",
        "dialog": {
            "id": "hbiq7ptr2ljpm2fjf0oc",
            "ani": "1002",
            "customerNumber": "1002",
            "associatedDialogUri": null,
            "callbackNumber": null,
            "outboundClassification": null,
            "scheduledCallbackInfo": null,
            "isCallEnded": 0,
            "eventType": "PUT",
            "callType": "OTHER_IN",
            "queueName": "voice regression",
            "queueType": "NAME",
            "dialedNumber": "2555",
            "dnis": "2555",
            "serviceIdentifier": "2555",
            "secondaryId": null,
            "state": "ACTIVE",
            "isCallAlreadyActive": false,
            "wrapUpReason": null,
            "wrapUpItems": null,
            "callEndReason": null,
            "fromAddress": "1002",
            "callVariables": {
                "CallVariable": []
            },
            "participants": [
                {
                    "actions": {
                        "action": [
                            "ANSWER"
                        ]
                    },
                    "mediaAddress": "2001",
                    "mediaAddressType": "SIP.js/0.21.2-CTI/Expertflow",
                    "startTime": "2026-08-20T06:50:39.998Z",
                    "alertingTime": "2026-08-20T06:50:34.985Z",
                    "state": "ACTIVE",
                    "stateCause": null,
                    "stateChangeTime": "2026-08-20T06:50:39.998Z",
                    "mute": false
                }
            ],
            "mediaType": "audio",
            "channelType": "VOICE",
            "primaryDN": null
        }
    }
}

Step 3 — Topic subscription (conversation UI)

After the task is started, Agent Manager emits taskRequest with taskState.name = "STARTED". The custom desk must then emit topicSubscription (same payload shape as chat accept in Developer Guide section 4.b):

JavaScript
socket.emit("topicSubscription", {
  topicParticipant: {
    type: "AGENT",
    participant: currentAgent,
    conversationId: taskRequest.conversationId,
    role: taskRequest.taskDirection === "CONSULT" ? "ASSISTANT" : "PRIMARY",
    state: "SUBSCRIBED"
  },
  agentId: currentAgent.id,
  conversationId: taskRequest.conversationId,
  roomInfo: taskRequest.roomInfo,
  taskId: taskRequest.taskId
});

Agent Manager responds with onTopicData (topicData, conversationId, roomInfo, taskId). Use that payload to open / refresh the conversation view (customer, channel sessions, gadgets, history). Call-control buttons stay on SIP dialogState (Developer Guide section 5.19); the conversation panel comes from onTopicData.

If taskRequest.taskDirection is consult, subscribe as ASSISTANT. If it is silent_monitor, subscribe as SILENT_MONITOR. Otherwise subscribe as PRIMARY.

Step 4 — Call ended (push to CCM)

When the SIP dialog is DROPPED, POST CIM CALL_LEG_ENDED to the same CCM API. Include header.conversationId, body.reasonCode (for a normal inbound hangup: DIALOG_ENDED), body.endingReason (from dialog.callEndReason, e.g. AGENT-HANDLED), and conversationId as a channelData.additionalAttributes entry.

POST {CCM_URL}/message/receive

Sample payloadintent: CALL_LEG_ENDED

JSON
{
    "id": "6ec50995-6354-4608-9626-a9e7a1139402",
    "header": {
        "channelData": {
            "channelCustomerIdentifier": "1002",
            "serviceIdentifier": "2555",
            "additionalAttributes": [
                {
                    "key": "conversationId",
                    "type": "String2000",
                    "value": "6a86a3bbfe8ac52c88a8ac79"
                }
            ]
        },
        "customer": {
            "_id": "6a86a304d32c74310ca0b33f",
            "firstName": "1002",
            "phoneNumber": [],
            "voice": [
                "1002"
            ],
            "isAnonymous": true,
            "timezoneSource": "UNRESOLVED",
            "timezoneConfidence": "UNRESOLVED_TIMEZONE",
            "timezone": null,
            "region": "",
            "__v": 0
        },
        "language": {},
        "timestamp": 1787208645000,
        "securityInfo": {},
        "stamps": [],
        "intent": "CALL_LEG_ENDED",
        "entities": {},
        "sender": {
            "id": "d07d020b-e7e8-4685-9b7e-3e8ba1ef8e16",
            "senderName": "regression1",
            "type": "AGENT",
            "additionalDetail": {}
        },
        "conversationId": "6a86a3bbfe8ac52c88a8ac79"
    },
    "body": {
        "type": "VOICE",
        "markdownText": null,
        "reasonCode": "DIALOG_ENDED",
        "leg": "hbiq7ptr2ljpm2fjf0oc:2001:1002:1787208634000",
        "callId": "hbiq7ptr2ljpm2fjf0oc",
        "dialog": {
            "id": "hbiq7ptr2ljpm2fjf0oc",
            "ani": "1002",
            "customerNumber": "1002",
            "associatedDialogUri": null,
            "callbackNumber": null,
            "outboundClassification": null,
            "scheduledCallbackInfo": null,
            "isCallEnded": 1,
            "eventType": "PUT",
            "callType": "OTHER_IN",
            "queueName": "voice regression",
            "queueType": "NAME",
            "dialedNumber": "2555",
            "dnis": "2555",
            "serviceIdentifier": "2555",
            "secondaryId": null,
            "state": "DROPPED",
            "isCallAlreadyActive": false,
            "wrapUpReason": null,
            "wrapUpItems": null,
            "callEndReason": "AGENT-HANDLED",
            "fromAddress": "1002",
            "callVariables": {
                "CallVariable": []
            },
            "participants": [
                {
                    "actions": {
                        "action": [
                            "ANSWER"
                        ]
                    },
                    "mediaAddress": "2001",
                    "mediaAddressType": "SIP.js/0.21.2-CTI/Expertflow",
                    "startTime": "2026-08-20T06:50:39.998Z",
                    "alertingTime": "2026-08-20T06:50:34.985Z",
                    "state": "DROPPED",
                    "stateCause": null,
                    "stateChangeTime": "2026-08-20T06:50:45.022Z",
                    "mute": false
                }
            ],
            "mediaType": "audio",
            "channelType": "VOICE",
            "primaryDN": null
        },
        "endingReason": "AGENT-HANDLED"
    }
}

Summary

Stage

What the desk receives

What the desk sends

Result

Call offered

FS JS newInboundCall (ALERTING)

CIM CALL_ALERTING → CCM

Ringing UI; CCM records alerting

Agent accepts

FS JS dialogState (ACTIVE)

FS JS answerCall, then CIM CALL_LEG_STARTED → CCM

Voice task state becomes STARTED

Conversation UI

Socket taskRequest (STARTED), then onTopicData

Socket topicSubscription

Customer / conversation view opens

Call ended

FS JS dialogState (DROPPED)

CIM CALL_LEG_ENDED → CCM

Voice leg closed; endingReason e.g. AGENT-HANDLED

Reference implementation in Agent Desk: sip.service.ts (handleCallAlertingEvent, acceptCallOnSip, handleCallActiveEvent, handleCallDroppedEvent, ccmChannelSessionApi) and socket.service.ts (taskRequest when taskState.name == startedtopicSubscriptiononTopicData).