Skip to main content
Ringing lets one user call another and lets the callee answer or decline, whether the callee’s app is in the foreground, in the background or not running at all. Two SDKs do the work:
Coming from the earlier ringing flow? The Chat SDK’s calling methods (CometChat.initiateCall, acceptCall, rejectCall, endCall, CallListener) are deprecated and are replaced by what this page describes. See Migrating from legacy ringing for a method-by-method mapping.

How ringing works

The caller creates the call with the Calls SDK. The CometChat backend sends a push to the callee. The Push SDK rings the callee’s device, and when the callee answers, your call screen joins the session. The caller’s app learns that the callee is ringing and has answered from the Calls SDK, without joining the session first. What each side sees:

Prerequisites

  • Calls SDK set up and the user logged in.
  • Push notifications configured with the Push Notifications SDK 2.0. The whole push side of ringing is on the Push ringing page; this page assumes it is done.
  • Microphone and camera usage strings in Info.plist: NSMicrophoneUsageDescription and NSCameraUsageDescription. These are yours to add — a library cannot supply them. On Android the VoIP package’s own manifest already declares RECORD_AUDIO and CAMERA, and they merge into your app; the SDKs request them at runtime.
  • React Native 0.78 or later, minSdkVersion 24 on Android, and an iOS deployment target of 15.1 or later.
package.json
Some ring behaviours degrade below API 26: Android gained notification channels and full-screen intents there, so on API 24–25 the ring posts a plain notification instead of the channel-backed, full-screen one. The packages themselves build and run at 24.
Send call pushes at high FCM priority. At normal priority the call still rings, but Android refuses the Push SDK’s foreground service, so the process can be killed between the ring and the answer.
A missing NSMicrophoneUsageDescription crashes the app mid-ring on iOS. The system terminates the process the moment the SDK touches the microphone, which looks exactly like the ring expiring on its own. Declare both usage strings before testing.

Start a call

CometChatCalls.initiateCall creates the call on the backend, which rings the callee, and prepares the caller’s media session. The caller does not join yet.
initiateCall and endCall resolve to { data, error } — one of the two is always null. Check error first; there is no isSuccess or value. Others differ: connect() and login() reject on failure, and init() resolves to { success, error }.
InitiatedCall gives you sessionId, messageId (the id of the call message in the conversation), sender, receiver, receiverType, type, status, initiatedAt, conversationId and session — the session already being prepared for this call.
Always set the session type in sessionSettings, and spell it in capitals. The only accepted values are 'VOICE' and 'VIDEO'. The Calls SDK freezes the session settings when it prepares the session and its default is video, so an audio call started without sessionType: 'VOICE' opens the caller’s camera — and so does a lowercase 'voice', which matches neither value and falls through to video. The settings you pass here are the ones the caller later joins with.
initiateCall resolves while the prepare is still in flight (session.prepareStatus === 'pending'). Await session.settled or read session.prepareStatus later for the outcome. A failed prepare does not fail the call — by then the call exists on the server and the callee is already ringing — and mounting the call component still works, falling back to a normal direct join.
There is no client-side ring timeout. Both sides stop at the ring window the backend sends in the push (45 seconds by default). A caller that wants its own screen to time out can end the call as unanswered after the same window, as shown under Ring timeout.
On iOS, report the outgoing call to CallKit. Nothing does it for you: without it CallKit does not own the audio session, so a cellular call or another VoIP app can take the microphone and never give it back, and the call never reaches Recents. Incoming calls get this for free because PushKit forces a CallKit report; an outgoing call has to ask. See Reporting a call you placed yourself. The methods are safe to call on Android, where they do nothing.

Show “Ringing…” and detect the answer

The caller stays on the session that initiateCall prepared and listens to it. Do not join yet: joining only when the callee has joined keeps the caller’s media off until there is someone to talk to, and it is what makes the two events below arrive.
addEventListener returns an unsubscribe function. Call it when your screen unmounts; listeners survive the session otherwise.
A participant who has only prepared isn’t in the roster. A callee whose device has prepared the session reaches the caller as onParticipantConnected, not onParticipantJoined, and that is the only caller-side signal that the callee’s device has been reached. Participants who have actually joined are reported as usual, including to a session that is still prepared, so handle a callee who is already there.
The caller only shows “Ringing…” once the push has reached the callee’s device and its session is prepared. Until then no onParticipantConnected reaches the caller, and the screen stays on “Calling…”.

Listen for calls starting and ending

On React Native the Push SDK — not the Calls SDK — is where an incoming call announces itself and its end. It fires even when nothing of yours was running when the push arrived, because it buffers natively and drains the buffer at init().
PNCallEndEvent is { sessionId, reason?, callAction? }. callAction is the raw push action the reason was mapped from, lower-cased, and is absent — not empty — when the end was generated locally rather than by a push, so branch on its presence. Treat reason as the exhaustive one and keep a fallback for an action this SDK version has not seen. PNRingEndEvent carries the same field.
Each on… method returns an unsubscribe function, and subscribing late is safe: a call that was answered before your component mounted is held and delivered to the first subscriber.

Answer a call

The Push SDK rings and, on Answer, brings your app to the front and emits onCallAccepted. It doesn’t ask for the microphone or camera, so call CometChatPNHelper.requestCallPermissions() after sign-in. There is no separate “accept” call to make.
PNCallInfo carries sessionId, callerUid, callerName, callerAvatar, callType, receiverId, receiverType, expiresAtMs, isPeerCall, source and callAction.
Every field of a call payload arrives from Android as a string. isPeerCall is "true" / "false", not a boolean, and expiresAtMs is a string of digits. Compare against strings or coerce before use.
On iOS, a programmatic answer from a cold or background launch is refused by CallKit. The user’s own tap on the CallKit screen is the only thing that can answer a call in that state, which also means automated cold-launch answer tests need a real tap.

Join the session

Both sides join the same way: mount the Calls SDK’s call component with the token of the prepared session. Because the session was prepared while ringing, the join is near-instant.
CometChatCalls.Component takes callToken or sessionId, and no event props. It has no onCallEnded; passing one is silently ignored, so a screen that relies on it never navigates away. Subscribe to onSessionLeft / onConnectionClosed on the session, as above.
See Join Session for the component, Session Settings for layouts and controls, and Events for everything the joined session reports.

Decline a call

Declining is handled by the Push SDK: the Decline button on the notification, on its full-screen incoming-call screen and on CallKit sends endCall(sessionId, 'rejected') through a queue that survives the process dying, and sends 'busy' instead when the user is already in another call. The caller receives onCallEnded with that reason. You only write decline code when you draw your own incoming-call screen:
Without the Push SDK’s VoIP package, decline through the Calls SDK directly:
Android can decline from a fully killed app. Register the background task so the decline survives the process going away:

Cancel a call

The caller ends a call that is still ringing with endCall and the cancelled status. The callee’s ring stops and a missed call is posted there.
Disable the End button after the first tap, and ignore an onParticipantJoined that arrives after you cancelled: the cancel wins.

Ring timeout

A call that rings out sends nothing from the callee. When the ring window closes (45 seconds unless the push carries a ringTimeout) the callee’s Push SDK stops the ring and posts a missed call locally — it does not call endCall. The caller’s unanswered comes from the backend when the same window closes there. To close the caller’s screen at that moment even if the push is late, run your own timer from initiateCall:

End a call

Hang up by leaving the session. The room tells the other side, which receives onParticipantLeft and then onSessionLeft or onConnectionClosed. No status needs to be sent and the call is logged by the backend.
The other participant:
Prefer session.leave() to leaveSession(): it acts on this call’s session. Leave only the session your screen owns: after answering a second call over a live one, the first screen’s clean-up must not leave the new session.
leave() takes the Calls SDK through a re-initialisation. A connect() issued inside that window never resolves, so wait for the leave to settle before preparing the next session — which is what the Push SDK does for you when a second call is answered over a live one.

Second call while in a call

Handled by the Push SDK, the same way a phone does it. A call that arrives while the user is in another call rings over it, with the ordinary incoming-call notification and its Answer and Decline buttons. Answer leaves the live call, prepares the new one and emits onCallAccepted again for the new session — open a fresh screen for it. Decline sends busy and keeps the live call.
The SDK never decides on its own that the user is busy. A second call rings, and only the user’s own Decline sends busy — so a call arriving while another is ringing, or while the user’s own outgoing call is still ringing, rings too rather than being auto-rejected.

Group calls

To ring a group, call initiateCall with the group’s GUID and receiverType: 'group', and leave isPeerCall off. A group call can also be a session every member joins with a session id you share with them (for example in a custom message). A group call rung through the push rings every member, and letting it ring out sends nothing.

Logout

Log out of the Calls SDK with CometChatCalls.logout() when the user signs out — CometChatUIKit.logout() covers the Chat SDK only, not this — and unregister push before the session goes away:
unregister() deletes the token from the backend and needs the logged-in credential, so after logout() there is nothing left to authenticate the delete. Skipping it leaves the device receiving the departed user’s call pushes.
The Push SDK keeps the ring identity in step with the Calls SDK’s login listener: logging in writes the uid, logging out clears it. An app that logs the Chat SDK in and out without CometChatCalls.login / logout must call clearIdentity() itself, as above.

Call status values

These are the backend’s own call statuses, which arrive as the push payload’s callAction and are what PNCallInfo.callAction carries. They are a different vocabulary from the reason on onCallEnded / onRingEnded — the SDK maps one to the other, so read callAction when you need the exact wire value and reason when you want the event: rejected and busy both map to declined, so a caller that must tell them apart reads event.callAction on onCallEnded — the raw action, lower-cased. The native iOS SDK’s CallEndReason collapses them the same way; this mapping is a verbatim port of it.