This document explains how to make support entry points (queues and channels) available in your app, and how to drive the full chat lifecycle: starting, resuming, sending messages and attachments, indicating typing, redacting sensitive data, escalating to human agents, and ending the session.
Before you begin
Set up the SDK. For more information, see Headless mobile SDK for Android: Get started.
Select queues and channels
A queue usually represents a support entry point, such as "Billing", "Technical Support", or "VIP Support".
Each queue can offer one or more of the following channels:
Chat
Instant call (voice)
Scheduled call
Email
External deflection links (links to your self-service pages or other channels)
Queues and channel availability are configured by administrators in the Contact Center AI Platform Admin Portal.
How it looks in your app
In your app, a typical flow is:
You add a support entry point in your app (for example, a Help tab or a Contact Us button).
When the user taps it, you fetch the queue menu using the SDK and render your own menu UI.
When the user chooses a queue, you inspect its available channels.
Your app shows those channels to the user (for example, Chat now, Call now, Schedule a call, Email us, Visit Help Center).
When the user selects a channel, you call the corresponding SDK API.
Fetch queue menus from the SDK
The SDK provides queueMenuService to fetch queue menus. The get method
returns a QueueMenuResult containing the list of queues (List<QueueMenu>)
and an isDirectAccess flag. You can either fetch the full menu tree or use a
Direct Access Key to jump directly to a specific queue:
import com.ccaiplatform.ccaikit.CCAI
import com.ccaiplatform.ccaikit.models.response.queuemenu.QueueMenuResult
import com.ccaiplatform.ccaikit.models.response.queuemenu.QueueMenu
// Fetch the full queue menu tree
val menuResult: QueueMenuResult? = CCAI.queueMenuService?.get()
// Or fetch a specific queue using a Direct Access Key
val menuResult: QueueMenuResult? = CCAI.queueMenuService?.get(key = "billing_vip")
The optional key parameter lets you bypass the full queue menu and jump the
user directly to a specific queue. In the CCAI Platform platform, this
is achieved with a direct access point (DAP).
A DAP can be configured by administrators in the CCAI Platform portal in the following way:
Go to Settings > Queue > Select any queue from the menu structure.
From the Access Point section, click + Create direct access point.
Enter the key in the text form.
Click Save.
QueueMenu data structure
Each QueueMenu contains:
| Property | Type | Description |
|---|---|---|
id |
Int | The unique identifier for this queue. |
name |
String? | The display name (for example, "Billing Support"). |
children |
List<QueueMenu> | Sub-menus (empty list if this is a leaf node). |
channels |
Channels? | Available channels for this queue. |
hidden |
Boolean | Whether this queue should be hidden from consumers (administrator configured). |
position |
Int? | Server-defined display position - preserves administrator portal ordering. |
settings |
List<QueueMenuSetting>? | Queue-level settings configured in the administrator portal. |
redirectionExtra |
QueueMenuRedirection? | Automatic redirection metadata when configured (see Redirect and deflection). |
deflectionFrom |
List<QueueMenuDeflection>? | The queues that deflected to this one. The platform returns this queue in their place, so a non-empty list explains a redirect that already happened (see Deflection) |
The Channels data class has these properties:
| Property | Type | Description |
|---|---|---|
chat |
ChatChannel? | Chat channel configuration (null if chat isn't available for this queue). |
voiceCall |
VoiceCallChannel? | Voice call channel configuration (null if voice isn't available). Contains flags such as scheduleEnabled and recording preferences. |
email |
EmailChannel? | Email channel configuration (null if email isn't available). Access the address using channels.email?.email. |
externalDeflectionLink |
ExternalDeflectionLink? | External deflection link configuration (null if not available). Access the URL using the structured type's properties. |
VoiceCallChannel fields (capability and deflection metadata)
// VoiceCallChannel.kt (simplified)
data class VoiceCallChannel(
val instantEnabled: Boolean, // Live voice call allowed now
val preSessionSmartAction: Boolean, // Per-channel pre-session smart action flag
val scheduleEnabled: Boolean, // Scheduled calls supported
val recordingOption: RecordingOption?, // ALWAYS / NEVER / ASK_USER
val voicemailReason: String?,
val phoneNumber: String?,
val scheduleDeflectionType: String?,
val deflected: Boolean?,
val deflectedReason: String?
)
Implementation example
fun showAvailableChannels(queueMenu: QueueMenu) {
val channels = queueMenu.channels
val voice = channels?.voiceCall
// Show only what the platform enabled, and hide everything else so
// nothing carries over from a previously selected queue
chatButton.isVisible = channels?.chat?.enabled == true
callButton.isVisible = voice != null && voice.instantEnabled && !voice.deflected
scheduleButton.isVisible = voice?.scheduleEnabled == true
emailButton.isVisible = channels?.email?.enabled == true
// External deflection links: the enabled entries that have a URL
val links = channels?.externalDeflectionLink
?.takeIf { it.enabled }
?.links.orEmpty()
.filter { it.enabled && it.url != null }
helpLinksButton.isVisible = links.isNotEmpty()
}
Fetching wait times
You can also retrieve estimated wait times for a specific queue:
// Fetch wait time for a specific queue
val waitTime = CCAI.queueMenuService?.getWaitTimes(menuId = queueMenu.id)
// waitTime contains:
// - chat: estimated wait time for chat (in seconds)
// - voiceCall: estimated wait time for voice call (in seconds)
Single-channel queues
If a queue offers only one channel (for example, email only), you can:
Skip the channel selection step and go straight to the appropriate UI.
Or you can still show the available channel explicitly if that makes more sense in your UX.
The SDK's configuration and metadata tells you which channels are allowed; your app decides how to show them.
Redirect and deflection
Admins can configure a queue to redirect users instead of offering channels. When QueueMenu.redirectionExtra is set, act on it as soon as the user selects the queue:
optionsays what to do: PHONE, MESSAGE, URL, VOICEMAIL, QUEUE, SIP, or CONTACT.datacarries the value, such as the number to dial, the message to show, or the URL to open.
QueueMenu.deflectionFrom is different. When a queue deflects to another queue, the platform returns the destination queue in place of the one the user selected and lists the source queues in deflectionFrom. It explains a redirect that already happened; it isn't a set of options to offer.
After-hours and over-capacity deflection is reported per channel. For how to handle it, see Headless Mobile SDK for Android: Smart Actions, Attachments, and Deflection.
Channels
In the Headless Mobile SDK, your CCAI Platform administrator portal acts as the source of truth for your support operations. Administrators configure which support channels (such as chat, voice, email, or helpful links) are available for different support queues (such as "Billing" or "Tech Support").
The SDK's job is to securely fetch this configuration and hand it to your app. Your app's job is to read that configuration and draw the appropriate user interface.
Inspecting available channels
What the SDK provides: A real-time snapshot of which support channels are active and available for the specific queue the user selected, respecting your portal's business hours and capacity limits.
What you build: The UI menu (buttons, lists, or cards) that displays these available options to the user.
Implementation example: The following Kotlin code takes the queue the user
just tapped and inspects the Channels data class to see which channels are
enabled. It then decides which buttons to show on the screen.
// Assuming 'selectedMenu' is a QueueMenu returned by the SDK
fun setupChannelMenu(selectedMenu: QueueMenu) {
val channels = selectedMenu.channels
if (channels.chat != null) {
showChatNowButton()
}
if (channels.voiceCall != null) {
showCallNowButton()
}
if (channels.email != null) {
showEmailUsButton()
}
if (channels.externalDeflectionLink != null) {
showHelpCenterLinks()
}
}
The following sections cover chat in detail. For voice, screen share, scheduled call, email, and external deflection links, see Headless Mobile SDK for Android: Voice, Screen Share, Scheduled Call, and Email
Chat service API
The chatService manages the full chat lifecycle. The core methods you use most
frequently are:
| Method | Description |
|---|---|
start |
Starts a new chat session with a ChatRequest. |
sendMessage |
Sends a message (text, image, video, or form event). |
typing |
Updates the typing indicator. |
endChat |
Ends the current chat session. |
resumeChat |
Resumes a previously established chat session. |
escalateToHumanAgent |
Transfers from a virtual agent to a human agent. |
checkStatus |
Refreshes the current chat status from the server. |
ChatRequest and ChatResponse data structures
The ChatRequest data class contains the configuration for starting a new chat:
data class ChatRequest(
val chat: Chat? = null, // The session configuration (always pass one)
val isScreenShareable: Boolean? = null, // Optional: enable screen share for this session
val isVerifiable: Boolean? = null // Optional: allow verification smart actions
)
data class Chat(
val menuId: Int, // Required: queue menu ID
val lang: String? = "en", // Optional: language code override (defaults to "en")
val ticketId: String? = null, // Optional: associate with existing ticket
val greeting: String? = null, // Optional: custom greeting message
val customData: CustomData? = null, // Optional: structured custom data
val displayOrderInAdapter: List<String>? = null,
val directAccessKeyId: Int? = null, // Optional: from QueueMenuResult (Direct Access Point)
val dapRequestId: String? = null // Optional: from QueueMenuResult (Direct Access Point)
)
After you start or resume a chat, the SDK delivers its ChatResponse on the
chatReceived Flow (start() and resumeChat() return nothing). The
properties you reference most in this guide include:
id- the unique chat session identifier.status- the current chat status (queued, active, ended, etc.).agent/virtualAgent- the assigned human or virtual agent.postSessionRequired- whether a post-session virtual agent is configured for the session. Surveys and CSAT are signaled bysurveyEnabledandcsatSetting.redactionEnabled- whether automatic PII redaction is active for this session.
Start a chat session
To start a chat session, use the chatService with a ChatRequest specifying the queue menu ID.
Minimal example
import android.util.Log
import androidx.lifecycle.lifecycleScope
import com.ccaiplatform.ccaichat.chatService
import com.ccaiplatform.ccaichat.model.Chat
import com.ccaiplatform.ccaichat.model.ChatRequest
import com.ccaiplatform.ccaikit.CCAI
import com.ccaiplatform.ccaiscreenshare.screenShareService
import kotlinx.coroutines.launch
// If screen share is initialized, pass isScreenShareable for agent visibility
val chatRequest = ChatRequest(
chat = Chat(menuId = 123),
isScreenShareable = CCAI.screenShareService != null
)
startOrResumeChat(chatRequest)
// Resumes an earlier chat if there is one; otherwise starts a new one
fun startOrResumeChat(chatRequest: ChatRequest) {
lifecycleScope.launch {
val chatService = CCAI.chatService ?: return@launch
try {
// Pre-flight check: resume an earlier chat instead of starting a second one
val existing = chatService.getLastChatInProgress()
if (existing != null) {
chatService.resumeChat(existing)
return@launch
}
chatService.start(chatRequest)
// Chat started - navigate to your custom chat screen
} catch (e: Exception) {
Log.e("CCAI", "Failed to start chat: ${e.message}")
}
}
}
If your app doesn't include the CCAIScreenShare module, remove the
screenShareService import and the isScreenShareable argument.
With explicit language
val chatRequest = ChatRequest(
chat = Chat(menuId = 123, lang = "en")
)
startOrResumeChat(chatRequest) // Defined in the Minimal example
Resume a chat session
Resume a previously established chat session using a ChatResponse object:
lifecycleScope.launch {
try {
CCAI.chatService?.resumeChat(chat)
} catch (e: Exception) {
Log.e("CCAI", "Failed to resume chat: ${e.message}")
}
}
Chat queue management
You can put the current chat back in the queue (its status becomes queued) or check for existing in-progress chats:
// Enqueue the current chat (automatically calls checkStatus() after enqueuing)
lifecycleScope.launch {
try {
CCAI.chatService?.enqueueCurrentChat()
Log.d("CCAI", "Chat enqueued successfully")
} catch (e: Exception) {
Log.e("CCAI", "Failed to enqueue chat: ${e.message}")
}
}
// Check for an existing in-progress chat
lifecycleScope.launch {
try {
val chat = CCAI.chatService?.getLastChatInProgress()
if (chat != null) {
Log.d("CCAI", "Found ongoing chat: ${chat.id}")
} else {
Log.d("CCAI", "No ongoing chat found")
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to get last chat: ${e.message}")
}
}
// Retrieve previous chat history with pagination
lifecycleScope.launch {
try {
val previousChats = CCAI.chatService?.getPreviousChats(currentPage = 1)
if (previousChats != null) {
Log.d("CCAI", "Found ${previousChats.messages.size} previous messages")
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to get previous chats: ${e.message}")
}
}
Send messages
The sendMessage method supports multiple message types through the
OutgoingMessageContent sealed class:
sealed class OutgoingMessageContent {
data class Text(val content: String) : OutgoingMessageContent()
data class Photos(
val images: List<UploadableImage>,
val smartAction: SmartAction? = null
) : OutgoingMessageContent()
data class Videos(
val video: UploadableVideo,
val smartAction: SmartAction? = null
) : OutgoingMessageContent()
data class FormComplete(
val type: String? = null,
val signature: String? = null,
val data: FormCompleteEventData
) : OutgoingMessageContent()
data class ScreenShare(
val event: String,
val detail: ScreenShareResponse? = null
) : OutgoingMessageContent()
}
The UploadableImage and UploadableVideo types (in com.ccaiplatform.ccaikit.models.smartaction) wrap media for upload:
data class UploadableImage(
val data: ByteArray, // The binary data of the image
val mimeType: String, // For example, "image/jpeg" or "image/png"
val uri: Uri? = null // Optional URI for display and local file reference
)
data class UploadableVideo(
val uri: Uri, // The file URI of the video
val mimeType: String // For example, "video/mp4"
)
Photos takes a list of images; Videos takes a single video. Avoid holding large media assets in memory while the user is waiting in queue or while an upload is pending.
Usage examples
// Send text message
lifecycleScope.launch {
try {
CCAI.chatService?.sendMessage(OutgoingMessageContent.Text("Hello world"))
} catch (e: Exception) {
Log.e("CCAI", "Failed to send message: ${e.message}")
}
}
// Send image message (read the file off the main thread)
lifecycleScope.launch {
try {
val imageData = withContext(Dispatchers.IO) {
requireNotNull(contentResolver.openInputStream(selectedImageUri)).use { it.readBytes() }
}
val images = listOf(
UploadableImage(data = imageData, mimeType = "image/jpeg", uri = selectedImageUri)
)
CCAI.chatService?.sendMessage(
OutgoingMessageContent.Photos(
images = images,
smartAction = null
)
)
} catch (e: Exception) {
Log.e("CCAI", "Failed to send image: ${e.message}")
}
}
// Send video message (one video per message)
val video = UploadableVideo(uri = selectedVideoUri, mimeType = "video/mp4")
lifecycleScope.launch {
try {
CCAI.chatService?.sendMessage(
OutgoingMessageContent.Videos(
video = video,
smartAction = null
)
)
} catch (e: Exception) {
Log.e("CCAI", "Failed to send video: ${e.message}")
}
}
// Send form completion event
// Note: status is a nullable String: "success", "error", or "cancelled".
val formCompleteData = FormCompleteEventData(
smartActionId = 123,
status = "success",
timestamp = "2024-01-01T12:00:00Z",
details = FormCompleteEventDetails(message = "Form submitted successfully")
)
lifecycleScope.launch {
try {
CCAI.chatService?.sendMessage(
OutgoingMessageContent.FormComplete(data = formCompleteData)
)
Log.d("CCAI", "Form completion event sent")
} catch (e: Exception) {
Log.e("CCAI", "Failed to send form completion: ${e.message}")
}
}
The ScreenShare message type is used to send screen sharing events and
responses to the server. Available screen share events include:
screenShareRequestedFromAgent- agent requested screen sharingscreenShareRequestedFromEndUser- end user requested screen sharingscreenShareCodeGenerated- screen share code was generatedscreenShareFailed- screen sharing failedscreenShareStarted- screen sharing session startedscreenShareEnded- screen sharing session ended
Typing indicators and message previews
Update the typing status and send message previews while the user is typing:
// Update typing indicator
CCAI.chatService?.typing(true) // Start typing
CCAI.chatService?.typing(false) // Stop typing
// Send message preview
lifecycleScope.launch {
try {
CCAI.chatService?.sendMessagePreview("Typing...")
} catch (e: Exception) {
Log.e("CCAI", "Failed to send message preview: ${e.message}")
}
}
// Example: Send preview as user types
fun onTextChanged(text: String) {
lifecycleScope.launch {
try {
CCAI.chatService?.sendMessagePreview(text)
} catch (e: Exception) {
Log.e("CCAI", "Failed to send preview: ${e.message}")
}
}
}
Message redaction
Automatic redaction of sensitive information (PII, credit card numbers, etc.) is supported for plain text chat messages and message previews.
Redaction configuration:
Controlled by the
redactionEnabledproperty inChatResponse.Configured in the portal under Settings > Chat Settings > Automatic Redaction.
When enabled, sensitive data is automatically replaced with placeholders before being sent to agents.
// Redaction is automatically handled, use sendMessage as usual
lifecycleScope.launch {
try {
CCAI.chatService?.sendMessage(
OutgoingMessageContent.Text("My phone number is 4155553423")
)
} catch (e: Exception) {
Log.e("CCAI", "Failed to send message: ${e.message}")
}
}
End and escalate chats
// End the current chat session
lifecycleScope.launch {
try {
CCAI.chatService?.endChat()
Log.d("CCAI", "Chat ended successfully")
} catch (e: Exception) {
Log.e("CCAI", "Failed to end chat: ${e.message}")
}
}
// Escalate to a human agent
lifecycleScope.launch {
try {
CCAI.chatService?.escalateToHumanAgent()
Log.d("CCAI", "Chat escalated to human agent")
} catch (e: Exception) {
Log.e("CCAI", "Failed to escalate chat: ${e.message}")
}
}
// Record the deflection channel the user chose for an existing escalation
// (the escalation must already exist; start one with escalateToHumanAgent())
val escalationOption = ChatEscalationOption(
escalationId = 123,
deflectionChannel = DeflectionChannel.Email
)
lifecycleScope.launch {
try {
CCAI.chatService?.updateEscalation(option = escalationOption)
Log.d("CCAI", "Escalation updated successfully")
} catch (e: Exception) {
Log.e("CCAI", "Failed to update escalation: ${e.message}")
}
}
// Check the current chat status
lifecycleScope.launch {
try {
CCAI.chatService?.checkStatus()
Log.d("CCAI", "Status check completed successfully")
} catch (e: Exception) {
Log.e("CCAI", "Failed to check status: ${e.message}")
}
}
Chat events
Send chat events to the server for tracking user interactions.
ChatEvent.payload is a non-nullable Map<String, String> — both keys and
values must be strings.
val event = ChatEvent(
name = "form_received",
payload = mapOf("name" to "contact_form")
)
lifecycleScope.launch {
try {
CCAI.chatService?.sendEvent(event)
Log.d("CCAI", "Event sent successfully")
} catch (e: Exception) {
Log.e("CCAI", "Failed to send event: ${e.message}")
}
}
Common event use cases include form interactions ("form_received", "form_clicked"), smart action events ("smart_action_received", "smart_action_clicked"), and custom user interactions for analytics.
Module-specific build considerations
Consider the following for a module-specific build.
Minimum Sdk level
The CCAIChatRed and CCAICallRed media-layer modules require min Sdk = 23
(Android 6.0). If your app declares a lower minSdkVersion, Gradle will fail to
merge manifests. Raise your min Sdk to at least 23 in app/build.gradle.kts:
android {
defaultConfig {
minSdk = 23
}
}