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:NSMicrophoneUsageDescriptionandNSCameraUsageDescription. These are yours to add — a library cannot supply them. On Android the VoIP package’s own manifest already declaresRECORD_AUDIOandCAMERA, and they merge into your app; the SDKs request them at runtime. - React Native 0.78 or later,
minSdkVersion 24on 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.
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.
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 thatinitiateCall 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.
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 atinit().
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 emitsonCallAccepted. 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.
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.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 sendsendCall(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:
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 withendCall and the cancelled status. The callee’s ring stops and a missed call is posted there.
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 aringTimeout) 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 receivesonParticipantLeft and then onSessionLeft or onConnectionClosed. No status needs to be sent and the call is logged by the backend.
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.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 emitsonCallAccepted 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, callinitiateCall 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 withCometChatCalls.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’scallAction
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.
Related
- Push ringing: dependencies,
PNConfiguration, the Firebase and PushKit setup, your own incoming-call screen, readiness checks and what happens when the app is killed. - Migrating from legacy ringing.
- Events, Actions, Call Logs.