Event callbacks
When users, friends, groups, messages, chat rooms, calls, moderation or files change in the app, the IM service sends the change as an event to the callback addresses subscribed to it. This page lists every event type, when it's sent and the fields of data. For the common request format, responses and retries see Callbacks overview; for signature verification see Verify callback requests.
Every event's data has an origin field saying who caused it (client / server / platform / system), explained in origin: who caused the event and not repeated in the tables below. Times in the tables are UTC strings, users are usernames, and deleted users are null. Fields and event types will be added, so ignore ones you don't recognize.
Subscribing to events
Choose the events each callback address receives in the console, see Callback configuration. Each item is one of these, up to 50:
- A full event type, such as
friend.added. {prefix}.*, such asgroup.*, subscribing to every event in that category, including ones added later. The prefix is the part of the event type before the first dot:user.*includes sign-in events likeuser.session_created;friend.*doesn't includefriend_request.*orblacklist.changed;message.*includes high-volume message deliverymessage.sent, andchatroom.*includes chat room message deliverychatroom.message_sent, so if you only need recalls, edits or chat room management changes, write the full event types.
Unknown types and prefixes return 400 invalid_argument on saving, with details.reason unknown_event_type and the unknown item in details.event_type.
Filters
Each callback address can set these filters; events not passing them aren't sent to that address:
| Filter | Default | Description |
|---|---|---|
Skip events caused by the tenant's server (skip_server_initiated) | Off | When on, events with origin server aren't received, i.e. actions by your server through OpenAPI or by you in the console, such as friends added, groups created and messages sent by the server. Exception: the recall events message.recalled and chatroom.message_recalled are still received, because businesses keeping message copies must learn about every recall; presence.changed has no origin and isn't affected |
Skip events cascading from account deletion (skip_user_deletion_cascade) | Off | Deleting a user produces a friend.removed for each of their friends and a group.members_removed for each of their groups, all with reason user_deleted. You already learn about the deletion from user.status_changed, so turning this on stops them one by one |
Message conversation types (message_conversation_types) | One-to-one and group | Only affects message.* events; receive only one-to-one or only group messages |
Include group notices (message_include_tips) | Off | Group notices (such as "X joined the group") are message.sent with type tip, duplicating group events, so they aren't sent by default |
Unsubscribing and changing filters only affect later events; events already queued are sent as usual.
Event list
| Category | Event type | When it's sent |
|---|---|---|
| Users | user.created | A user registers: client registration, or created by the server or console |
user.status_changed | A user is banned, unbanned (including when a ban ends) or deleted | |
user.profile_changed | A user's nickname, avatar or custom attributes change | |
user.mute_changed | A global mute is set or lifted | |
| Sign-in | user.session_created | A user signs in on a device |
user.sessions_revoked | Sign-in sessions are revoked: sign-out, kicked, replaced, password change, expiry and so on | |
user.relogin_required | Every user of the app is required to sign in again | |
| Presence | presence.changed | A user comes online, goes offline, or gains or loses an online platform (best effort) |
| Friends | friend.added | Two users become friends; one event for each |
friend.removed | A friendship ends; one event for each | |
friend.updated | A friend's remark or custom attributes change | |
friend_request.received | A friend request needing approval is received | |
friend_request.declined | A friend request is declined | |
blacklist.changed | Someone is blocked or unblocked | |
| Groups | group.created | A group is created |
group.info_changed | Group profile, settings, mute-all or owner changes, or the group is banned or unbanned | |
group.dismissed | A group is dissolved | |
group.members_added | Members join; one event per operation | |
group.members_removed | Members leave, are removed, are put on the group block list or have their accounts deleted | |
group.members_updated | Members' roles, group nicknames, member attributes or mutes change | |
group.request_created | A new join application or invitation | |
group.request_handled | A join application or invitation is accepted, declined, withdrawn or lapses | |
| Messages | message.sent | Message delivery: one event for every one-to-one or group message written |
message.recalled | A message is recalled | |
message.edited | A message is edited | |
| Chat rooms | chatroom.created | A chat room is created |
chatroom.info_changed | A chat room's profile, settings, owner or admins change, or it's banned or unbanned | |
chatroom.dismissed | A chat room is dissolved | |
chatroom.members_muted, chatroom.members_unmuted | Members are muted or unmuted | |
chatroom.members_kicked | Members are removed | |
chatroom.members_banned, chatroom.members_unbanned | Members are banned or unbanned | |
chatroom.allowlist_changed | The allowlist changes | |
chatroom.attributes_changed | A chat room's attributes change | |
chatroom.message_sent | Chat room message delivery (best effort) | |
chatroom.message_recalled | A chat room message is recalled (best effort) | |
| Audio and video | rtc.call_created | A call is started |
rtc.call_answered | A call connects | |
rtc.member_joined | A member joins a group call | |
rtc.member_left | A member leaves a group call | |
rtc.call_ended | A call ends, with durations | |
| Standalone channels | rtc_channel.created | A channel is created |
rtc_channel.updated | A channel's profile is updated | |
rtc_channel.closing | A channel starts closing | |
rtc_channel.closed | A channel finishes closing | |
rtc_channel.session_joined | A session is confirmed as joined | |
rtc_channel.session_leaving | A session starts leaving | |
rtc_channel.session_left | A session finishes leaving | |
rtc_channel.member_banned | A channel ban changes | |
rtc_channel.member_unbanned | A channel ban is lifted | |
| Moderation | moderation.item_created | A new item to review |
moderation.item_decided | A review item gets a decision | |
moderation.report_created | A user files a report | |
moderation.violation_recorded | A violation is recorded, possibly triggering an automatic penalty | |
| Files | media.file_uploaded | A file finishes uploading |
media.file_blocked | A file is blocked | |
media.file_unblocked | A file is unblocked | |
| AI conversations | ai.message_received | A user sent a one-to-one message to an enabled bot |
ai.stream_finished | An AI reply ended (whatever the result) | |
ai.stream_stopped | A user tapped stop generating |
There are no callbacks for friend requests being marked as read, chat room members entering and leaving, message read receipts or reactions.
Users
user.created
Sent after a user registers, including client registration, your server calling Create a user, and creation in the console.
| Field | Type | Description |
|---|---|---|
username | String | The username |
created_via | String | Entry point: client client registration, openapi created by the server, console created in the console |
{
"origin": "server",
"username": "alice",
"created_via": "openapi"
}user.status_changed
Sent after a user is banned, unbanned (including automatically when a ban ends) or deleted. Banning and deleting also end the user's sessions, without a separate user.sessions_revoked.
| Field | Type | Description |
|---|---|---|
username | String | The username; the username before deletion when deleted |
status | String | The new status: active (unbanned), disabled banned, deleted |
{
"origin": "server",
"username": "erin",
"status": "disabled"
}user.profile_changed
Sent after a user's nickname, avatar or custom attributes change, from a client or the server. The event doesn't include the changes; read them with Get a user if needed.
| Field | Type | Description |
|---|---|---|
username | String | The username |
version | Number | The profile version after the change; only re-read when it's greater than the one you saved |
{
"origin": "client",
"username": "bob",
"version": 2
}user.mute_changed
Sent after a global mute is set or lifted, one event per scope. Not sent when a mute ends automatically.
| Field | Type | Description |
|---|---|---|
username | String | The username |
scope | String | The scope: chat one-to-one, group groups, room chat rooms |
muted | Boolean | true muted, false unmuted |
expires_at | String | When the mute ends; null for permanent mutes and when unmuting |
{
"origin": "server",
"username": "bob",
"scope": "chat",
"muted": true,
"expires_at": "2026-10-04T20:22:07.207Z"
}Sign-in
user.session_created
Sent after a user signs in on a device.
| Field | Type | Description |
|---|---|---|
username | String | The username |
session_id | String | The sign-in session ID, the same as in List devices |
device_id | String | Device ID |
device_name | String | Device name; empty string if not given |
platform | String | Platform, such as ios, android or web |
login_method | String | password or ticket |
created_at | String | Sign-in time, with six microsecond digits, such as 2026-10-04T19:21:11.735215Z |
{
"origin": "client",
"username": "alice",
"session_id": "100311275329814528",
"device_id": "5f0c2a9e-7d1b-4c7e-9a51-2f6d0f3b8c11",
"device_name": "iPhone 15",
"platform": "ios",
"login_method": "password",
"created_at": "2026-10-04T19:21:11.735215Z"
}user.sessions_revoked
Sent after a user's sessions are revoked: signing out, the same device signing in again, being replaced over the device limit, kicked, password change, a refresh token reused, or expiry. Bans, user deletion and requiring everyone to sign in again don't send one per session; see user.status_changed and user.relogin_required instead.
| Field | Type | Description |
|---|---|---|
username | String | The username |
session_ids | Array<String> | Revoked session IDs. An empty array means every session of this user signed in before revoked_before; don't also revoke sessions signed in after it |
revoked_before | String | The revocation cutoff, six microsecond digits |
reason | String | logout signed out, or replaced, device_limit, kicked, password_changed, token_reuse, expired, see Sign-out reasons |
by_device_name | String | For replaced or device_limit, the name of the new device that replaced these sessions; null otherwise |
by_platform | String | As above, the new device's platform |
{
"origin": "client",
"username": "alice",
"session_ids": ["100315151017705472"],
"revoked_before": "2026-10-04T19:36:35.885537Z",
"reason": "replaced",
"by_device_name": "iPhone 15 Pro",
"by_platform": "ios"
}When the server signs out all devices, session_ids is empty:
{
"origin": "server",
"username": "frank",
"session_ids": [],
"revoked_before": "2026-10-04T19:22:07.496243Z",
"reason": "kicked",
"by_device_name": null,
"by_platform": null
}user.relogin_required
Sent once, not per user, after the console requires every user of the app to sign in again.
| Field | Type | Description |
|---|---|---|
sessions_revoked_before | String | Every session signed in before this is invalid; six microsecond digits |
{
"origin": "server",
"sessions_revoked_before": "2026-10-04T19:36:25.378490Z"
}Presence
presence.changed
Sent when a user's set of online platforms changes: going from offline to online, from online to offline, or gaining or losing a platform while online (such as signing in on a computer while the phone is online). A second device on the same platform coming online, or the same session reconnecting after a network switch, doesn't change the platforms and isn't sent. For what online means, see Presence.
This event is best effort (see Best-effort events) and not ordered, so use version to tell new from old and ignore events not greater than the version you processed. When a device loses its network abruptly, the server only notices after up to about 90 seconds (about 6 minutes with the app in the background), showing it online until then. Volume can be high, so check your receiver's capacity before subscribing and turn on batching. The event doesn't depend on whether the app enables client presence.
| Field | Type | Description |
|---|---|---|
username | String | The username |
online | Boolean | Whether online after the change |
platforms | Array<String> | Online platforms after the change, sorted alphabetically; empty when offline |
version | Number | The version; order a user's events by it |
last_seen_at | String | When they last went offline; null while online |
cause | String | What triggered it: connect, disconnect, restore, or cleanup (the server cleaning up dead connections) |
session_id | String | The sign-in session that triggered it; null when cause is cleanup |
platform | String | The triggering device's platform; null when cause is cleanup |
reason | String | Why it disconnected, only when cause is disconnect, such as client_closed, network_error, idle_timeout (heartbeat timeout), connection_replaced (replaced by a new connection from the same device), session_revoked; null otherwise |
{
"origin": null,
"username": "bob",
"online": false,
"platforms": [],
"version": 1791141956256,
"last_seen_at": "2026-10-04T19:25:56.256Z",
"cause": "disconnect",
"session_id": "100311275719884800",
"platform": "android",
"reason": "client_closed"
}Friends
Friend and block list events describe changes to one user's (username's) list, with version being that user's friend list or block list version after the change. When two users become friends or stop being friends, each gets an event. For the APIs see Friends and Friend requests.
friend.added
Sent to each side after two users become friends.
| Field | Type | Description |
|---|---|---|
username | String | The friend list's owner |
friend_username | String | The new friend |
friend_nickname | String | The friend's nickname at the time |
friend_avatar_url | String | The friend's avatar address at the time |
remark | String | The owner's remark for the friend |
attributes | Object | The friend's custom attributes; null if none |
add_source | String | How they met, the add_source from the request; empty string if none |
source | String | How the friendship formed (from the owner's view): request_sent the owner's request was accepted; request_auto the owner's request made them friends directly (the other user lets anyone add them, or had already requested the owner); request_accepted the owner accepted the other's request; auto_accepted the owner lets anyone add them and the other's request was accepted automatically; server added by your server or the console |
created_at | String | When the friendship formed, the same for both |
version | Number | The owner's friend list version |
The event bob receives after accepting alice's request:
{
"origin": "client",
"username": "bob",
"friend_username": "alice",
"friend_nickname": "爱丽丝",
"friend_avatar_url": "",
"remark": "爱丽丝",
"attributes": null,
"add_source": "search",
"source": "request_accepted",
"created_at": "2026-10-04T19:22:42.446Z",
"version": 1
}friend.removed
Sent to each side after a friendship ends; when one account is deleted, sent to the remaining side.
| Field | Type | Description |
|---|---|---|
username | String | The friend list's owner |
friend_username | String | The removed friend; may be null |
friend_created_at | String | When this friendship formed, the same as created_at in friend.added, to tell apart several friendships between the same two people over time |
reason | String | self the owner removed it, peer the other removed it, server your server or console, user_deleted the other's account was deleted |
version | Number | The owner's friend list version |
{
"origin": "client",
"username": "alice",
"friend_username": "bob",
"friend_created_at": "2026-10-04T19:22:42.446Z",
"reason": "self",
"version": 4
}Events cascading from account deletion have origin system:
{
"origin": "system",
"username": "alice",
"friend_username": "gone",
"friend_created_at": "2026-10-04T19:26:37.099Z",
"reason": "user_deleted",
"version": 6
}friend.updated
Sent after the owner changes a friend's remark or custom attributes.
| Field | Type | Description |
|---|---|---|
username | String | The friend list's owner |
friend_username | String | The friend |
remark | String | The remark after the change |
attributes | Object | The custom attributes after the change; null if none |
version | Number | The owner's friend list version |
{
"origin": "client",
"username": "alice",
"friend_username": "bob",
"remark": "老鲍",
"attributes": { "tag": "同事" },
"version": 3
}friend_request.received
Sent after a user sends or resends a friend request that needs approval. Requests that make them friends directly (the other lets anyone add them, or they requested each other) only send friend.added.
| Field | Type | Description |
|---|---|---|
from_username | String | The requester |
to_username | String | The person requested |
message | String | The message, after moderation |
add_source | String | How they met; empty string if none |
requested_at | String | When requested |
{
"origin": "client",
"from_username": "alice",
"to_username": "bob",
"message": "我是爱丽丝",
"add_source": "search",
"requested_at": "2026-10-04T19:22:42.403Z"
}friend_request.declined
Sent after the person requested declines; declining again isn't sent. Accepted requests send friend.added.
| Field | Type | Description |
|---|---|---|
from_username | String | The requester |
to_username | String | The person requested, who declined |
handled_at | String | When declined |
{
"origin": "client",
"from_username": "carol",
"to_username": "alice",
"handled_at": "2026-10-04T19:22:42.604Z"
}blacklist.changed
Sent after a user blocks or unblocks someone. Not sent when deleted users are removed from others' block lists.
| Field | Type | Description |
|---|---|---|
username | String | The block list's owner |
target_username | String | The user blocked or unblocked |
blocked | Boolean | true blocked, false unblocked |
created_at | String | When blocked; null when unblocking |
version | Number | The owner's block list version |
{
"origin": "client",
"username": "alice",
"target_username": "dave",
"blocked": true,
"created_at": "2026-10-04T19:22:53.474Z",
"version": 1
}Groups
Group events carry the versions after the change, info_version for the group profile and member_version for the member list; ignore events whose version isn't greater than what you've processed. operator is the user who did it; null means your server, the console, the platform or the system, told apart by origin. For the APIs see Group management and Group members.
group.created
Sent after a group is created. Initial members who join at creation get a separate group.members_added (not including the owner).
| Field | Type | Description |
|---|---|---|
group_id | String | The group ID |
type | String | private or public |
owner_username | String | The owner |
created_via | String | Entry point: client, openapi, console |
created_at | String | When created |
{
"origin": "client",
"group_id": "100311903108071424",
"type": "private",
"owner_username": "alice",
"created_via": "client",
"created_at": "2026-10-04T19:23:41.409Z"
}group.info_changed
Sent after the group profile, settings or mute-all change, ownership transfers, or the group is banned or unbanned.
| Field | Type | Description |
|---|---|---|
group_id | String | The group ID |
info_version | Number | The group profile version after the change |
changed | Array<String> | The changed fields, such as name, announcement, mute_all, owner, status |
group | Object | The full group info after the change, with the same fields as the group object, without self |
operator | String | Who did it; may be null |
{
"origin": "client",
"group_id": "100311903108071424",
"info_version": 2,
"changed": ["name", "announcement"],
"group": {
"group_id": "100311903108071424",
"type": "private",
"name": "项目组(二期)",
"avatar_url": "",
"description": "",
"announcement": "周五评审",
"announcement_updated_at": "2026-10-04T19:24:16.057Z",
"announcement_updated_by": "alice",
"attributes": null,
"owner": "alice",
"member_invite": "free",
"join_mode": null,
"mute_all": false,
"max_members": 500,
"max_members_setting": null,
"member_count": 4,
"status": "active",
"disabled_by": null,
"info_version": 2,
"member_version": 4,
"created_at": "2026-10-04T19:23:41.409Z"
},
"operator": "alice"
}group.dismissed
Sent after a group is dissolved.
| Field | Type | Description |
|---|---|---|
group_id | String | The group ID |
info_version | Number | The group profile version after dissolution |
dismissed_by | String | Who dissolved it: owner, tenant your server or console, platform, system |
dismissed_at | String | When dissolved |
{
"origin": "server",
"group_id": "100311964139388928",
"info_version": 2,
"dismissed_by": "tenant",
"dismissed_at": "2026-10-04T19:26:39.228Z"
}group.members_added
Sent after members join. One operation (such as inviting several people at once) sends one event, with everyone who joined in members.
| Field | Type | Description |
|---|---|---|
group_id | String | The group ID |
member_version | Number | The member list version after the change |
member_count | Number | The group size after the change |
members | Array<Object> | Who joined, see below |
operator | String | Who did it: the inviter, the owner or admin who approved, or the person joining through an invite link; may be null |
Each item in members:
| Field | Type | Description |
|---|---|---|
username | String | The member |
role | String | owner, admin or member |
joined_via | String | How they joined: create at creation, invite, apply, link invite link or QR code, server added by your server or console |
inviter | String | The inviter, or null; for invite links, who created the link |
joined_at | String | When they joined |
{
"origin": "client",
"group_id": "100311903108071424",
"member_version": 3,
"member_count": 3,
"members": [
{
"username": "bob",
"role": "member",
"joined_via": "invite",
"inviter": "carol",
"joined_at": "2026-10-04T19:23:55.911Z"
}
],
"operator": "carol"
}group.members_removed
Sent after members leave, are removed, are put on the group block list or have their accounts deleted; one event per operation.
| Field | Type | Description |
|---|---|---|
group_id | String | The group ID |
member_version | Number | The member list version after the change |
member_count | Number | The group size after the change |
members | Array<Object> | Who left, each with username and joined_at (when they joined, to tell apart the same person joining several times) |
reason | String | left, removed, blacklisted put on the group block list, user_deleted account deleted |
operator | String | Who did it; may be null |
{
"origin": "client",
"group_id": "100311903108071424",
"member_version": 7,
"member_count": 3,
"members": [
{ "username": "bob", "joined_at": "2026-10-04T19:23:55.911Z" }
],
"reason": "removed",
"operator": "alice"
}group.members_updated
Sent after members' roles, group nicknames, member attributes or mutes change. Ownership transfers list both the old and new owners.
| Field | Type | Description |
|---|---|---|
group_id | String | The group ID |
member_version | Number | The member list version after the change |
members | Array<Object> | Member info after the change, each with username, role, group_nickname, attributes, muted, muted_until, joined_via, inviter, joined_at, as in the group member object |
changed | Array<String> | What changed: role, group_nickname, attributes, mute |
operator | String | Who did it; may be null |
{
"origin": "client",
"group_id": "100311903108071424",
"member_version": 6,
"members": [
{
"username": "bob",
"role": "member",
"group_nickname": "",
"attributes": null,
"muted": true,
"muted_until": "2026-10-04T19:34:16.137Z",
"joined_via": "invite",
"inviter": "carol",
"joined_at": "2026-10-04T19:23:55.911Z"
}
],
"changed": ["mute"],
"operator": "alice"
}group.request_created
Sent when there's a new pending join application or invitation. Applications and invitations that join directly only send group.members_added.
| Field | Type | Description |
|---|---|---|
group_id | String | The group ID |
kind | String | application a user applying to a public group, approved by the owner or admins; invite_review a regular member inviting someone in a group that needs approval, approved by the owner or admins; invitation an invitation waiting for the invitee |
username | String | The user joining |
inviter_username | String | The inviter; null for application |
message | String | The application reason or invitation message |
requested_at | String | When applied or invited |
{
"origin": "client",
"group_id": "100311964139388928",
"kind": "application",
"username": "bob",
"inviter_username": null,
"message": "我是鲍勃,想加入",
"requested_at": "2026-10-04T19:24:05.593Z"
}group.request_handled
Sent after a join application or invitation is accepted, declined, withdrawn or lapses. Accepted ones that join also send group.members_added.
| Field | Type | Description |
|---|---|---|
group_id | String | The group ID |
kind | String | As in group.request_created |
username | String | The user joining |
inviter_username | String | The inviter; may be null |
status | String | accepted, declined, canceled withdrawn or lapsed, expired |
reason | String | The decline reason; empty string if none |
cancel_cause | String | When status is canceled: withdrawn, joined joined some other way, blacklisted put on the group block list, group_private the public group became private, not_eligible found unable to invite this person on approval, dismissed the group was dissolved; null otherwise |
handled_by_username | String | Who handled it; may be null |
{
"origin": "client",
"group_id": "100311964139388928",
"kind": "application",
"username": "bob",
"inviter_username": null,
"status": "accepted",
"reason": "",
"cancel_cause": null,
"handled_by_username": "alice"
}Messages
message.sent
Message delivery: once you subscribe to message.sent, every one-to-one and group message written sends an event, including messages users send from clients, messages your server and console send, group messages sent as the system, and messages the system writes (such as call records after calls). These aren't delivered:
- Group notices (
typetip), unless the address turns on "include group notices". - Online-only messages: not stored, and not delivered.
- Messages rejected by moderation or the before-send callback: never written, and not delivered.
A bot starting an AI reply is also delivered once, with type ai and status streaming in the body; no message.edited is sent when the reply ends, and the final result comes in ai.stream_finished.
| Field | Type | Description |
|---|---|---|
conversation_id | String | The conversation ID; the group ID for groups |
conversation_type | String | single or group |
seq | Number | The message's sequence in the conversation |
message_id | String | The message ID |
client_msg_id | String | The sender's deduplication ID; for server sends without one, the server-generated value |
sender_username | String | The sender; null for group messages sent as the system |
sender_type | String | user or system |
recipient_username | String | The one-to-one recipient; null for groups |
group_id | String | The group ID for groups; null for one-to-one |
type | String | The message type, such as text, image, custom, call, see Message format |
body | Object | The message body; null when content isn't available |
ext | Object | Extension fields; null if none |
mention_usernames | Array<String> | Mentioned members; empty if none |
mention_all | Boolean | Whether everyone is mentioned |
reply_seq | Number | The sequence of the replied-to message; null if not a reply |
need_receipt | Boolean | Whether group read receipts were requested |
exclude_from_unread | Boolean | Whether it's excluded from unread counts |
via | String | Entry point: client, openapi server, console, system written by the system |
created_at | String | When the message was written |
content_available | Boolean | Whether the content is available, usually true; see Content retention below |
{
"origin": "client",
"conversation_id": "100311903108071424",
"conversation_type": "group",
"seq": 10,
"message_id": "100312173556793344",
"client_msg_id": "c-0003",
"sender_username": "alice",
"sender_type": "user",
"recipient_username": null,
"group_id": "100311903108071424",
"type": "text",
"body": { "text": "明天上午十点评审 @卡罗尔" },
"ext": null,
"mention_usernames": ["carol"],
"mention_all": false,
"reply_seq": null,
"need_receipt": false,
"exclude_from_unread": false,
"via": "client",
"created_at": "2026-10-04T19:24:45.886Z",
"content_available": true
}The message in the event is the content as written, i.e. after moderation replacement and before-send callback rewriting. The event doesn't include the message's push options. Call records written by the system after calls have type call, via system and origin system:
{
"origin": "system",
"conversation_id": "100312173003145216",
"conversation_type": "single",
"seq": 10,
"message_id": "100315480782274560",
"client_msg_id": "sys_rtc_100315433877372928",
"sender_username": "alice",
"sender_type": "user",
"recipient_username": "carol",
"group_id": null,
"type": "call",
"body": {
"call_id": "100315433877372928",
"call_type": "single",
"media": "audio",
"result": "completed",
"duration_seconds": 2,
"started_at": "2026-10-04T19:37:43.207Z"
},
"ext": null,
"mention_usernames": [],
"mention_all": false,
"reply_seq": null,
"need_receipt": false,
"exclude_from_unread": true,
"via": "system",
"created_at": "2026-10-04T19:37:54.390Z",
"content_available": true
}Making the most of message delivery
- Volume: delivery volume equals the app's message volume, so an app with 30,000 messages a minute gets about 500 events a second. Use a separate callback address for it with batching on (50 to 100 events per batch), deduplicate by event ID and write to your storage in bulk.
- Recalls and edits: subscribe to
message.recalledandmessage.editedtoo. Businesses archiving messages should update their own copies on recall events: content delivered before the recall can't be taken back. - Filling gaps: messages aren't delivered while an address is paused for more than 24 hours, automatically disabled, at its backlog limit, or while the app is unavailable. For a complete archive, fill gaps by time range with Export the app's messages, deduplicating by
message_id.
Content retention
Once delivered, message content is cleared from the delivery queue; for failed deliveries, body and ext are cleared after 24 hours, keeping only IDs and the like. When redelivered later, the current content is looked up by message_id: if the message was recalled, erased or passed retention, body and ext are null, mention_usernames is empty and content_available is false. This keeps recalled content from lingering in the callback queue. The same applies to message.edited and chatroom.message_sent; chat room messages can't be found once cleared.
message.recalled
Sent after a message is recalled, including by the sender, owner or admins in clients, by your server or the console, and by moderation. Recall events aren't affected by "skip events caused by the tenant's server", so recalls by your server are sent too. The event doesn't include the message content.
| Field | Type | Description |
|---|---|---|
conversation_id | String | The conversation ID |
conversation_type | String | single or group |
seq | Number | The recalled message's sequence |
message_id | String | The recalled message's ID |
sender_username | String | The original sender |
recipient_username | String | The one-to-one recipient; null for groups |
group_id | String | The group ID for groups; null for one-to-one |
recalled_by_username | String | Who recalled it; null for the server, console and platform |
recalled_by_role | String | The recaller's role: sender, owner, admin, server your server or console, platform |
recalled_at | String | When recalled |
{
"origin": "client",
"conversation_id": "100312173003145216",
"conversation_type": "single",
"seq": 1,
"message_id": "100312173024116736",
"sender_username": "alice",
"recipient_username": "carol",
"group_id": null,
"recalled_by_username": "alice",
"recalled_by_role": "sender",
"recalled_at": "2026-10-04T19:25:06.077Z"
}message.edited
Sent after a message is edited, with the full edited content.
| Field | Type | Description |
|---|---|---|
conversation_id, conversation_type, seq, message_id, client_msg_id, sender_username, recipient_username, group_id, type | As in message.sent | |
body | Object | The edited message body |
ext | Object | The edited extension fields; null if none |
edited_at | String | When edited |
edit_count | Number | How many times the message has been edited |
via | String | Entry point: openapi or console |
content_available | Boolean | As in message.sent |
{
"origin": "server",
"conversation_id": "100312173003145216",
"conversation_type": "single",
"seq": 2,
"message_id": "100312173946863616",
"client_msg_id": "c-0004",
"sender_username": "alice",
"recipient_username": "carol",
"group_id": null,
"type": "text",
"body": { "text": "这条消息已被编辑" },
"ext": null,
"edited_at": "2026-10-04T19:25:06.125Z",
"edit_count": 1,
"via": "openapi",
"content_available": true
}Chat rooms
In chat room management events, operator is the user who did it (null for your server, the platform and the system) and operator_role is their role: owner, admin, member, server your server or console, platform, system. For the APIs see Chat room management. Members entering and leaving don't send events.
chatroom.created
Sent after a chat room is created.
| Field | Type | Description |
|---|---|---|
room_id | String | The chat room ID |
name | String | Name |
avatar_url | String | Avatar address |
owner_username | String | The owner; null if none |
max_members | Number | Size limit |
listed | Boolean | Whether it appears in the chat room list |
created_via | String | Entry point: client, openapi, console |
created_by_username | String | The user who created it from a client; null for the server and console |
created_at | String | When created |
{
"origin": "server",
"room_id": "100312409813549056",
"name": "发布会直播间",
"avatar_url": "",
"owner_username": "alice",
"max_members": 1000,
"listed": true,
"created_via": "openapi",
"created_by_username": null,
"created_at": "2026-10-04T19:25:42.214Z"
}chatroom.info_changed
Sent after a chat room's profile, settings, mute-all, owner or admins change, or it's banned or unbanned.
| Field | Type | Description |
|---|---|---|
room_id | String | The chat room ID |
info_version | Number | The profile version after the change |
changed | Array<String> | The changed fields |
room | Object | The chat room info after the change, with the same fields as the chat room object, without the count, self and other API-only fields |
operator, operator_role | String | Who did it, see above |
{
"origin": "server",
"room_id": "100312409813549056",
"info_version": 2,
"changed": ["announcement"],
"room": {
"room_id": "100312409813549056",
"name": "发布会直播间",
"description": "",
"avatar_url": "",
"announcement": "九点开始",
"announcement_updated_at": "2026-10-04T19:26:12.514Z",
"announcement_updated_by": null,
"owner": "alice",
"admins": [],
"max_members": 1000,
"mute_all": false,
"listed": true,
"status": "active",
"disabled_by": null,
"info_version": 2,
"attributes_version": 1,
"created_at": "2026-10-04T19:25:42.214Z"
},
"operator": null,
"operator_role": "server"
}chatroom.dismissed
Sent after a chat room is dissolved.
| Field | Type | Description |
|---|---|---|
room_id | String | The chat room ID |
dismissed_by | String | owner, tenant your server or console, platform |
operator | String | The user who dissolved it; may be null |
dismissed_at | String | When dissolved |
{
"origin": "server",
"room_id": "100312409813549056",
"dismissed_by": "tenant",
"operator": null,
"dismissed_at": "2026-10-04T19:26:26.838Z"
}Chat room member changes
chatroom.members_muted, chatroom.members_unmuted, chatroom.members_kicked, chatroom.members_banned and chatroom.members_unbanned are sent after members are muted, unmuted, removed, banned or unbanned, one event per operation; chatroom.allowlist_changed is sent after the allowlist changes.
| Field | Type | Description |
|---|---|---|
room_id | String | The chat room ID |
members | Array<Object> | The members involved, each with username; members_muted also has muted_until (when the mute ends, null for permanent), and members_banned expires_at (when the ban ends, null for permanent). Not in allowlist_changed |
added, removed | Array<Object> | Only in allowlist_changed: users added to and removed from the allowlist, each with username |
reason | String | Only in members_muted and members_banned: the reason |
operator, operator_role | String | Who did it, see above |
{
"origin": "server",
"room_id": "100312409813549056",
"members": [
{ "username": "carol", "muted_until": "2026-10-04T19:36:12.225Z" }
],
"reason": "刷屏",
"operator": null,
"operator_role": "server"
}{
"origin": "server",
"room_id": "100312409813549056",
"added": [{ "username": "dave" }],
"removed": [],
"operator": null,
"operator_role": "server"
}chatroom.attributes_changed
Sent after chat room attributes are set or deleted.
| Field | Type | Description |
|---|---|---|
room_id | String | The chat room ID |
attributes_version | Number | The attributes version after the change |
set | Object | Attributes set, keyed by name, each an object: value, owner_username who set it (null for the server), auto_delete whether it's deleted when the setter leaves, updated_at |
removed | Array<String> | Names of deleted attributes |
reason | String | set, removed, auto_deleted deleted after the setter left, owner_deleted the setter's account was deleted |
operator, operator_role | String | Who did it, see above |
{
"origin": "server",
"room_id": "100312409813549056",
"attributes_version": 1,
"set": {
"stage": {
"value": "q&a",
"owner_username": null,
"auto_delete": false,
"updated_at": "2026-10-04T19:26:12.470Z"
}
},
"removed": [],
"reason": "set",
"operator": null,
"operator_role": "server"
}chatroom.message_sent
Chat room message delivery: one event for every chat room message pushed out, including messages from clients, the server and the console. This event is best effort, see Best-effort events; dropped low-priority messages and rejected messages aren't delivered. origin follows the entry point: client for clients, server for the server and console, system for the system.
| Field | Type | Description |
|---|---|---|
room_id | String | The chat room ID |
message_id | String | The message ID |
client_msg_id | String | The sender's deduplication ID |
sender | String | The sender; null for system messages |
sender_type | String | user or system |
sender_role | String | The sender's room role, such as owner, admin, member |
type | String | The message type |
body | Object | The message body |
ext | Object | Extension fields; null if none |
priority | String | high, normal or low |
via | String | Entry point: client, openapi, console, system |
created_at | String | When sent |
content_available | Boolean | Whether the content is available, see Content retention |
{
"origin": "client",
"room_id": "100312409813549056",
"message_id": "100312464125591552",
"client_msg_id": "r-0001",
"sender": "bob",
"sender_type": "user",
"sender_role": "member",
"type": "text",
"body": { "text": "大家好" },
"ext": null,
"priority": "normal",
"via": "client",
"created_at": "2026-10-04T19:25:55.163Z",
"content_available": true
}chatroom.message_recalled
Sent after a chat room message is recalled, best effort; not affected by "skip events caused by the tenant's server". The event doesn't include the message content.
| Field | Type | Description |
|---|---|---|
room_id | String | The chat room ID |
message_id | String | The recalled message's ID |
recalled_by | String | Who recalled it; null for the server, platform and system |
role | String | The recaller's role: sender, owner, admin, server, platform |
recalled_at | String | When recalled |
sender | String | The original sender; null for system messages, or when the message is no longer among the room's recent messages |
{
"origin": "server",
"room_id": "100312409813549056",
"message_id": "100312464125591552",
"recalled_by": null,
"role": "server",
"recalled_at": "2026-10-04T19:26:26.811Z",
"sender": "bob"
}Audio and video
Call events are useful for tallying call durations, billing and reconciliation. For calls, see Audio and video calls. One-to-one and group calls both have rtc.call_created, rtc.call_answered and rtc.call_ended; group calls also have rtc.member_joined and rtc.member_left for each member joining and leaving. Every call has exactly one rtc.call_ended.
rtc.call_created
Sent after a call is started.
| Field | Type | Description |
|---|---|---|
call_id | String | The call ID |
type | String | single one-to-one call, group group call |
group_id | String | The group ID for group calls; null for one-to-one |
media | String | audio or video |
initiator_username | String | Who started it |
invitees | Array<Object> | Who was invited, each with username and state (ringing at this point) |
ext | Object | Extension fields given when starting; null if none |
created_at | String | When started |
ring_expires_at | String | For one-to-one calls, when ringing stops and the call ends if unanswered; null for group calls |
{
"origin": "client",
"call_id": "100315526282084352",
"type": "group",
"group_id": "100311903108071424",
"media": "video",
"initiator_username": "alice",
"invitees": [
{ "username": "carol", "state": "ringing" },
{ "username": "dave", "state": "ringing" }
],
"ext": null,
"created_at": "2026-10-04T19:38:05.238Z",
"ring_expires_at": null
}rtc.call_answered
Sent once when the first invitee answers and the call connects.
| Field | Type | Description |
|---|---|---|
call_id, type, group_id, media | As in rtc.call_created | |
answered_at | String | When connected |
answered_by_username | String | Who answered |
{
"origin": "client",
"call_id": "100315433877372928",
"type": "single",
"group_id": null,
"media": "audio",
"answered_at": "2026-10-04T19:37:52.114Z",
"answered_by_username": "carol"
}rtc.member_joined
Sent after a member joins a group call (not the initiator).
| Field | Type | Description |
|---|---|---|
call_id | String | The call ID |
group_id | String | The group ID |
username | String | The member who joined |
role | String | invitee, or joiner a group member who joined without being invited |
joined_at | String | When they joined |
joined_count | Number | People in the call after joining |
{
"origin": "client",
"call_id": "100315526282084352",
"group_id": "100311903108071424",
"username": "carol",
"role": "invitee",
"joined_at": "2026-10-04T19:38:16.412Z",
"joined_count": 2
}rtc.member_left
Sent after a member leaves a group call. Members still in the call when it ends aren't sent; see members in rtc.call_ended.
| Field | Type | Description |
|---|---|---|
call_id | String | The call ID |
group_id | String | The group ID |
username | String | The member who left |
left_at | String | When they left |
leave_reason | String | Why: hangup, connection_lost, removed from the call, removed_from_group, session_revoked, user_unavailable, app_unavailable |
joined_ms | Number | This member's total time in the call, in milliseconds |
joined_count | Number | People in the call after leaving |
{
"origin": "client",
"call_id": "100315526282084352",
"group_id": "100311903108071424",
"username": "dave",
"left_at": "2026-10-04T19:38:17.512Z",
"leave_reason": "hangup",
"joined_ms": 1048,
"joined_count": 2
}rtc.call_ended
Sent once per call when it ends, with durations and each member's details.
| Field | Type | Description |
|---|---|---|
call_id, type, group_id, media, initiator_username, ext, created_at | As in rtc.call_created | |
answered_at | String | When connected; null if never connected |
ended_at | String | When it ended |
end_reason | String | Why it ended: completed, canceled by the initiator, rejected, busy, no_answer, connection_lost, server_ended by your server or console, platform_ended, user_unavailable, group_unavailable, app_unavailable, service_disabled media service unavailable, max_duration over the maximum duration |
ended_by_username | String | Who ended it; may be null |
ended_via | String | Entry point: client, openapi, console, platform, system |
duration_ms | Number | Call duration from connection to end, in milliseconds; 0 if never connected |
participant_ms | Number | Sum of all members' time in the call, in milliseconds |
billable_minutes | Number | Billable minutes |
peak_joined | Number | The most people in the call at once |
joined_members | Number | How many people joined |
invited_members | Number | How many people were invited |
members | Array<Object> | Each member: username; role (initiator, invitee, joiner); state final state (left, declined, busy, missed, canceled and so on); joined_platform platform when joining; first_joined_at; left_at; leave_reason (including ended when the call ended); joined_ms total time in the call; media_joined_at when connected to the media service. Time fields are null for members who never joined |
{
"origin": "client",
"call_id": "100315526282084352",
"type": "group",
"group_id": "100311903108071424",
"media": "video",
"initiator_username": "alice",
"ext": null,
"created_at": "2026-10-04T19:38:05.238Z",
"answered_at": "2026-10-04T19:38:16.412Z",
"ended_at": "2026-10-04T19:38:18.556Z",
"end_reason": "completed",
"ended_by_username": "alice",
"ended_via": "client",
"duration_ms": 2144,
"participant_ms": 5336,
"billable_minutes": 3,
"peak_joined": 3,
"joined_members": 3,
"invited_members": 2,
"members": [
{
"username": "alice",
"role": "initiator",
"state": "left",
"joined_platform": "ios",
"first_joined_at": "2026-10-04T19:38:05.238Z",
"left_at": "2026-10-04T19:38:18.556Z",
"leave_reason": "ended",
"joined_ms": 2144,
"media_joined_at": null
},
{
"username": "carol",
"role": "invitee",
"state": "left",
"joined_platform": "macos",
"first_joined_at": "2026-10-04T19:38:16.412Z",
"left_at": "2026-10-04T19:38:18.556Z",
"leave_reason": "ended",
"joined_ms": 2144,
"media_joined_at": null
},
{
"username": "dave",
"role": "invitee",
"state": "left",
"joined_platform": "web",
"first_joined_at": "2026-10-04T19:38:16.464Z",
"left_at": "2026-10-04T19:38:17.512Z",
"leave_reason": "hangup",
"joined_ms": 1048,
"media_joined_at": null
}
]
}Moderation
Moderation events tell your server about new items to review, review decisions, user reports and violations, for example to notify reviewers or warn violating users. Events don't include the reviewed content or matched words; read them with the moderation APIs by item_id if needed. For field meanings, see Moderation.
Targets involving users (target_type user, user_profile, group_member, group_request, friend_request) have no target_id and use target_username and target_group_id instead; other targets (messages, groups, chat rooms, files and so on) have the object's ID in target_id.
moderation.item_created
Sent when your review queue gets a new item: content matched a word list set to review, third-party review suggested manual review, post-review automatically acted on content, or a user reported something. When the same target already has an unresolved item, new matches and reports join it without another event. origin is always system.
| Field | Type | Description |
|---|---|---|
item_id | String | The review item ID |
scene | String | The scene, such as message, chatroom_message, user_profile, group_profile, media_upload; report for items created by reports |
target_type | String | message, chatroom_message, user_profile, group_profile, group_member, group_request, friend_request, chatroom_profile, chatroom_attribute, file, user, group, chatroom |
target_id | String | The target's ID; absent for targets involving users |
target_username | String | The user involved; may be null |
target_group_id | String | The group involved; may be null |
conversation_id | String | The message's conversation; null otherwise |
author_username | String | The content's author; may be null |
source_types | Array<String> | Why it's under review: word_list, provider third-party review, url_rejected an external address that couldn't be reviewed, report, escalation |
categories | Array<String> | Matched categories |
suggestion | String | Third-party review's suggestion: review or block; null if none |
status | String | pending, or auto_violation acted on automatically and awaiting confirmation |
auto_actions | Array<String> | Actions already taken automatically, such as recall_message, block_file |
report_count | Number | How many reports |
created_at | String | When created |
A user's message matched a word list set to review:
{
"origin": "system",
"item_id": "100318924263915520",
"scene": "message",
"target_type": "message",
"target_id": "100318922028351488",
"target_username": null,
"target_group_id": null,
"conversation_id": "100318921969631232",
"author_username": "bob",
"source_types": ["word_list"],
"categories": ["ad"],
"suggestion": "review",
"status": "pending",
"auto_actions": [],
"report_count": 0,
"created_at": "2026-10-04T19:51:35.391Z"
}A user was reported:
{
"origin": "system",
"item_id": "100318971017822208",
"scene": "report",
"target_type": "user",
"target_username": "bob",
"target_group_id": null,
"conversation_id": null,
"author_username": "bob",
"source_types": ["report"],
"categories": ["ad"],
"suggestion": null,
"status": "pending",
"auto_actions": [],
"report_count": 1,
"created_at": "2026-10-04T19:51:46.529Z"
}moderation.item_decided
Sent when a review item gets a decision or is re-decided. Items in the platform's queue are only sent when the platform confirms a violation. origin: server for decisions by your reviewers (console) or your server, platform for platform reviewers.
| Field | Type | Description |
|---|---|---|
item_id | String | The review item ID |
queue | String | tenant your review queue, platform the platform's |
target_type, target_id, target_username, target_group_id, author_username | As in moderation.item_created | |
previous_status | String | The status before the decision |
decision | String | violation or no_violation |
actions | Array<String> | Actions taken: recall_message, recall_chatroom_message, remove_chatroom_attribute, block_file, unblock_file, clear_profile, mute_user, disable_user, disable_group, disable_chatroom |
decided_by_type | String | Who decided: account a console member, app your server, platform_admin the platform |
decided_at | String | When decided |
{
"origin": "server",
"item_id": "100318924263915520",
"queue": "tenant",
"target_type": "message",
"target_id": "100318922028351488",
"target_username": null,
"target_group_id": null,
"author_username": "bob",
"previous_status": "pending",
"decision": "violation",
"actions": ["recall_message"],
"decided_by_type": "app",
"decided_at": "2026-10-04T19:51:46.604Z"
}Actions also send their own events, such as message.recalled for recalls and user.mute_changed for mutes.
moderation.report_created
Sent after a user reports from a client, or your server reports on a user's behalf. origin: client for client reports, server for server reports on a user's behalf.
| Field | Type | Description |
|---|---|---|
report_id | String | The report ID |
item_id | String | The review item the report joined |
reporter_username | String | The reporter |
target_type | String | What was reported: message, user, group, chatroom, chatroom_message |
target_id | String | The target's ID; absent when reporting a user |
target_username | String | The reported user when reporting a user; may be null |
target_group_id | String | The group involved; may be null |
reason | String | The report reason, such as ad, fraud, abuse |
via | String | client or openapi server |
created_at | String | When reported |
{
"origin": "server",
"report_id": "100318971017822209",
"item_id": "100318971017822208",
"reporter_username": "carol",
"target_type": "user",
"target_username": "bob",
"target_group_id": null,
"reason": "ad",
"via": "openapi",
"created_at": "2026-10-04T19:51:46.530Z"
}moderation.violation_recorded
Sent after a user's violation is recorded, with any automatic penalty it triggered, so you can notify or warn the user. origin: system for rejections before sending or saving and automatic post-review actions, server when your reviewers confirmed it, platform when the platform did.
| Field | Type | Description |
|---|---|---|
username | String | The violating user |
scene | String | The scene |
category | String | The violation category |
source | String | How it was found: sync rejected before sending or saving, async acted on automatically in post-review, review confirmed by reviewers |
authority | String | Whose rules: tenant yours, platform the platform's |
item_id | String | The corresponding review item; null for rejections before sending, which have none |
penalties | Array<Object> | Automatic penalties triggered; empty if none. Each has kind (mute_user global mute, disable_user ban), scopes (mute scopes, empty for bans) and duration_seconds (null for permanent) |
created_at | String | When recorded |
A user's message was rejected by a word list, triggering the rule "mute one-to-one for 10 minutes after 1 violation":
{
"origin": "system",
"username": "bob",
"scene": "message",
"category": "fraud",
"source": "sync",
"authority": "tenant",
"item_id": null,
"penalties": [
{ "kind": "mute_user", "scopes": ["chat"], "duration_seconds": 600 }
],
"created_at": "2026-10-04T19:51:34.896Z"
}Automatic penalties also send their own user.mute_changed or user.status_changed, with origin system.
Files
For the APIs and field meanings of file events, see Files overview. url and thumbnail_url are file addresses that only the app's users and your server can exchange for download addresses.
media.file_uploaded
Sent after a file finishes uploading, from clients, the server or the console. origin follows the entry point: client for clients, server for the server and console.
| Field | Type | Description |
|---|---|---|
file_id | String | The file ID |
purpose | String | attachment, user_avatar, group_avatar, group_file |
kind | String | image, voice, video, file |
content_type | String | The detected format, such as image/png |
size | Number | Size in bytes |
width, height | Number | Image dimensions; null for other kinds |
sha256 | String | SHA-256 for images and avatars; null for other kinds |
name | String | File name; may be null |
url | String | The file address (the public address for avatars) |
thumbnail_url | String | The thumbnail address; null if none |
owner_username | String | The owning user; may be null |
group_id | String | The group of a group file; null otherwise |
via | String | Entry point: client, openapi, console |
created_at | String | When the upload was created |
completed_at | String | When the upload completed |
{
"origin": "server",
"file_id": "100312819781599232",
"purpose": "attachment",
"kind": "image",
"content_type": "image/png",
"size": 154,
"width": 64,
"height": 48,
"sha256": "6c8107d3712cc2d23a40f2ee38b2030027b46b01a1060510e87ad991f606813b",
"name": "pic.png",
"url": "https://im.example.com/media/v1/f/100312819781599232/xHa5BkJq8pYxZQwoRIgq2Q",
"thumbnail_url": "https://im.example.com/media/v1/f/100312819781599232/xHa5BkJq8pYxZQwoRIgq2Q/thumb",
"owner_username": "alice",
"group_id": null,
"via": "openapi",
"created_at": "2026-10-04T19:27:19.958Z",
"completed_at": "2026-10-04T19:27:20.532Z"
}media.file_blocked
Sent after a file is blocked for violating rules, see Blocking violating files.
| Field | Type | Description |
|---|---|---|
file_id, purpose, kind, owner_username, group_id | As in media.file_uploaded | |
blocked_by | String | Who blocked it: system a moderation action (automatic, or after reviewers confirmed a violation), platform a platform administrator |
rule_source | String | Whose rules: tenant your app's, platform the platform's |
reason | String | Why it was blocked |
blocked_at | String | When blocked |
{
"origin": "platform",
"file_id": "100312819781599232",
"purpose": "attachment",
"kind": "image",
"owner_username": "alice",
"group_id": null,
"blocked_by": "platform",
"rule_source": "platform",
"reason": "违规图片",
"blocked_at": "2026-10-04T19:27:57.911Z"
}media.file_unblocked
Sent after a file is unblocked.
| Field | Type | Description |
|---|---|---|
file_id, purpose, kind, owner_username, group_id | As in media.file_uploaded | |
unblocked_by | String | Who unblocked it: system (such as a review item re-decided as no violation) or platform |
status | String | Status after unblocking: active usable again, or deleted the file lost its owner and wasn't restored |
unblocked_at | String | When unblocked |
{
"origin": "platform",
"file_id": "100312819781599232",
"purpose": "attachment",
"kind": "image",
"owner_username": "alice",
"group_id": null,
"unblocked_by": "platform",
"status": "active",
"unblocked_at": "2026-10-04T19:27:57.950Z"
}AI conversations
Events for AI conversations. Delivery differs from other events: when a bot has a callback address configured (endpoint_id), its three events go only to that address, even if it didn't subscribe; otherwise they go to addresses subscribed to the event.
ai.message_received
A user sent a one-to-one message to an enabled bot. On receiving it, start a reply with reply_to_seq set to message.seq. Messages sent by bots and group messages don't trigger it. It usually arrives within a second of the user sending the message.
| Field | Type | Description |
|---|---|---|
bot | String | The bot's username |
from | String | The user who sent the message |
conversation_id | String | The one-to-one conversation ID |
message | Object | The message, structured like the message in message.sent |
context | Array | The most recent messages before this one (including the bot's earlier replies), oldest first, as many as the bot's context_messages (10 by default); at most 48 KB in total, dropping the oldest beyond that |
{
"origin": "client",
"bot": "assistant",
"from": "zhangsan",
"conversation_id": "1840098765432101",
"message": {
"conversation_id": "1840098765432101",
"conversation_type": "single",
"seq": 356,
"message_id": "1840120033445566777",
"sender": "zhangsan",
"recipient": "assistant",
"type": "text",
"body": { "text": "明天上海天气?" },
"created_at": "2026-09-28T08:00:00.000Z"
},
"context": [
{
"conversation_id": "1840098765432101",
"seq": 355,
"sender": "zhangsan",
"type": "text",
"body": { "text": "你好" },
"created_at": "2026-09-28T07:59:40.000Z"
}
]
}Some fields are omitted from the messages in the example.
ai.stream_finished
An AI reply ended, whatever the result: completed, stopped by the user, timed out, interrupted and so on.
| Field | Type | Description |
|---|---|---|
stream_id | String | The stream ID |
client_stream_id | String | The idempotency ID given when starting; null if none |
bot, to | String | The bot and the user receiving the reply |
conversation_id | String | The conversation ID |
message_seq | Number | The AI message's sequence in the conversation |
status | String | completed, stopped or failed |
finish_reason | String | The finish reason |
error | Object | { "code": "…", "message": "…" } when failed, null otherwise |
last_seq | Number | The sequence of the last delta |
started_at, finished_at | String | Start and finish times |
{
"origin": "server",
"stream_id": "1841000000000000123",
"client_stream_id": "req-7f3a",
"bot": "assistant",
"to": "zhangsan",
"conversation_id": "1840098765432101",
"message_seq": 358,
"status": "completed",
"finish_reason": "stop",
"error": null,
"last_seq": 57,
"started_at": "2026-09-28T08:00:01.000Z",
"finished_at": "2026-09-28T08:00:09.000Z"
}ai.stream_stopped
A user tapped stop generating in a client. The reply has already ended as stopped; stop calling the model as soon as you receive this. An ai.stream_finished with status stopped follows. The server SDKs' callback handlers stop the matching stream in the process automatically when they receive this event.
| Field | Type | Description |
|---|---|---|
stream_id | String | The stream ID |
client_stream_id | String | The idempotency ID given when starting; null if none |
bot, to | String | The bot and the user receiving the reply |
conversation_id | String | The conversation ID |
stopped_by | String | The user who tapped stop |
stopped_at | String | When stopped |
{
"origin": "client",
"stream_id": "1841000000000000123",
"client_stream_id": null,
"bot": "assistant",
"to": "zhangsan",
"conversation_id": "1840098765432101",
"stopped_by": "zhangsan",
"stopped_at": "2026-09-28T08:00:05.000Z"
}Standalone RTC channels
Subscribe to rtc_channel.* or the full event names below. rtc.* only covers calls, not channels. rtc_channel.config_changed is an internal configuration notice with no tenant callback.
Channel callbacks use the same signatures, filters, retries and request body format as other events. Deduplicate by event id; arrival order isn't guaranteed, so use version for channel profiles and session_version / members_version for members, without mixing versions. For full state, query the data within your authorization through Channel management or Session management. Callbacks don't include tickets, media tokens, device info or explanations of management actions. The common fields of each event are below.
Common channel data fields
| Field | Type | Meaning |
|---|---|---|
channel_id, channel_uid | String | Business ID and unique resource ID |
generation, version, members_version | Integer | Media generation, channel profile version, member version |
occurred_at | String | When the change happened |
origin | String | The common origin values |
session_id, username, role, reason, session_version | Optional | Session identity, role, reason code and version in session events; don't assume these exist in non-session events |
The data of rtc_channel.created (not the whole callback request body):
{"channel_id":"demo","channel_uid":"10001","generation":1,"version":1,"members_version":1,"occurred_at":"2026-10-09T01:00:00.000Z","origin":"system"}rtc_channel.created
The channel was created; no IM group is created and no call is started.
rtc_channel.updated
The name, ext or deadline actually changed; setting the same values doesn't increase the profile version.
rtc_channel.closing
New joins are already refused and media cleanup is in progress; don't assume every member is offline yet.
rtc_channel.closed
Closing cleanup finished; the channel ID can't be reused.
rtc_channel.session_joined
The server confirmed the media connection as joined; it doesn't mean the user turned on their microphone or camera.
rtc_channel.session_leaving
Removal, leaving or timeout cleanup started; authorization has stopped, but the seat may still be being cleaned up.
rtc_channel.session_left
The session's media cleanup finished; this is a different final state from rtc_channel.closed.
rtc_channel.member_banned
A channel ban changed. The callback doesn't currently guarantee the target username; to check members, query the ban list and session APIs rather than guessing from missing fields.
rtc_channel.member_unbanned
A channel ban was lifted; old tickets and ended sessions aren't restored. The callback doesn't currently guarantee the target username, so read the relevant ban state again.
