The Stream Video API offers a ringing flow that simulates traditional phone calls. This feature lets developers implement call experiences where users can receive incoming calls with ringtones and choose to accept or decline.
You can also show missed call notifications if someone doesn't answer the call on time or is busy in another call.
To start the ringing flow, we need to set the ring flag to true and provide the list of members we want to call. It is important to note that the caller should also be included in the list of members.
Stream API allows reusing call ids (for example, to implement recurring meetings), but this is not something we advise for ring calls. You should create a unique id for every ring call (for example by using UUIDs).
You can set the video flag to true or false (for audio calls), which will control the type of incoming call screen client-side SDKs will display.
// Make sure to use a unique id when creating ring callsconst call = client.video.call("default", crypto.randomUUID());await call.getOrCreate({ ring: true, video: true, // Set it to `false` for audio calls. This is used to decide what type of incoming call screen is displayed client-side data: { created_by_id: "myself", members: [{ user_id: "myself" }, { user_id: "my friend" }], },});
from getstream.models import CallRequest, MemberRequestimport uuid# Make sure to use a unique id when creating ring callsclient.video.call("default", uuid.uuid4()).get_or_create( data=CallRequest( created_by_id=user_id, members=[MemberRequest(user_id=alice.id), MemberRequest(user_id=bob.id)], ), ring=True, video=True, # Set it to `false` for audio calls. This is used to decide what type of incoming call screen is displayed client-side)
import ( "github.com/google/uuid")// Make sure to use a unique id when creating ring callsclient.Video().Call("default", uuid.New().String()).GetOrCreate(ctx, &getstream.GetOrCreateCallRequest{ Ring: getstream.PtrTo(true), Video: getstream.PtrTo(true), // Set it to `false` for audio calls. This is used to decide what type of incoming call screen is displayed client-side Data: &getstream.CallRequest{ CreatedByID: getstream.PtrTo("myself"), Members: []getstream.MemberRequest{ {UserID: "myself"}, {UserID: "my friend"}, }, },})
List<MemberRequest> members = List.of( MemberRequest.builder().userID("myself").build(), MemberRequest.builder().userID("my friend").build());// Make sure to use a unique id when creating ring callsvar call = new Call("default", UUID.randomUUID().toString(), client.video());call.getOrCreate( GetOrCreateCallRequest.builder() .data( CallRequest.builder() .createdByID("myself") .members(members) .build() ) .ring(true) .video(true) // Set it to `false` for audio calls. This is used to decide what type of incoming call screen is displayed client-side .build());
CALL_TYPE='default'# Make sure to use a unique id when creating ring callsCALL_ID=$(uuidgen)curl -X POST "https://video.stream-io-api.com/api/v2/video/call/${CALL_TYPE}/${CALL_ID}?api_key=${API_KEY}" \ -H "Authorization: ${TOKEN}" \ -H "Content-Type: application/json" \ -H "stream-auth-type: jwt" \ -d '{ "ring": true, "video": true, "data": { "created_by_id": "myself", "members": [{ "user_id": "myself" }, { "user_id": "my friend" }] } }'
In some cases, you may want to ring individual members instead of the whole call, or you want to ring a member into an existing call.
You can do this by using the ring method:
// Make sure to use a unique id when creating ring callsconst call = client.call("default", crypto.randomUUID());await call.getOrCreate({ ring: false, // set to false to avoid ringing the whole call data: { created_by_id: "myself", members: [{ user_id: "myself" }, { user_id: "my-friend" }], },});// note: my-friend needs to be a member of the callawait call.ring({ members_ids: ["my-friend"] });// to invite a new member and ring themawait call.updateCallMembers({ update_members: [{ user_id: "my-other-friend" }],});await call.ring({ members_ids: ["my-other-friend"] });// to ring everyoneawait call.ring();
# Make sure to use a unique id when creating ring callsimport uuidfrom stream_video.types.call import CallRequest, MemberRequestcall_id = str(uuid.uuid4())call = client.video.call("default", call_id)call.get_or_create( ring=False, # set to false to avoid ringing the whole call data=CallRequest( created_by_id="myself", members=[ MemberRequest(user_id="myself"), MemberRequest(user_id="my-friend") ], ))# note: my-friend needs to be a member of the callcall.ring(member_ids=["my-friend"])# to invite a new member and ring themcall.update_call_members( update_members=[MemberRequest(user_id="my-other-friend")])call.ring(member_ids=["my-other-friend"])
// Make sure to use a unique id when creating ring callsimport ( "github.com/google/uuid" getstream "github.com/GetStream/stream-video-go")// Create a new call with a random IDcallID := uuid.NewString()call := client.Video().Call("default", callID)// Create user IDsmyself := "myself"myFriend := "my-friend"myOtherFriend := "my-other-friend"// Create call with two membersmembers := []getstream.MemberRequest{ {UserID: myself}, {UserID: myFriend},}callRequest := getstream.GetOrCreateCallRequest{ Ring: getstream.PtrTo(false), // set to false to Data: &getstream.CallRequest{ avoid ringing the whole call CreatedByID: getstream.PtrTo(myself), Members: members, },}call.GetOrCreate(ctx, &callRequest)// note: my-friend needs to be a member of the callcall.Ring(ctx, []string{myFriend})// to invite a new member and ring themcall.UpdateCallMembers(ctx, &getstream.UpdateCallMembersRequest{ UpdateMembers: []getstream.MemberRequest{ {UserID: myOtherFriend}, },})// Ring the newly added membercall.Ring(ctx, []string{myOtherFriend})
This event is dispatched to the call members when the caller or callee rejects the call.
There are multiple reasons why someone could reject a call (for example, someone explicitly rejecting a call with a "Decline" button is different from someone not answering the call). The call.rejected event contains a reason field to indicate why a call was rejected.
You can pass any string to the reason field, but the Stream API and the SDKs recognize the following values by default:
Name
Description
rejected
The callee explicitly rejected the call (for example, with a "Decline" button)
cancel
The caller cancelled the call before it was answered. SDKs will also send this event if all callees rejected the call.
timeout
1. When a callee is online but doesn't answer the call in incoming_call_timeout_ms time. 2. When no callee accepts the call in auto_cancel_timeout_ms time, the caller will send this event
busy
Signals that the callee is busy. By default, SDKs don't send this reason; integrators can implement this in their own application logic.
SDKs implement ringing flow in a way that while a call is in the ringing phase, there is no active session. The session starts when the first callee accepts the call.
All accept
Example: Sara calls Ben and Jane. Both Ben and Jane accept the call.
Sara rings Ben and Jane (with ring: true): call.created and call.ring events from Sara to all call members - ringing flow starts
Ben accepts the call: call.accepted event from Ben
Ben joins the call: call.session_started and call.session_participant_joined events
As soon as Ben accepts the call Sara also joins: call.session_participant_joined
Jane also accepts, and joins: call.accepted and call.session_participant_joined
Jane leaves the call: call.participant_left
Ben and Sara also leave: 2x call.participant_left
call.session_ended when inactivity_timeout_seconds elapsed since last participant left the call
Someone rejects
Example: Sara calls Ben and Jane. Ben accepts the call, Jane rejects.
Sara rings Ben and Jane (with ring: true): call.created and call.ring events from Sara to all call members - ringing flow starts
Ben accepts the call: call.accepted event from Ben
Ben joins the call: call.session_started and call.session_participant_joined
As soon as Ben accepts the call Sara also joins: call.session_participant_joined
Jane declines the call: call.rejected event with reason: "decline" from Jane
Ben and Sara leave the call: 2x call.participant_left
call.session_ended when inactivity_timeout_seconds elapsed since last participant left the call
All reject
Example: Sara calls Ben and Jane. Both Jane and Ben reject the call.
Sara rings Ben and Jane (with ring: true): call.created and call.ring events from Sara to all call members - ringing flow starts
Ben declines the call: call.rejected event with reason: "decline" from Ben
Jane is online, but doesn't interact with the incoming call screen: call.rejected event with reason: "timeout" from Jane after incoming_call_timeout_ms elapsed
call.missed is sent to Jane
Since both Ben and Jane rejected the call Sara will reject as well (this is sent from SDKs): call.rejected event with reason: "cancel" from Sara
Caller cancels
Example: Sara calls Ben and Jane, but Sara cancels before anyone answers.
Sara rings Ben and Jane (with ring: true): call.created and call.ring events from Sara to all call members - ringing flow starts
Sara changes her mind, and cancels: call.rejected event with reason: "cancel" from Sara
No one answers the call
Example: Sara calls Ben and Jane, but no one answers.
Sara rings Ben and Jane (with ring: true): call.created and call.ring events from Sara to all call members - ringing flow starts
No one accepts or rejects the call in auto_cancel_timeout_ms time (this can happen if users are offline, and away from their devices): call.rejected event with reason: "timeout" from Sara