Call management
This page explains how your server queries the app's calls, ends calls, removes members from calls, and gets daily call statistics. For call flows, states, end reasons and call object fields, see Audio and video calls.
| You want to | API |
|---|---|
Look up calls by user or time in support or consultation services, matching your records via ext (such as an order number) | List calls, Get call details |
| End a call when you find a violation | End a call |
| Remove a member from a group call | Remove a member from a call |
| Confirm a user isn't in a call before a business operation | Get a user's ongoing calls |
| Check daily call counts and historical minutes | Get call statistics. Actual charges follow the audio/video transactions in the console |
Notes:
- Not subject to the runtime policy
rtc_enabled: you can still query and end calls after the app turns off audio and video, and also while the app is read-only. - You can't start calls or answer on users' behalf: calls need the user's device connected to the media service, so only clients can start and answer them.
- Call data is deleted 180 days after the call ends and can't be queried afterwards; to keep it longer, save it to your business system from the
rtc.call_endedevent callback. - The "Audio/video" page in the app's details in the console can also query calls and show statistics, and
ownerandadmincan end calls and remove members there.
List calls
Lists the app's calls matching the conditions, newest started first, including ongoing and ended calls. Items in the list don't include members; for members, get call details.
/{org_name}/{app_name}/callsPath parameters
For org_name and app_name, see Server overview.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
username | String | No | Only calls this user took part in, that is calls where they're a member, including ones they missed or declined. An empty list if the user doesn't exist |
group_id | String | No | Only group calls in this group |
type | String | No | Call type: single or group |
status | String | No | Call status: ringing, active or ended. For ongoing calls, query ringing and active separately |
start_time | String | No | Only calls started at or after this time, RFC 3339, such as 2026-10-04T19:23:00Z |
end_time | String | No | Only calls started before this time, same format |
limit | Number | No | Items per page, 1 to 100, 20 by default |
cursor | String | No | Cursor for the next page, the next_cursor from the previous page; omit for the first page |
Conditions can be combined.
Example request
curl "$IM_API/$ORG/$APP/calls?username=alice&limit=2" \
-H "Authorization: Bearer $APP_TOKEN"Response
Returns 200 OK:
| Field | Type | Description |
|---|---|---|
items | Array<Object> | Calls on this page: call objects without members |
next_cursor | String | Cursor for the next page. An empty string "" when there's none |
{
"items": [
{
"call_id": "100313814330769408",
"type": "single",
"group_id": null,
"media": "audio",
"status": "ended",
"initiator": "bob",
"max_participants": 2,
"joined_count": 0,
"ext": null,
"created_at": "2026-10-04T19:31:17.077Z",
"answered_at": "2026-10-04T19:31:21.115Z",
"ended_at": "2026-10-04T19:31:49.168Z",
"end_reason": "completed",
"ended_by": "bob",
"duration_seconds": 28,
"version": 3,
"ended_via": "client",
"member_count": 2,
"peak_joined": 2,
"billable_minutes": 2
},
{
"call_id": "100313666666102784",
"type": "group",
"group_id": "100311544667045888",
"media": "video",
"status": "ended",
"initiator": "alice",
"max_participants": 16,
"joined_count": 0,
"ext": {
"topic": "周会"
},
"created_at": "2026-10-04T19:30:41.871Z",
"answered_at": "2026-10-04T19:30:44.923Z",
"ended_at": "2026-10-04T19:31:17.049Z",
"end_reason": "completed",
"ended_by": "alice",
"duration_seconds": 32,
"version": 5,
"ended_via": "client",
"member_count": 3,
"peak_joined": 3,
"billable_minutes": 3
}
],
"next_cursor": "100313666666102784"
}Use next_cursor to tell whether there are more pages
Each request checks at most 1000 calls. With conditions that rarely match (such as filtering by type, or by both status and username), a page may have fewer than limit items or even none while next_cursor isn't empty; keep paging with it until next_cursor is an empty string. Don't judge whether you're done by the number of items on a page.
Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | Invalid parameter, named in details.field: type isn't single or group, status isn't ringing, active or ended, start_time or end_time isn't RFC 3339, group_id isn't a group ID, limit isn't between 1 and 100, or cursor is invalid |
Get call details
Gets one call by ID, including all members. Call IDs come from the call list, call record messages in conversations, or event callbacks.
/{org_name}/{app_name}/calls/{call_id}Path parameters
For org_name and app_name, see Server overview.
| Parameter | Type | Description |
|---|---|---|
call_id | String | Call ID |
Example request
curl "$IM_API/$ORG/$APP/calls/100313814330769408" \
-H "Authorization: Bearer $APP_TOKEN"Response
Returns 200 OK with a call object. An ended one-to-one audio call:
{
"call_id": "100313814330769408",
"type": "single",
"group_id": null,
"media": "audio",
"status": "ended",
"initiator": "bob",
"max_participants": 2,
"joined_count": 0,
"ext": null,
"created_at": "2026-10-04T19:31:17.077Z",
"answered_at": "2026-10-04T19:31:21.115Z",
"ended_at": "2026-10-04T19:31:49.168Z",
"end_reason": "completed",
"ended_by": "bob",
"duration_seconds": 28,
"version": 3,
"members": [
{
"username": "bob",
"role": "initiator",
"state": "left",
"media_uid": 1,
"invited_by": null,
"ring_expires_at": null,
"joined_at": "2026-10-04T19:31:17.077Z",
"left_at": "2026-10-04T19:31:49.168Z",
"leave_reason": "hangup",
"invited_at": null,
"joined_platform": "android",
"join_count": 1,
"joined_ms": 28053,
"first_joined_at": "2026-10-04T19:31:17.077Z",
"media_joined_at": "2026-10-04T19:31:17.094Z",
"media_left_at": null
},
{
"username": "alice",
"role": "invitee",
"state": "left",
"media_uid": 2,
"invited_by": "bob",
"ring_expires_at": "2026-10-04T19:32:17.077Z",
"joined_at": "2026-10-04T19:31:21.115Z",
"left_at": "2026-10-04T19:31:49.168Z",
"leave_reason": "ended",
"invited_at": "2026-10-04T19:31:17.077Z",
"joined_platform": "ios",
"join_count": 1,
"joined_ms": 28053,
"first_joined_at": "2026-10-04T19:31:21.115Z",
"media_joined_at": "2026-10-04T19:31:21.131Z",
"media_left_at": null
}
],
"ended_via": "client",
"member_count": 2,
"peak_joined": 2,
"billable_minutes": 2
}Errors
| HTTP status | code | Description |
|---|---|---|
| 404 | not_found | The call doesn't exist, doesn't belong to this app, or is past the 180-day retention |
End a call
Ends an ongoing call, for example when you find a violation or the business service time is up. After ending:
- All members in the call leave (
leave_reasonended), ringing invitations are canceled, and incoming call notifications on callees' devices are replaced with "missed call"; - The call's
end_reasonisserver_ended,ended_viaisopenapi, andended_byisnull; - Members' online devices get the call-ended notification, the media room closes, and devices still connected are disconnected;
- As with calls ending for other reasons, a call record is written to the conversation and the
rtc.call_endedevent callback is sent.
If the call has already ended, returns 200 OK with changed false and the call unchanged, so retrying is safe.
/{org_name}/{app_name}/calls/{call_id}/endPath parameters
For org_name and app_name, see Server overview.
| Parameter | Type | Description |
|---|---|---|
call_id | String | Call ID |
Request body
The body can be omitted.
| Field | Type | Required | Description |
|---|---|---|---|
reason | String | No | Why it was ended, 1 to 256 characters with surrounding whitespace trimmed. Recorded in the console's audit log; not shown to users |
Example request
curl -X POST "$IM_API/$ORG/$APP/calls/100311615936659456/end" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reason": "会议已结束"
}'Response
Returns 200 OK:
| Field | Type | Description |
|---|---|---|
call | Object | The call object after ending |
changed | Boolean | Whether the call was ended by this request. false if it had already ended |
A group audio call being ended, where dave had earlier been removed from the call:
{
"call": {
"call_id": "100311615936659456",
"type": "group",
"group_id": "100311544667045888",
"media": "audio",
"status": "ended",
"initiator": "alice",
"max_participants": 16,
"joined_count": 0,
"ext": {
"topic": "周会"
},
"created_at": "2026-10-04T19:22:32.940Z",
"answered_at": "2026-10-04T19:22:33.988Z",
"ended_at": "2026-10-04T19:22:38.122Z",
"end_reason": "server_ended",
"ended_by": null,
"duration_seconds": 4,
"version": 6,
"members": [
{
"username": "alice",
"role": "initiator",
"state": "left",
"media_uid": 1,
"invited_by": null,
"ring_expires_at": null,
"joined_at": "2026-10-04T19:22:32.940Z",
"left_at": "2026-10-04T19:22:38.122Z",
"leave_reason": "ended",
"invited_at": null,
"joined_platform": "ios",
"join_count": 1,
"joined_ms": 4134,
"first_joined_at": "2026-10-04T19:22:32.940Z",
"media_joined_at": "2026-10-04T19:22:34.045Z",
"media_left_at": null
},
{
"username": "bob",
"role": "invitee",
"state": "left",
"media_uid": 2,
"invited_by": "alice",
"ring_expires_at": "2026-10-04T19:23:32.940Z",
"joined_at": "2026-10-04T19:22:33.988Z",
"left_at": "2026-10-04T19:22:38.122Z",
"leave_reason": "ended",
"invited_at": "2026-10-04T19:22:32.940Z",
"joined_platform": "android",
"join_count": 1,
"joined_ms": 4134,
"first_joined_at": "2026-10-04T19:22:33.988Z",
"media_joined_at": "2026-10-04T19:22:34.056Z",
"media_left_at": null
},
{
"username": "carol",
"role": "invitee",
"state": "declined",
"media_uid": 3,
"invited_by": "alice",
"ring_expires_at": "2026-10-04T19:23:32.940Z",
"joined_at": null,
"left_at": null,
"leave_reason": null,
"invited_at": "2026-10-04T19:22:32.940Z",
"joined_platform": null,
"join_count": 0,
"joined_ms": 0,
"first_joined_at": null,
"media_joined_at": null,
"media_left_at": null
},
{
"username": "dave",
"role": "joiner",
"state": "left",
"media_uid": 4,
"invited_by": null,
"ring_expires_at": null,
"joined_at": "2026-10-04T19:22:34.030Z",
"left_at": "2026-10-04T19:22:37.089Z",
"leave_reason": "removed",
"invited_at": null,
"joined_platform": "ios",
"join_count": 1,
"joined_ms": 3059,
"first_joined_at": "2026-10-04T19:22:34.030Z",
"media_joined_at": "2026-10-04T19:22:34.067Z",
"media_left_at": null
}
],
"ended_via": "openapi",
"member_count": 4,
"peak_joined": 3,
"billable_minutes": 3
},
"changed": true
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | reason is longer than 256 characters; details.field is reason |
| 404 | not_found | The call doesn't exist, doesn't belong to this app, or is past the 180-day retention |
Remove a member from a call
Removes one member from a call, for dealing with individual members in group calls:
- Members in the call: leave the call (
leave_reasonremoved) and are removed from the media room, so they can't keep listening with the media credential they hold; - Ringing members: the invitation is canceled (
statecanceled) and ringing stops; - Other members get a member-change notification, and the removed person gets a notification that they left or their invitation was canceled;
- If removal leaves nobody in the call, the call ends:
end_reasoniscompletedif it ever connected,canceledif not; if only one person remains with no ringing invitations, it ends after 60 seconds, see Group calls; - One-to-one calls: removing either side is the same as ending the call, with
end_reasonserver_ended.
A removed member can rejoin the group call as long as they can still speak in the group. To stop them rejoining, also remove them from the group or mute them in the group.
If the member is no longer in the call and not ringing (such as having left or declined), or the call has ended, returns 200 OK with changed false.
/{org_name}/{app_name}/calls/{call_id}/members/{username}/removePath parameters
For org_name and app_name, see Server overview.
| Parameter | Type | Description |
|---|---|---|
call_id | String | Call ID |
username | String | Username of the member to remove |
Request body
The body can be omitted.
| Field | Type | Required | Description |
|---|---|---|---|
reason | String | No | Why they were removed, 1 to 256 characters with surrounding whitespace trimmed. Recorded in the console's audit log; not shown to users |
Example request
curl -X POST "$IM_API/$ORG/$APP/calls/100311615936659456/members/dave/remove" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reason": "违规发言,移出通话"
}'Response
Returns 200 OK in the same format as End a call: call is the call object after removal, and changed says whether anything changed.
{
"call": {
"call_id": "100311615936659456",
"type": "group",
"group_id": "100311544667045888",
"media": "audio",
"status": "active",
"initiator": "alice",
"max_participants": 16,
"joined_count": 2,
"ext": {
"topic": "周会"
},
"created_at": "2026-10-04T19:22:32.940Z",
"answered_at": "2026-10-04T19:22:33.988Z",
"ended_at": null,
"end_reason": null,
"ended_by": null,
"duration_seconds": 3,
"version": 5,
"members": [
{
"username": "alice",
"role": "initiator",
"state": "joined",
"media_uid": 1,
"invited_by": null,
"ring_expires_at": null,
"joined_at": "2026-10-04T19:22:32.940Z",
"left_at": null,
"leave_reason": null,
"invited_at": null,
"joined_platform": "ios",
"join_count": 1,
"joined_ms": 0,
"first_joined_at": "2026-10-04T19:22:32.940Z",
"media_joined_at": "2026-10-04T19:22:34.045Z",
"media_left_at": null
},
{
"username": "bob",
"role": "invitee",
"state": "joined",
"media_uid": 2,
"invited_by": "alice",
"ring_expires_at": "2026-10-04T19:23:32.940Z",
"joined_at": "2026-10-04T19:22:33.988Z",
"left_at": null,
"leave_reason": null,
"invited_at": "2026-10-04T19:22:32.940Z",
"joined_platform": "android",
"join_count": 1,
"joined_ms": 0,
"first_joined_at": "2026-10-04T19:22:33.988Z",
"media_joined_at": "2026-10-04T19:22:34.056Z",
"media_left_at": null
},
{
"username": "carol",
"role": "invitee",
"state": "declined",
"media_uid": 3,
"invited_by": "alice",
"ring_expires_at": "2026-10-04T19:23:32.940Z",
"joined_at": null,
"left_at": null,
"leave_reason": null,
"invited_at": "2026-10-04T19:22:32.940Z",
"joined_platform": null,
"join_count": 0,
"joined_ms": 0,
"first_joined_at": null,
"media_joined_at": null,
"media_left_at": null
},
{
"username": "dave",
"role": "joiner",
"state": "left",
"media_uid": 4,
"invited_by": null,
"ring_expires_at": null,
"joined_at": "2026-10-04T19:22:34.030Z",
"left_at": "2026-10-04T19:22:37.089Z",
"leave_reason": "removed",
"invited_at": null,
"joined_platform": "ios",
"join_count": 1,
"joined_ms": 3059,
"first_joined_at": "2026-10-04T19:22:34.030Z",
"media_joined_at": "2026-10-04T19:22:34.067Z",
"media_left_at": null
}
],
"ended_via": null,
"member_count": 4,
"peak_joined": 3,
"billable_minutes": 0
},
"changed": true
}Members still in the call (alice, bob) have joined_ms 0 here because their time is added when they leave.
Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | reason is longer than 256 characters; details.field is reason |
| 404 | not_found | The call doesn't exist, doesn't belong to this app, or is past the 180-day retention ("call not found"); or the user doesn't exist or isn't a member of this call ("user not found") |
Get a user's ongoing calls
Gets the call a user is in right now and their ringing incoming calls, for example to show whether a support agent is on a call, or to confirm a user isn't in a call before a business operation.
/{org_name}/{app_name}/users/{username}/calls/activePath parameters
For org_name and app_name, see Server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | Username |
Example request
curl "$IM_API/$ORG/$APP/users/bob/calls/active" \
-H "Authorization: Bearer $APP_TOKEN"Response
Returns 200 OK:
| Field | Type | Description |
|---|---|---|
current | Object | The user's ongoing call, that is a call they're in or one they started and are waiting to be answered, as a call object with members; null if not in a call |
incoming | Array<Object> | The user's ringing incoming calls that haven't timed out, at most 20, each a call object with members; an empty array if none |
A user receiving a one-to-one video call:
{
"current": null,
"incoming": [
{
"call_id": "100311142651396096",
"type": "single",
"group_id": null,
"media": "video",
"status": "ringing",
"initiator": "alice",
"max_participants": 2,
"joined_count": 1,
"ext": {
"order_id": "8812"
},
"created_at": "2026-10-04T19:20:40.099Z",
"answered_at": null,
"ended_at": null,
"end_reason": null,
"ended_by": null,
"duration_seconds": 0,
"version": 1,
"members": [
{
"username": "alice",
"role": "initiator",
"state": "joined",
"media_uid": 1,
"invited_by": null,
"ring_expires_at": null,
"joined_at": "2026-10-04T19:20:40.099Z",
"left_at": null,
"leave_reason": null,
"invited_at": null,
"joined_platform": "ios",
"join_count": 1,
"joined_ms": 0,
"first_joined_at": "2026-10-04T19:20:40.099Z",
"media_joined_at": null,
"media_left_at": null
},
{
"username": "bob",
"role": "invitee",
"state": "ringing",
"media_uid": 2,
"invited_by": "alice",
"ring_expires_at": "2026-10-04T19:21:40.099Z",
"joined_at": null,
"left_at": null,
"leave_reason": null,
"invited_at": "2026-10-04T19:20:40.099Z",
"joined_platform": null,
"join_count": 0,
"joined_ms": 0,
"first_joined_at": null,
"media_joined_at": null,
"media_left_at": null
}
],
"ended_via": null,
"member_count": 2,
"peak_joined": 1,
"billable_minutes": 0
}
]
}A user neither in a call nor with incoming calls:
{
"current": null,
"incoming": []
}Errors
| HTTP status | code | Description |
|---|---|---|
| 404 | not_found | The user doesn't exist or was deleted |
Get call statistics
Gets the app's daily call statistics over a date range, the same as on the console's "Audio/video" page. Days are calendar days in Beijing time, and a call counts on the day it ended; today's numbers update as calls end.
/{org_name}/{app_name}/rtc/statsPath parameters
For org_name and app_name, see Server overview.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | String | Yes | Start date, YYYY-MM-DD |
to | String | Yes | End date, YYYY-MM-DD, inclusive. Not before from, at most 92 days per query |
Example request
curl "$IM_API/$ORG/$APP/rtc/stats?from=2026-10-01&to=2026-10-05" \
-H "Authorization: Bearer $APP_TOKEN"Response
Returns 200 OK; items holds each day's statistics in ascending date order. Days without calls are omitted.
| Field | Type | Description |
|---|---|---|
items[].date | String | Date, YYYY-MM-DD |
items[].calls | Number | Calls that ended that day, including those ending immediately because the callee was busy |
items[].group_calls | Number | Of those, group calls |
items[].answered_calls | Number | Of those, calls that connected. Answer rate is answered_calls / calls |
items[].ended_completed | Number | Calls with end reason completed |
items[].ended_canceled | Number | Calls with end reason canceled |
items[].ended_rejected | Number | Calls with end reason rejected |
items[].ended_busy | Number | Calls with end reason busy |
items[].ended_no_answer | Number | Calls with end reason no_answer |
items[].ended_connection_lost | Number | Calls with end reason connection_lost |
items[].ended_other | Number | Calls with other end reasons, such as server_ended, user_unavailable, app_unavailable |
items[].duration_ms | Number | Total duration of connected calls (connect to end), in milliseconds |
items[].audio_participant_ms | Number | Total time members spent in audio calls, the sum of members' joined_ms, in milliseconds |
items[].video_participant_ms | Number | Total time members spent in video calls, in milliseconds |
items[].billable_audio_minutes | Number | Total billable minutes of audio calls |
items[].billable_video_minutes | Number | Total billable minutes of video calls |
items[].answer_wait_ms | Number | Total wait from start to connect for connected calls, in milliseconds. Average wait is answer_wait_ms / answered_calls |
items[].joined_members | Number | Member joins in connected calls |
items[].media_missing_members | Number | Of those, joins that never connected to the media service. If this is a noticeably high share of joined_members, some users' networks may not reach the media service; contact us to investigate |
items[].peak_concurrent_calls | Number | The day's peak of simultaneous calls, sampled once a minute, so very short calls may be missed |
items[].peak_at | String | When the peak occurred, null if none was sampled |
{
"items": [
{
"date": "2026-10-05",
"calls": 18,
"group_calls": 3,
"answered_calls": 10,
"ended_completed": 5,
"ended_canceled": 4,
"ended_rejected": 1,
"ended_busy": 2,
"ended_no_answer": 1,
"ended_connection_lost": 1,
"ended_other": 4,
"duration_ms": 117736,
"audio_participant_ms": 69425,
"video_participant_ms": 184133,
"billable_audio_minutes": 13,
"billable_video_minutes": 9,
"answer_wait_ms": 11317,
"joined_members": 22,
"media_missing_members": 12,
"peak_concurrent_calls": 2,
"peak_at": "2026-10-04T19:23:22.358Z"
}
]
}date follows Beijing time while times such as peak_at are UTC, so the peak for 2026-10-05 in the example shows as 2026-10-04T19:23:22.358Z (03:23 on October 5 in Beijing time).
Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | details.reason is invalid_date_range: from or to is missing, isn't YYYY-MM-DD, to is before from, or the range exceeds 92 days |
Standalone channel APIs
This page only manages calls identified by call_id. For meetings and voice rooms identified by channel_id, use Channel management, Channel sessions and members and Channel usage and corrections; don't pass channel IDs to the APIs on this page.
Statistics versus actual charges
billable_minutes and the daily audio and video minutes in responses are only statistics, not amounts charged. New billing deducts the balance by the tier the media service actually received and the app's monthly total of standard duration, and never charges these fields again. When reconciling, check calls' end times, the day statistics are counted on, and the audio/video transactions separately.
