Skip to main content
Ringing lets one user call another and lets the callee answer or decline. On the web, an incoming call rings in every open tab of your app where the callee is logged in. Two SDKs do the work: The web has no VoIP push. A tab that is closed does not ring, and your app plays the ringtone, shows the incoming-call screen and stops ringing when the ring is over. A callee on Android or iOS rings through the Push Notifications SDK.
Web and mobile users can call each other when the mobile app also rings with the Calls SDK 5.1 and the Push Notifications SDK 2.0. Calls between an app on legacy ringing (the Chat SDK’s calling methods) and an app on this flow ring once Connect legacy ringing is on in the CometChat dashboard; see Migrating from Legacy Ringing.
Coming from the earlier ringing flow? The Chat SDK’s calling methods (CometChat.initiateCall, acceptCall, rejectCall, endCall, startCall) are superseded by the Calls SDK and are no longer used. CometChat.addCallListener is the exception on the web: it is still how an incoming call reaches a tab. 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 delivers the call message to the callee’s open tabs through the Chat SDK, and to the callee’s phones by push. The callee’s tab rings and prepares the session while it rings; when the callee answers, it joins. 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:
The Calls SDK does not report why a ring ended. endCall records the status on the server, which sends it as a Chat SDK call message: a decline, busy or no answer reaches the caller as CallListener.onOutgoingCallRejected (call.getStatus() is rejected, busy or unanswered), and a cancel reaches the callee as onIncomingCallCancelled. A 1:1 decline from a callee whose device had prepared the session also closes the caller’s prepared session (onConnectionClosed), which carries no reason. A tab can still miss a message (it was asleep in the background, or reconnecting), and if the caller’s own prepare failed, the caller is not in the room and hears neither the ring nor the answer. So while the call rings, also read its status from the server every 3 seconds as a backstop, and keep the ring timer.

Prerequisites

  • Calls SDK set up and the user logged in with CometChatCalls.login or loginWithAuthToken. initiateCall, connect and endCall need a logged-in Calls SDK user.
  • The Chat SDK (@cometchat/chat-sdk-javascript 4.2.1 or later) initialized and logged in as the same user, so that incoming calls, declines and cancels reach the tab. See Chat SDK setup.
  • HTTPS in production, for camera and microphone access.

Start a call

CometChatCalls.initiateCall creates the call on the backend, which rings the callee, and starts preparing the caller’s session. The caller does not join yet.
initiateCall never rejects. It resolves with { data, error }:
  • data is the InitiatedCall: sessionId, messageId (the id of the call message in the conversation), conversationId, sender, receiver, receiverType, type, status, initiatedAt (in seconds) and session, the CallSession being prepared.
  • error is set when no call was created: USER_NOT_LOGGED_IN, VALIDATION_ERROR (no receiver), SESSION_ALREADY_ACTIVE (the user is joining or in another call; leave it first), BAD_RESPONSE, or the failure of the request.
initiateCall resolves as soon as the call exists, before the caller’s session has finished preparing: call.session.prepareStatus is "pending" at first. A failed prepare does not fail initiateCall, because the callee is already ringing; it shows on call.session.prepareStatus ("failed") and call.session.error. After a failed prepare call.session.join(container) still performs a direct join, unless no call token came back with the call (it then resolves with VALIDATION_ERROR: generate a token and use joinSession, as joinOutgoing below does).
Always set the session type in sessionSettings, and isPeerCall: true for a 1:1 call. The Calls SDK freezes the session settings when it prepares the session, and session.join(container) takes no settings: the ones you pass here are the ones the caller joins with. The default session type is video, so an audio call started without sessionType: "VOICE" opens the caller’s camera. Without isPeerCall, a hang-up only removes one side from the room and the other side stays in it alone.

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 camera and microphone off until there is someone to talk to. While the session is only prepared, the SDK still reports the callee’s device reaching the room and the callee joining it. Listeners registered with CometChatCalls.addEventListener (or call.session.addEventListener) are global: they work before the prepare has finished and stay registered across sessions until you call the function they return. Register them right after initiateCall resolves, and check that the event belongs to the call on screen.
call.session.join(container) called while the prepare is still running waits for it. See Join the session. These events come from the room, so they only arrive while the caller’s session is in it. If the caller’s own prepare failed (call.session.prepareStatus === "failed"), neither arrives: the status poll below then reports the answer as ongoing.

Detect a decline, busy or no answer

A decline, busy or no answer reaches the caller as onOutgoingCallRejected on the Chat SDK’s CallListener, with the reason in call.getStatus() (see Receive a call). As a backstop for a message the tab missed, and for an answer the caller cannot hear because its own prepare failed, also read the call’s status from the server every 3 seconds from the moment initiateCall resolves until the call is answered, cancelled or times out (watchOutgoingCall above starts and stops the poll). Start at once, not only after “Ringing…”: a callee who declines before their device prepared the session never sends onParticipantConnected. The status is the call’s entry in Call Logs, read with the logged-in user’s auth token:
fetchCallStatus calls a CometChat REST endpoint directly: the Calls SDK has no public method that returns a call’s status, so this endpoint is not part of the SDK’s API. The sample builds the URL for the default host, {appId}.call-{region}.cometchat.io; if your app initializes the Calls SDK with a custom host, build the URL from that host instead.
Never end a call because the status could not be read: null means “keep ringing”. The Chat SDK’s CallListener.onOutgoingCallRejected carries the same outcome (rejected, busy or unanswered); end the screen on whichever arrives first (see Receive a call for filtering it).

Receive a call

On the web an incoming call arrives as a call message on the Chat SDK’s real-time connection. Register a CallListener once, after the Chat SDK login, and remove it at logout.
Your app does the ringing itself: play a ringtone, show the incoming-call screen, and, if the tab is in the background, show a browser notification (request the Notification permission on a user gesture, for example when the user places their first call). Browsers may block audio that starts without a user gesture on the page. endCall(sessionId, "busy") while the user is in another call leaves that other call untouched: endCall only tears down the live session when it is the call being ended.

Prepare the ringing call

As soon as the call rings, prepare its session with CometChatCalls.connect. Preparing is signalling only: no camera or microphone is opened and the caller does not see the callee in the call, but it is what turns the caller’s screen to “Ringing…” and makes the answer near-instant. In a 1:1 call (isPeerCall: true) it also lets the caller’s cancel reach this tab as onConnectionClosed, because the caller leaving a prepared peer session ends the room.
connect(sessionId, sessionSettings) resolves with the CallSession once the prepare has settled: session.prepareStatus is "prepared", or "cancelled" when leave() was called meanwhile or a later connect for another session replaced it. Calling it again for the same session while it is preparing or prepared returns the same prepare. It rejects when the session could not be prepared, with one of two shapes:
  • { errorCode, errorDescription } before the prepare starts: ERROR_SESSION_ID_MISSING, ERROR_USER_NOT_LOGGED_IN, or a failed call token request.
  • { name, message } (PrepareSessionException) for everything after: a token verification, connection or conference failure, or SESSION_ALREADY_ACTIVE while another session is joined. The same error is left on session.error.
The settings you pass to connect are frozen: they are the ones the callee joins with. A prepared session that nobody joins within 5 minutes is torn down: onConnectionClosed fires and getActiveSession() returns null, while session.prepareStatus stays "prepared" (it is "failed" with PREPARE_TIMEOUT only if the prepare had not finished connecting by then). onSessionTimedOut does not fire for it.
Leaving a prepared 1:1 session ends the room for everyone in it. With isPeerCall: true, tearing down a prepared session that is in the room, whether by leave(), endCall, a connect or initiateCall for another session, or the 5-minute expiry, asks the server to end the room, not only to remove this tab. That is how a cancel or decline reaches the other side, and it does no harm once the ring is over.It is a problem when the call was answered in another tab or on another device: that tab’s prepared session is still in the room of the live call. Do not call leave() on it. That only delays the problem: the 5-minute expiry, or the next connect or initiateCall in that tab, tears the session down the same way. The Calls SDK has no local-only leave for a prepared 1:1 session, so there is no client-side way around this.

Stop ringing on the callee

Stop ringing when any of these happens for the ringing call:
  • onIncomingCallCancelled arrives on the Chat SDK listener (the caller cancelled or gave up).
  • onOutgoingCallRejected arrives for a call you did not place (you declined it in another tab or on another device).
  • onConnectionClosed fires while the call is still ringing and this tab had prepared it (in a 1:1 call: the caller cancelled, or the call ended).
  • Your own ring timer runs out.
  • Another of your tabs tells you it answered or declined the call (see Several tabs and devices).
  • The call’s status is no longer initiated. Poll it every 3 seconds while ringing, with the fetchCallStatus function above, as a backstop for a message this tab missed (it was asleep in the background, or reconnecting): onConnectionClosed only fires when the room is destroyed.

Answer a call

There is no separate “accept” call. Answering is joining the session the tab prepared; if the prepare failed, join directly.
The browser asks for the camera and microphone when the session is joined, not when it is prepared.

Join the session

Both sides join the same way: the callee when the user answers, the caller from onParticipantJoined. session.join(container) mounts the call UI into container and takes no settings. session.join resolves with { data, error } like joinSession. CometChatCalls.joinSession(token, settings, container) refuses to run while a session is preparing or prepared (SESSION_ALREADY_PREPARED): join the prepared one with session.join(container), or cancel it with session.leave() first.
CometChatCalls.getActiveSession() returns the SDK’s one CallSession while a session is preparing, prepared, being joined or joined: from the moment connect is called, or from the moment initiateCall has created the call. It returns null otherwise, including after a failed or cancelled prepare and after the 5-minute expiry. Always check it for null (getActiveSession()?.leave()), and compare session.sessionId with the call you mean: a later connect for another session re-points the same object, so an old reference describes the newer session. sessionId is set only for a session from connect or initiateCall; after a direct joinSession it is null.
See Join Session for the container, Session Settings for layouts and controls, and Events for everything the joined session reports.

Decline a call

Decline a 1:1 call with endCall and the rejected status. When this tab had prepared the session, endCall first starts tearing it down (in a 1:1 call that asks the server to end the room), then tells the server the status. onConnectionClosed follows when the room closes.
Use "busy" instead of "rejected" when the user is already in another call. endCall(sessionId, status) never rejects. It resolves with { data: undefined, error: null } once the server has the new status, or with error set: USER_NOT_LOGGED_IN, VALIDATION_ERROR (no session ID), or the failure of the request. The status defaults to "rejected".

Cancel a call

The caller ends a call that is still ringing with endCall and the cancelled status. The SDK first starts tearing the caller’s prepared session down, then tells the server; onConnectionClosed follows when the room closes (the sample below has already removed its own listener). The callee’s ring stops.
Disable the cancel button after the first click, and ignore an onParticipantJoined that arrives after you cancelled: the cancel wins. If the user cancels before initiateCall has resolved, cancel the call when it resolves, or the callee rings for a call nobody is waiting on:

Ring timeout

Nothing in the web SDKs ends a ring on a timer, so run a timer on both sides. On the caller, start it when initiateCall resolves and end the call as unanswered when it runs out; watchOutgoingCall above does this. Use 45 seconds, the default ring window of a callee on a phone. A callee on a phone stops ringing at its Push SDK’s ring window, but if the callee’s device is never reached, nothing else ends the caller’s ringing until the prepared session expires after 5 minutes. A group call the caller stops waiting on is withdrawn as cancelled instead: unanswered is a 1:1 outcome. On the callee, stop ringing after 60 seconds, a little after the caller would have given up, with stopRinging(sessionId, "over"). Do not send a status: the caller ends the call.

End a call

Hang up by leaving the session with session.leave() or CometChatCalls.leaveSession(). The other side receives onParticipantLeft, and in a peer call (isPeerCall: true) the session ends for both. No status needs to be sent and the call is logged by the backend.
onConnectionClosed fires for a joined call that ends and for a prepared session that is torn down, so check which screen is showing before acting on it.

Several tabs and devices

Every open tab where the callee is logged in receives the call and rings. Answering or declining in one tab does not stop the others by itself:
  • Tell your other tabs, for example with a BroadcastChannel (tellOtherTabs in the samples above). A tab that hears another tab answered calls stopRinging(sessionId, "elsewhere"), which does not call leave() on its prepared session, because releasing a prepared 1:1 session ends the room. The session still expires after 5 minutes, with the same effect (see the warning under Prepare the ringing call). A tab that hears another tab declined calls stopRinging(sessionId, "over").
  • A 1:1 decline also reaches your other tabs as onOutgoingCallRejected, as shown in Receive a call.
  • The status poll is the backstop: a call answered on another of the user’s devices reads ongoing, which stops a 1:1 ring as "elsewhere".
  • While one tab is in a call, keep your other tabs from ringing (tell them over the same channel), and have the tab in the call decline a 1:1 call with busy, as isBusy() in Receive a call does.
A tab that is already joined to a call cannot prepare another one: connect rejects with SESSION_ALREADY_ACTIVE and initiateCall returns SESSION_ALREADY_ACTIVE.

Group calls

To ring a group, call initiateCall with the group’s GUID as the receiver and receiverType: "group", and leave isPeerCall off so the call outlives any one member leaving. Every member’s open tabs (and phones) ring. When a group call rings, declining it or letting it ring out sends nothing: the call carries on for the other members, and the decline is local to that tab. You can also start a group call without ringing: share a session ID (for example in a custom message) and have each member join it with joinSession.

Logout

End or cancel any call of your own before logging out: endCall needs a logged-in user. Then remove the CallListener, stop any ring, and log out of both SDKs (CometChatCalls.logout() and CometChat.logout()).

Call status values