Ring Calls
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.
Recommended setup for phone-style calls
Most apps that ring want phone-call behavior: the caller rings, someone answers or nobody does, and the call is over once it ends. Three parts of the Stream API are designed to work together for this, and we recommend using all three:
- A new call ID for every ring. One call is one conversation. Don't ring an old call again, create a new one.
- The
single_usecall lifecycle. When the caller hangs up before anyone answers, or nobody answers before the ring times out, Stream ends the call for you, and an ended call can no longer be accepted, joined, rung or re-created. Leaving an answered call doesn't end it, so end the call when the conversation is over if you want the same protection afterwards. A late tap on a stale incoming-call screen fails cleanly instead of putting someone alone in an empty call. The built-inaudio_callcall type uses it by default. See Call lifecycle. - The
call.ring_settledwebhook. Your backend receives one event per ring, sent once the ring has a final outcome, with the final state of every user you rang. Use it for call logs, missed-call records and analytics instead of assembling the outcome yourself fromcall.accepted,call.rejectedandcall.missed. See Ring call events.
Each ring has a ring_id, carried by both call.ring and call.ring_settled, so your backend can match the start of a ring to its outcome.
Creating ring calls
To start the ringing flow, set the ring flag to true and provide the list of members you want to call. The caller should also be included in the list of members. Use a new call ID for every ring, for example a UUID.
The example rings a voice call on the built-in audio_call type, which already uses the single_use lifecycle. The video flag decides which incoming call screen the client-side SDKs show: false for an audio call, true for a video call.
For video calls, don't use audio_call with video: true: the type has video and screen sharing disabled, so the callee would see a video call screen in a call where nobody can send video. Ring on a call type with video enabled instead, and give the call the single_use lifecycle, either on a call type of your own or for each call with settings_override. Setting the mode shows both, including a video call on the default type.
// Make sure to use a unique id when creating ring calls
const call = client.video.call("audio_call", crypto.randomUUID());
await call.getOrCreate({
ring: true,
video: false, // a voice call: the SDKs show the audio incoming call screen
data: {
created_by_id: "myself",
members: [{ user_id: "myself" }, { user_id: "my friend" }],
},
});from getstream.models import CallRequest, MemberRequest
import uuid
# Make sure to use a unique id when creating ring calls
client.video.call("audio_call", 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=False, # a voice call: the SDKs show the audio incoming call screen
)import (
"github.com/google/uuid"
)
// Make sure to use a unique id when creating ring calls
client.Video().Call("audio_call", uuid.New().String()).GetOrCreate(ctx, &getstream.GetOrCreateCallRequest{
Ring: getstream.PtrTo(true),
Video: getstream.PtrTo(false), // a voice call: the SDKs show the audio incoming call screen
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 calls
var call = new Call("audio_call", UUID.randomUUID().toString(), client.video());
call.getOrCreate(
GetOrCreateCallRequest.builder()
.data(
CallRequest.builder()
.createdByID("myself")
.members(members)
.build()
)
.ring(true)
.video(false) // a voice call: the SDKs show the audio incoming call screen
.build()
);CALL_TYPE='audio_call'
# Make sure to use a unique id when creating ring calls
CALL_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": false,
"data": {
"created_by_id": "myself",
"members": [{ "user_id": "myself" }, { "user_id": "my friend" }]
}
}'Accepting and rejecting ring calls
Accepting and rejecting calls are client-side operations, you can read more about them in SDK documentations.
Ringing individual members
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 calls
const call = client.video.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 call
await call.ring({ members_ids: ["my-friend"] });
// to invite a new member and ring them
await call.updateCallMembers({
update_members: [{ user_id: "my-other-friend" }],
});
await call.ring({ members_ids: ["my-other-friend"] });
// to ring everyone
await call.ring();# Make sure to use a unique id when creating ring calls
import uuid
from getstream.models import CallRequest, MemberRequest
call_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 call
call.ring(members_ids=["my-friend"])
# to invite a new member and ring them
call.update_call_members(
update_members=[MemberRequest(user_id="my-other-friend")]
)
call.ring(members_ids=["my-other-friend"])CALL_TYPE='default'
CALL_ID=$(uuidgen)
ME=myself
SARA=sara
ADAM=adam
JOHN=john
echo "Step 1: Creating a call without ringing..."
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\": false,
\"data\": {
\"created_by_id\": \"${ME}\",
\"members\": [{ \"user_id\": \"${ME}\" }, { \"user_id\": \"${SARA}\" }]
}
}"
echo -e "\n"
echo "Step 2: Ringing sara..."
curl -X POST "https://video.stream-io-api.com/api/v2/video/call/${CALL_TYPE}/${CALL_ID}/ring?api_key=${API_KEY}" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-H "stream-auth-type: jwt" \
-d "{
\"members_ids\": [\"${SARA}\"],
\"video\": true
}"
echo -e "\n"
echo "Step 3: Adding a new members (adam, john)..."
curl -X POST "https://video.stream-io-api.com/api/v2/video/call/${CALL_TYPE}/${CALL_ID}/members?api_key=${API_KEY}" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-H "stream-auth-type: jwt" \
-d "{
\"update_members\": [{ \"user_id\": \"${ADAM}\" }, { \"user_id\": \"${JOHN}\" }]
}"
echo -e "\n"
echo "Step 4: Ringing adam and john..."
ENCODED_USER_ADAM=$(echo ${ADAM} | perl -MURI::Escape -lne 'print uri_escape($_)')
ENCODED_USER_JOHN=$(echo ${JOHN} | perl -MURI::Escape -lne 'print uri_escape($_)')
curl -X POST "https://video.stream-io-api.com/api/v2/video/call/${CALL_TYPE}/${CALL_ID}/ring?api_key=${API_KEY}" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-H "stream-auth-type: jwt" \
-d "{
\"members_ids\": [\"${ADAM}\", \"${JOHN}\"],
\"video\": true
}"// Make sure to use a unique id when creating ring calls
import (
"github.com/google/uuid"
getstream "github.com/GetStream/stream-video-go"
)
// Create a new call with a random ID
callID := uuid.NewString()
call := client.Video().Call("default", callID)
// Create user IDs
myself := "myself"
myFriend := "my-friend"
myOtherFriend := "my-other-friend"
// Create call with two members
members := []getstream.MemberRequest{
{UserID: myself},
{UserID: myFriend},
}
callRequest := getstream.GetOrCreateCallRequest{
Ring: getstream.PtrTo(false), // set to false to avoid ringing the whole call
Data: &getstream.CallRequest{
CreatedByID: getstream.PtrTo(myself),
Members: members,
},
}
call.GetOrCreate(ctx, &callRequest)
// note: my-friend needs to be a member of the call
call.Ring(ctx, []string{myFriend})
// to invite a new member and ring them
call.UpdateCallMembers(ctx, &getstream.UpdateCallMembersRequest{
UpdateMembers: getstream.PtrTo([]getstream.MemberRequest{
{UserID: myOtherFriend},
}),
})
// Ring the newly added member
call.Ring(ctx, []string{myOtherFriend})Ring call events
The ring flow relies on the following events:
call.ringcall.acceptedcall.rejectedcall.missedcall.ring_settled(backend only)
call.ring event
This is the start of the ring flow. This event is dispatched to call members when someone starts a call with the ring flag set to true. It carries a ring_id that identifies this ring.
It's possible to send push notifications to call members on this event, for more information see Notification settings section
call.accepted event
This event is dispatched to the call members when a callee accepts an incoming call.
call.rejected event
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 |
|---|---|
decline |
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's SDK sends this event (the iOS SDK sends cancel instead) |
busy |
Signals that the callee is busy. The iOS SDK sends it when the user rejects a ring while already in another call. The JavaScript and Android SDKs can reject incoming calls while the user is busy when rejectCallWhenBusy is enabled (off by default). |
call.missed event
The call.missed event is dispatched to callees who didn't answer (accept or reject) an incoming call within a given time.
The event is dispatched in the following scenarios:
- The callee rejected the call with a
timeoutorbusyreason - When a callee didn't accept or reject the call in
missed_call_timeout_mstime (probably the user was offline) - The caller rejected the call with any reason, unless the callee previously rejected the call (declined the call)
It's possible to send push notifications to call members on this event, for more information see Notification settings section
call.ring_settled event
The call.ring_settled event tells your backend how one ring ended. It is sent once per ring, when the ring has a final outcome: every user you rang has accepted, rejected or missed it, the caller cancelled it, or the call ended while it was still ringing.
- Backend only. It goes to your webhook, and to SQS, SNS or Pub/Sub if you use them. It is never sent to clients and never triggers a push notification. Clients learn each answer from
call.accepted,call.rejectedandcall.missed, as before. - One event per ring. The server sends at most one
call.ring_settledfor a ring, with the samering_idas thecall.ringthat started it. Webhook delivery is retried, so the same event can reach you more than once: deduplicate onring_id. - Sent with the event that closed the ring. It is created within milliseconds of that event: just before a callee's
call.acceptedorcall.rejected, the server'scall.missedor acall.ended, and just after the caller'scall.rejected. Webhook deliveries can arrive in any order, so order events bycreated_at, not by arrival. - A group ring stays open after the first accept. The ring settles once every user you rang has answered, when the missed-call timeout passes and the server marks the rest as missed, or when the call ends. So
call.ring_settledcan arrive some time after the firstcall.accepted. - Subscribing. A webhook that receives all events gets
call.ring_settledautomatically. If your webhook subscribes to a list of specific events, addcall.ring_settledto the list. See webhooks.
The payload:
| Field | Description |
|---|---|
call_cid |
The call (<type>:<id>) |
session_id |
The call session the ring belongs to |
ring_id |
Identifies this ring, the same value as on call.ring |
initiated_by |
ID of the user who rang |
rang_at |
When the ring was sent |
outcome |
The final outcome of the ring, see below |
settled_at |
When the ring reached its outcome |
settled_by |
ID of the user whose action closed the ring, or server for the missed-call timeout and for an end with no user behind it |
settle_reason |
What closed the ring, see below |
targets |
The final state of every user you rang, keyed by user ID: state, state_at and reason, see below |
Each target's state is accepted, rejected, missed or cancelled. A callee whose device rejected with reason timeout or busy counts as missed, with that reason kept on the target. A callee still unanswered when the caller cancels, or when the call ends, is cancelled, and so is a callee the missed-call timeout can't mark as missed, for example one who joined the call without accepting or was removed from it while it rang. A rejected or missed target's reason is what the callee rejected with. A cancelled target's reason is the caller's reject reason, or why the ring was closed.
The outcome summarizes the targets:
| Outcome | Meaning |
|---|---|
answered_all |
Everyone you rang accepted |
answered_some |
At least one user accepted, the others rejected, missed or were cancelled |
rejected_all |
Everyone you rang declined |
missed_all |
Everyone you rang missed it, including callees whose device rejected with timeout or busy |
cancelled |
Everyone you rang was still unanswered when the caller cancelled or the call ended |
mixed |
Any other combination, for example one decline and one miss |
The settle_reason says what closed the ring. Read it together with settled_by:
settle_reason |
settled_by |
What happened |
|---|---|---|
answered |
a callee | The callee's accept was the last answer the ring was waiting for |
rejected |
a callee | The callee's decline was the last answer the ring was waiting for |
timeout |
a callee | The callee's device rejected with timeout or busy, and that was the last answer |
timeout |
server |
The missed-call timeout passed while some callees were still unanswered |
cancel |
the caller | The caller rejected the ring (hung up), with any reason |
ended |
a user | A user ended or deleted the call from a client while it was ringing |
auto_end |
server |
The call ended while it was ringing with no user behind it: the server ended it (for example, a session timeout), or your backend ended or deleted it |
The SDKs reject on the caller's behalf when nobody accepts within auto_cancel_timeout_ms, so an unanswered ring often settles as cancelled with settle_reason: "cancel". If the callees' devices time out first, it settles as missed_all with settle_reason: "timeout", settled by the last callee. It settles as missed_all with settled_by: "server" when nobody rejects before the missed-call timeout, for example because the apps were closed while ringing.
If an event is lost despite the retries, the call session's accepted_by, rejected_by and missed_by maps, returned when you get the call, still record each user's answer, merged over every ring of the session. They don't match the targets one to one: rejected_by also holds timeout and busy rejects and the caller's cancel, and callees the caller cancelled on are in missed_by.
call.ring_settled event reference
Examples
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.
call.ring_settled is sent to your backend only, see ring call events. The call.ended steps happen only on a call with the single_use lifecycle; a reusable call stays open.
All accept
Example: Sara calls Ben and Jane. Both Ben and Jane accept the call.
- Sara rings Ben and Jane (with
ring: true):call.createdandcall.ringevents from Sara to all call members - ringing flow starts - Ben accepts the call:
call.acceptedevent from Ben - Ben joins the call:
call.session_startedandcall.session_participant_joinedevents - As soon as Ben accepts the call Sara also joins:
call.session_participant_joined - Jane also accepts, and joins:
call.acceptedandcall.session_participant_joined - Everyone has answered, so the ring settles:
call.ring_settledwithoutcome: "answered_all",settled_by: "jane"andsettle_reason: "answered" - Jane leaves the call:
call.session_participant_left - Ben and Sara also leave: 2x
call.session_participant_left call.session_endedwheninactivity_timeout_secondselapsed 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.createdandcall.ringevents from Sara to all call members - ringing flow starts - Ben accepts the call:
call.acceptedevent from Ben - Ben joins the call:
call.session_startedandcall.session_participant_joined - As soon as Ben accepts the call Sara also joins:
call.session_participant_joined - Jane declines the call:
call.rejectedevent withreason: "decline"from Jane - Everyone has answered, so the ring settles:
call.ring_settledwithoutcome: "answered_some",settled_by: "jane"andsettle_reason: "rejected" - Ben and Sara leave the call: 2x
call.session_participant_left call.session_endedwheninactivity_timeout_secondselapsed 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.createdandcall.ringevents from Sara to all call members - ringing flow starts - Ben declines the call:
call.rejectedevent withreason: "decline"from Ben - Jane is online, but doesn't interact with the incoming call screen:
call.rejectedevent withreason: "timeout"from Jane afterincoming_call_timeout_mselapsed call.missedis sent to Jane- Everyone has answered, so the ring settles:
call.ring_settledwithoutcome: "mixed"(one decline, one miss),settled_by: "jane"andsettle_reason: "timeout" - Since both Ben and Jane rejected the call Sara will reject as well (this is sent from SDKs):
call.rejectedevent withreason: "cancel"from Sara - On a
single_usecall, Sara's reject ends the call:call.endedwithreason: "RejectedByCaller"
Caller cancels
Example: Sara calls Ben and Jane, but Sara cancels before anyone answers.
- Sara rings Ben and Jane (with
ring: true):call.createdandcall.ringevents from Sara to all call members - ringing flow starts - Sara changes her mind, and cancels:
call.rejectedevent withreason: "cancel"from Sara, thencall.ring_settledwithoutcome: "cancelled",settled_by: "sara"andsettle_reason: "cancel", andcall.missedsent to Ben and Jane - On a
single_usecall, the call ends after thecall.missedevents:call.endedwithreason: "RejectedByCaller". A late accept from Ben or Jane now fails with HTTP 400, and a late join with HTTP 403
No one answers the call
Example: Sara calls Ben and Jane, but no one answers.
- Sara rings Ben and Jane (with
ring: true):call.createdandcall.ringevents from Sara to all call members - ringing flow starts - No one accepts or rejects the call in
auto_cancel_timeout_mstime (this can happen if users are offline, and away from their devices):call.rejectedevent withreason: "timeout"from Sara (the iOS SDK sendsreason: "cancel") - The ring settles, because Sara's SDK closed it:
call.ring_settledwithoutcome: "cancelled",settled_by: "sara"andsettle_reason: "cancel". Thencall.missedevents are sent to both Ben and Jane - On a
single_usecall, Sara's reject ends the call:call.endedwithreason: "RejectedByCaller"
If Sara's app is closed while it rings, her SDK never sends that reject. If Ben's and Jane's devices don't reject either, for example because they are offline, the server closes the ring at its missed-call timeout: call.ring_settled with outcome: "missed_all", settled_by: "server" and settle_reason: "timeout", then call.missed events to Ben and Jane. On a single_use call, the server then ends the call: call.ended with reason: "RingNotAnswered". A late accept from Ben or Jane fails with HTTP 400, and a late join with HTTP 403. A reusable call stays open.
Call lifecycle
The lifecycle.mode call setting decides whether a call can be used again after it ends. It has two values:
reusableis the default for custom call types and for the built-indefault,audio_room,livestreamanddevelopmenttypes, and the behavior every call had before the setting existed. After the ring, the call stays open: it can be rung again, and users with thejoin-ended-callpermission can join it after it has ended. This suits calls that are places, such as a meeting room or a recurring meeting.single_usegives a call phone-call semantics. It is the recommended mode for ringing apps that create a new call for every ring, and the default for the built-inaudio_callandagenttypes.
What single_use changes
The caller's reject ends the call. The caller is the user who created the call. When the caller rejects the ring, with any reason, the call is ended with reason: "RejectedByCaller" on the call.ended event. This holds even if a callee has already accepted the ring or joined the call. The SDKs send this reject when the caller hangs up while it rings, and also when nobody accepts within auto_cancel_timeout_ms. call.ended comes after the call.rejected and call.missed events for that ring. If the caller had already joined the call session while it was ringing, the session ends too.
A ring that times out on the server ends the call. If the caller's app never sends that reject, for example because it was closed while ringing, the server closes the ring at its missed-call timeout and then ends the call with reason: "RingNotAnswered". The server ends it, so this call.ended has no user. Your backend doesn't need to end these calls itself; if it does, check ended_at first, because ending a call that has already ended still succeeds and sends another call.ended. Ordered by created_at, call.ended comes after the call.missed events and call.ring_settled for that ring. The call is not ended if someone accepted the ring or joined the call, if the call was rung again and that ring is still open, if a new call session has started, or if auto_cancel_timeout_ms is longer than the missed-call timeout, because the caller may then still be waiting. The missed-call timeout is missed_call_timeout_ms (30 s when unset), except that a call whose settings_override sets incoming_call_timeout_ms uses that value plus 3 seconds.
An ended call stays ended. Once a single_use call has ended, whether by the caller's reject, by the ring timing out or by end call:
- Accepting or rejecting it fails with HTTP 400, as on any ended call.
- Joining it fails with HTTP 403, whatever the user's permissions. The
join-ended-callpermission has no effect on asingle_usecall. - Get-or-create on its call ID fails with HTTP 400 instead of returning the ended call. Create a new call with a new ID instead.
- Ringing it fails with HTTP 400: the ring endpoint refuses any ended call, in either mode, and get call with
ringornotifyrefuses an endedsingle_usecall. - Get call still returns it, so you can read its final state.
What it does not change:
- A callee's reject never ends the call, whatever the reason.
- Calls that never ring, such as meetings and livestreams, behave the same in both modes until they end. Once ended, a
single_usecall is closed for good whether it rang or not.
Setting the mode
Set the mode on the call type, so every new call of that type uses it:
await client.video.updateCallType({
name: callTypeName,
settings: {
lifecycle: { mode: "single_use" },
},
});client.Video().UpdateCallType(ctx, callTypeName, &getstream.UpdateCallTypeRequest{
Settings: &getstream.CallSettingsRequest{
Lifecycle: &getstream.LifecycleSettingsRequest{
Mode: getstream.PtrTo(getstream.CallLifecycleModeSingleUse),
},
},
})curl -X PUT "https://video.stream-io-api.com/api/v2/video/calltypes/${CALL_TYPE_NAME}?api_key=${API_KEY}" \
-H "Authorization: ${TOKEN}" \
-H "stream-auth-type: jwt" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"lifecycle": { "mode": "single_use" }
}
}'Or set it for one call when you create it, with settings_override. This example rings a video call on the built-in default type:
const call = client.video.call("default", crypto.randomUUID());
await call.getOrCreate({
ring: true,
video: true,
data: {
created_by_id: "myself",
members: [{ user_id: "myself" }, { user_id: "my friend" }],
settings_override: {
lifecycle: { mode: "single_use" },
},
},
});client.Video().Call("default", uuid.New().String()).GetOrCreate(ctx, &getstream.GetOrCreateCallRequest{
Ring: getstream.PtrTo(true),
Video: getstream.PtrTo(true),
Data: &getstream.CallRequest{
CreatedByID: getstream.PtrTo("myself"),
Members: []getstream.MemberRequest{
{UserID: "myself"},
{UserID: "my friend"},
},
SettingsOverride: &getstream.CallSettingsRequest{
Lifecycle: &getstream.LifecycleSettingsRequest{
Mode: getstream.PtrTo(getstream.CallLifecycleModeSingleUse),
},
},
},
})curl -X POST "https://video.stream-io-api.com/api/v2/video/call/default/${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" }],
"settings_override": {
"lifecycle": { "mode": "single_use" }
}
}
}'The lifecycle setting needs the Node.js SDK v0.8.14 or later, or the Go SDK v8.0.0 or later. The Python and Java SDKs don't have it yet, so use the REST API from those languages.
A call that doesn't set its own mode follows its call type, so changing the call type's mode also changes it for the existing calls of that type. A mode set in settings_override when the call is created stays with the call. No call can change its mode afterwards: updating a call with a lifecycle.mode different from its current one fails with call lifecycle cannot be changed after creation, and an update that repeats the current mode stores it on the call, which then stops following its call type. A settings_override sent from a client needs the update-call-settings permission, like any other settings override.
The call's resolved mode is returned in call.settings.lifecycle.mode, and is always present: a call type that never set the mode reports single_use on the built-in audio_call and agent types and reusable on every other type, including a custom type created under one of those names before it became built-in.
Ring call settings
Timeouts
You can control how much time to wait before automatic call rejections and missed call events are dispatched.
RingSettingsRequest
| Name | Type | Description | Constraints |
|---|---|---|---|
auto_cancel_timeout_ms | integer | When none of the callees accept a ring call in this time a rejection will be sent by the caller with reason 'timeout' by the SDKs | Required, Minimum: 5000, Maximum: 180000 |
incoming_call_timeout_ms | integer | When a callee is online but doesn't answer a ring call in this time a rejection will be sent with reason 'timeout' by the SDKs | Required, Minimum: 5000, Maximum: 180000 |
missed_call_timeout_ms | integer | When a callee doesn't accept or reject a ring call in this time a missed call event will be sent | Minimum: 5000, Maximum: 180000 |
These can be set on the call type level or on the call level:
// on call type level
await client.video.updateCallType({
name: callTypeName,
settings: {
ring: {
incoming_call_timeout_ms: 10000,
auto_cancel_timeout_ms: 15000,
missed_call_timeout_ms: 15000,
},
},
});
// or on call level
await call.update({
settings_override: {
ring: {
incoming_call_timeout_ms: 10000,
auto_cancel_timeout_ms: 15000,
missed_call_timeout_ms: 15000,
},
},
});# On call type level
client.video.update_call_type(
name=callTypeName,
settings={
"ring": {
"incoming_call_timeout_ms": 10000,
"auto_cancel_timeout_ms": 15000,
"missed_call_timeout_ms": 15000
}
}
)
# On call level
call.update(
settings_override={
"ring": {
"incoming_call_timeout_ms": 10000,
"auto_cancel_timeout_ms": 15000,
"missed_call_timeout_ms": 15000
}
}
)// Call type level
client.Video().UpdateCallType(ctx, callTypeName, &getstream.UpdateCallTypeRequest{
Settings: &getstream.CallSettingsRequest{
Ring: &getstream.RingSettingsRequest{
IncomingCallTimeoutMs: 10000,
AutoCancelTimeoutMs: 15000,
MissedCallTimeoutMs: getstream.PtrTo(15000),
},
},
})
// On call level
call.Update(ctx, &getstream.UpdateCallRequest{
SettingsOverride: &getstream.CallSettingsRequest{
Ring: &getstream.RingSettingsRequest{
IncomingCallTimeoutMs: 10000,
AutoCancelTimeoutMs: 15000,
MissedCallTimeoutMs: getstream.PtrTo(15000),
},
},
})# On call type level
curl -X PUT "https://video.stream-io-api.com/api/v2/video/calltypes/${CALL_TYPE_NAME}?api_key=${API_KEY}" \
-H "Authorization: ${TOKEN}" \
-H "stream-auth-type: jwt" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"ring": {
"incoming_call_timeout_ms": 10000,
"auto_cancel_timeout_ms": 15000,
"missed_call_timeout_ms": 15000
}
}
}'
# On call level
curl -X PATCH "https://video.stream-io-api.com/api/v2/video/call/${CALL_TYPE}/${CALL_ID}?api_key=${API_KEY}" \
-H "Authorization: ${TOKEN}" \
-H "stream-auth-type: jwt" \
-H "Content-Type: application/json" \
-d '{
"settings_override": {
"ring": {
"incoming_call_timeout_ms": 10000,
"auto_cancel_timeout_ms": 15000,
"missed_call_timeout_ms": 15000
}
}
}'Push notifications
You can enable/disable push notifications for the call.ring and call.missed events. Provider configuration (APNs certificates and Firebase credentials) is covered in push notifications.
await client.video.createCallType({
name: "<call type name>",
notification_settings: {
enabled: true,
call_notification: {
apns: {
title: "{{ user.display_name }} calls you",
body: "{{ user.display_name }} calls you",
},
enabled: true,
},
call_ring: {
apns: {
title: "{{ user.display_name }} calls you",
body: "{{ user.display_name }} calls you",
},
enabled: true,
},
call_live_started: {
enabled: true,
apns: {
title: "{{ call.display_name }} started",
body: "{{ user.display_name }} started",
},
},
call_missed: {
enabled: true,
apns: {
title: "missed call from {{ user.display_name }}",
body: "missed call from {{ user.display_name }}",
},
},
session_started: {
enabled: true,
apns: {
title: "{{ call.display_name }} started",
body: "{{ call.display_name }} started",
},
},
},
});client.video.create_call_type(
name=call_type_name,
notification_settings={
"enabled": True,
"call_notification": {
"apns": {
"title": "{{ user.display_name }} calls you",
"body": "{{ user.display_name }} calls you"
},
"enabled": True
},
"call_ring": {
"apns": {
"title": "{{ user.display_name }} calls you",
"body": "{{ user.display_name }} calls you"
},
"enabled": True
},
"call_live_started": {
"enabled": True,
"apns": {
"title": "{{ call.display_name }} started",
"body": "{{ user.display_name }} started"
}
},
"call_missed": {
"enabled": True,
"apns": {
"title": "missed call from {{ user.display_name }}",
"body": "missed call from {{ user.display_name }}"
}
},
"session_started": {
"enabled": True,
"apns": {
"title": "{{ call.display_name }} started",
"body": "{{ call.display_name }} started"
}
}
}
)client.Video().CreateCallType(ctx, &getstream.CreateCallTypeRequest{
Name: "test-call-type",
NotificationSettings: &getstream.NotificationSettings{
Enabled: true,
CallNotification: getstream.EventNotificationSettings{
APNS: getstream.APNS{
Title: "{{ user.display_name }} calls you",
Body: "{{ user.display_name }} calls you",
},
Enabled: true,
},
CallRing: getstream.EventNotificationSettings{
APNS: getstream.APNS{
Title: "{{ user.display_name }} calls you",
Body: "{{ user.display_name }} calls you",
},
Enabled: true,
},
CallLiveStarted: getstream.EventNotificationSettings{
Enabled: true,
APNS: getstream.APNS{
Title: "{{ call.display_name }} started",
Body: "{{ user.display_name }} started",
},
},
CallMissed: getstream.EventNotificationSettings{
Enabled: true,
APNS: getstream.APNS{
Title: "missed call from {{ user.display_name }}",
Body: "missed call from {{ user.display_name }}",
},
},
SessionStarted: getstream.EventNotificationSettings{
Enabled: true,
APNS: getstream.APNS{
Title: "{{ call.display_name }} started",
Body: "{{ call.display_name }} started",
},
},
},
})curl -X POST "https://video.stream-io-api.com/api/v2/video/calltypes?api_key=${API_KEY}" \
-H "Authorization: ${TOKEN}" \
-H "stream-auth-type: jwt" \
-H "Content-Type: application/json" \
-d '{
"name": "<call type name>",
"notification_settings": {
"enabled": true,
"call_notification": {
"apns": {
"title": "{{ user.display_name }} calls you",
"body": "{{ user.display_name }} calls you"
},
"enabled": true
},
"call_ring": {
"apns": {
"title": "{{ user.display_name }} calls you",
"body": "{{ user.display_name }} calls you"
},
"enabled": true
},
"call_live_started": {
"enabled": true,
"apns": {
"title": "{{ call.display_name }} started",
"body": "{{ user.display_name }} started"
}
},
"call_missed": {
"enabled": true,
"apns": {
"title": "missed call from {{ user.display_name }}",
"body": "missed call from {{ user.display_name }}"
}
},
"session_started": {
"enabled": true,
"apns": {
"title": "{{ call.display_name }} started",
"body": "{{ call.display_name }} started"
}
}
}
}'