Managing Call Hold

The Vonage Client SDK does not have a dedicated "hold" API. Instead, hold is implemented as mute + earmuff combined:

  • mute(callId) — stops sending your audio to the remote party
  • enableEarmuff(callId) — stops receiving audio from the remote party

To unhold, reverse both: call unmute(callId) and disableEarmuff(callId).

On iOS and Android, the system call UI (CallKit / Telecom framework) provides a native hold button. The platform guides below show how to wire that button to the SDK's mute + earmuff calls.

This guide will walk you through the process of implementing call hold functionality in your iOS and Android applications using our VoIP SDK.

Call hold is not currently supported on the Web SDK.

Telecom Integration

Before diving into this tutorial, make sure you have integrated the Android Telecom framework in your application.

You can follow the official guide here or explore our Android Voice Sample app available here.

Support Holding via Connection Implementation

Once you have built your implementation of the Connection class, you will need to add the CAPABILITY_SUPPORT_HOLD to the connection capabilities and override the onHold() and onUnhold() methods.

Below is an example in Kotlin:

class CallConnection(val callId: CallId) : Connection() {
    private lateinit var clientManager: VoiceClientManager
    var isOnHold = false
        private set

    init {
        connectionProperties = connectionProperties or PROPERTY_SELF_MANAGED
        connectionCapabilities = connectionCapabilities or CAPABILITY_SUPPORT_HOLD /** add other capabilities */
        audioModeIsVoip = true
    }

    // ... Other methods ...

    override fun onHold() {
        if (!isOnHold) {
            clientManager.holdCall(this)
        }
    }

    override fun onUnhold() {
        if (isOnHold) {
            clientManager.unholdCall(this)
        }
    }

    fun toggleHoldState() {
        isOnHold = !isOnHold
        if (isOnHold) setOnHold() else setActive()
    }
}

Interaction with the Voice SDK

To implement the holding state for a call, enable mute and earmuff simultaneously.

The following code inside the VoiceClientManager class demonstrates this:

class VoiceClientManager(private val context: Context) {
    private lateinit var client : VoiceClient

    fun holdCall(call: CallConnection){
        call.apply{
            client.mute(callId){ error ->
                error?.let {
                    println("Error muting when holding call with id: $callId")
                } ?: run {
                    client.enableEarmuff(callId){ error ->
                        error?.let {
                            println("Error enabling earmuff when holding with id: $callId")
                        } ?: run {
                            println("Call $callId successfully put on hold")
                            toggleHoldState()
                        }
                    }
                }
            }
        }
    }

    fun unholdCall(call: CallConnection){
        call.apply{
            client.unmute(callId){ error ->
                error?.let {
                    println("Error unmuting when unholding call with id: $callId")
                } ?: run {
                    client.disableEarmuff(callId){ error ->
                        error?.let {
                            println("Error disabling earmuff unholding call with id: $callId")
                        } ?: run {
                            println("Call $callId successfully removed from hold")
                            toggleHoldState()
                        }
                    }
                }
            }
        }
    }
}

Feel free to adapt the code snippets to fit your specific implementation and requirements.

Callkit Integration

Before diving into this tutorial, make sure you have integrated the Callkit framework in your application.

You can follow the official guide, or explore our iOS Voice Sample app available here.

Support Holding via CXUpdate

Once you have built your CallController class, holding reference to a CXProvider and a CXCallController you will need to enable the supportsHolding flag on the CXCallUpdate both for outgoing and incoming calls.

Below is an example in Swift:

import CallKit

class CallController {
    let callProvider: CXProvider
    let cxController = CXCallController()

    // ... Other methods ...

    func reportOutgoingCall(callId: String){
        self.callProvider.reportOutgoingCall(with: callId, connectedAt: Date.now)
        let update = CXCallUpdate()
        /** add other capabilities */
        update.supportsHolding = true
        self.callProvider.reportCall(with: callId, updated: update)
    }

    func reportIncomingCall(callId: String){
        let update = CXCallUpdate()
        /** add other capabilities */
        update.supportsHolding = true
        self.callProvider.reportNewIncomingCall(with: callId, update: update)
    }
}

Interaction with the Voice SDK

Once you have built your CXProviderDelegate implementation, listen to the CXSetHeldCallAction to put calls on hold using the Voice SDK.

To implement the holding state for a call, enable mute and earmuff simultaneously.

The following code inside the CallController class demonstrates this:

import VonageClientSDKVoice

class CallController: NSObject {
    let callProvider: CXProvider
    let cxController = CXCallController()
    let client: VGVoiceClient
    var activeCalls: [UUID: String] = [:] // Maps CallKit UUID to Vonage callId

    // ... initialization and other methods ...
}

extension CallController: CXProviderDelegate {

    func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
        guard let callId = self.activeCalls[action.callUUID] else {
            action.fail()
            return
        }
        if action.isOnHold {
            self.client.mute(callId) { error in
                if let error = error {
                    print("Error muting when holding call: \(error)")
                    return
                }
                self.client.enableEarmuff(callId) { error in
                    if let error = error {
                        print("Error enabling earmuff when holding call: \(error)")
                        return
                    }
                    print("Call \(callId) successfully put on hold")
                }
            }
        } else {
            self.client.unmute(callId) { error in
                if let error = error {
                    print("Error unmuting when unholding call: \(error)")
                    return
                }
                self.client.disableEarmuff(callId) { error in
                    if let error = error {
                        print("Error disabling earmuff when unholding call: \(error)")
                        return
                    }
                    print("Call \(callId) successfully removed from hold")
                }
            }
        }
        // CallKit requires fulfilling the action synchronously
        action.fulfill()
    }
}

Feel free to adapt the code snippets to fit your specific implementation and requirements.