Flag, Mute & Ban
Overview
Stream provides moderation APIs for flagging content, muting users, banning users, and blocking users. These actions integrate with the moderation review queue and dashboard. All endpoints use the v2 moderation API and are available through Stream's server-side SDKs.
Not every action works outside Stream's products. Flag, ban, and bypass also work for your own (standalone) content and users. Mute, block, and channel bans only take effect inside Stream Chat, Feeds, and Video.
| Action | Standalone content and users | Stream Chat, Feeds, Video |
|---|---|---|
| Flag | Yes. Any entity_type; custom types need server-side auth |
Yes |
| Ban | Yes. Stream records the ban; your app enforces it | Yes. Stream enforces it |
| Channel ban | No | Stream Chat only |
| Mute | No | Yes |
| Block | No | Yes |
| Bypass | Yes. Skips Check for the user's content | Yes |
Flag
Flagging allows users or moderators to report content for review. Flagged content is automatically added to the moderation review queue. You can flag messages, users, activities, reactions, or any custom entity type.
Flag Content
Use the flag method to report an entity for moderation review. You can attach a reason, custom metadata, and a moderation payload containing the content to be reviewed.
client.moderation().flag(FlagRequest.builder()
.entityType("stream:chat:v1:message")
.entityID(messageId)
.entityCreatorID(creatorId)
.reason("spam")
.custom(Map.of("user_comment", "This user is spamming the channel"))
.userID(moderatorId)
.build()).execute();Request parameters
| key | required | type | description |
|---|---|---|---|
| entity_type | true | string | Type of entity being flagged. Examples: stream:chat:v1:message, stream:user, stream:feeds:v2:activity, stream:feeds:v2:reaction, or any custom type. |
| entity_id | true | string | Unique identifier of the entity being flagged. |
| entity_creator_id | false | string | User ID of the entity creator. |
| reason | false | string | Reason for flagging. Use a slug or keyword for easy filtering. |
| custom | false | object | Custom metadata to attach to the flag. |
| moderation_payload | false | object | Content to be reviewed (texts, images, videos). Displayed on the dashboard. |
| user_id | false | string | User ID of the person flagging (required for server-side usage). |
Response
| key | type | description |
|---|---|---|
| item_id | string | ID of the review queue item created by the flag. |
Flag a User
To flag a user rather than a message, set entity_type to stream:user and provide the target user's ID as entity_id.
client.moderation().flag(FlagRequest.builder()
.entityType("stream:user")
.entityID(targetUserId)
.reason("harassment")
.userID(moderatorId)
.build()).execute();Query Moderation Flags
Query flags with filtering, sorting, and pagination. This allows you to retrieve flags matching specific criteria.
var response = client.moderation().queryModerationFlags(QueryModerationFlagsRequest.builder()
.filter(Map.of("entity_type", "stream:chat:v1:message"))
.sort(List.of(SortParam.builder().field("created_at").direction(-1).build()))
.limit(20)
.build()).execute();Request parameters
| key | required | type | description |
|---|---|---|---|
| filter | false | object | Filter conditions. |
| sort | false | array | Sort parameters. |
| limit | false | number | Maximum flags to return. |
| next | false | string | Pagination cursor. |
Mute
Mute only takes effect inside Stream Chat, Feeds, and Video. For your own app, store mutes in your own system.
Muting allows users to silence other users. Muted users' messages are still delivered via WebSocket but not via push notifications (APN/Firebase). Implementing UI logic for muted users (hiding or displaying differently) is left to the developer.
Mute User
Mute one or more users. You can optionally set a timeout after which the mute automatically expires.
client.moderation().mute(MuteRequest.builder()
.targetIds(List.of("user_to_mute"))
.userID(mutingUserId)
.timeout(60)
.build()).execute();Request parameters
| key | required | type | description |
|---|---|---|---|
| target_ids | true | array | List of user IDs to mute. |
| user_id | false | string | User ID of the person performing the mute. |
| timeout | false | number | Mute duration in minutes. If not set, the mute has no expiration. |
Unmute User
Remove a mute from one or more users.
client.moderation().unmute(UnmuteRequest.builder()
.targetIds(List.of("user_to_unmute"))
.userID(unmutingUserId)
.build()).execute();Ban
Banning stops a user from posting. An app-wide ban works for any user: Stream records it on the user (banned, ban_expires) and sends a user.banned webhook. Stream Chat and Feeds enforce it for you; in your own app, read the ban state or handle the webhook and block the user yourself. Stream Chat users can also be banned from a single channel. Bans support timeouts (temporary bans), shadow banning, IP banning, and optional message deletion.
Ban User
Ban a user from the entire app. You can optionally specify a timeout for temporary bans, a reason, and whether to delete the user's messages.
client.moderation().ban(BanRequest.builder()
.targetUserID("user_to_ban")
.bannedByID(moderatorId)
.reason("Repeated spam")
.timeout(1440)
.build()).execute();Request parameters
| key | required | type | description |
|---|---|---|---|
| target_user_id | true | string | User ID to ban. |
| banned_by_id | false | string | User ID of the moderator performing the ban. |
| channel_cid | false | string | Channel CID for channel-scoped bans (e.g., messaging:general). Omit for app-wide bans. |
| reason | false | string | Reason for the ban. |
| timeout | false | number | Ban duration in minutes. If not set, the ban is permanent. |
| shadow | false | boolean | When true, the user is shadow banned. Their messages are only visible to themselves. |
| ip_ban | false | boolean | When true, also bans the user's last known IP address. Automatically applies a 30-day timeout unless overridden. |
| delete_messages | false | string | Delete the user's messages. Values: soft (removes from client, retained on server) or hard (permanently deleted). |
Channel-Scoped Ban
Stream Chat only. Ban a user from a specific channel by providing the channel_cid parameter. The user will still be able to use the rest of the app.
client.moderation().ban(BanRequest.builder()
.targetUserID("user_to_ban")
.bannedByID(moderatorId)
.channelCID("messaging:general")
.reason("Trolling in this channel")
.timeout(60)
.build()).execute();Shadow Ban
Shadow banning allows a user to continue posting, but their messages are only visible to themselves. Other users will not see the shadow-banned user's messages. This is useful for handling disruptive users without alerting them.
client.moderation().ban(BanRequest.builder()
.targetUserID("user_to_shadow_ban")
.bannedByID(moderatorId)
.reason("Disruptive behavior")
.shadow(true)
.build()).execute();Unban User
Remove a ban from a user. For channel-scoped bans, provide the channel_cid parameter.
client.moderation().unban(UnbanRequest.builder()
.targetUserID("user_to_unban")
.createdBy(moderatorId)
.build()).execute();Query Banned Users
List bans with the metadata that is intentionally not returned by queryUsers — the moderator who issued the ban (banned_by), the original reason, when it expires, and the channel CID for channel-scoped bans. Use this when you need to render a "who banned this user and why" view, build a moderation dashboard, or audit ban history.
queryUsers returns only the ban state (banned, ban_expires) on the user object — banned_by and reason are not part of the user payload by design. Use queryBannedUsers to retrieve them. This endpoint is part of the Chat product API; bans are app-wide, so it works for any app that has Chat enabled regardless of which product applied the ban.
Client-side the endpoint is restricted to a single channel: a request without a channel_cid filter is rejected, so app-wide bans can only be listed server-side. Both regular and shadow bans are returned, and so are bans that have already expired unless exclude_expired_bans is set. To list everyone banned from one channel, including future channel bans, use Query Channel Bans.
// Client-side the query is always scoped to a channel. The iOS SDK adds the
// channel_cid filter, so anything you pass narrows down that channel's bans.
let channelId = ChannelId(type: .messaging, id: "general")
let chat = chatClient.makeChat(for: channelId)
// All of the channel's bans
let bans = try await chat.queryBannedUsers()
for ban in bans {
// BannedUser: user, bannedBy, cid, createdAt, expiresAt, reason, isShadowBan
print("\(ban.user.id) banned by \(ban.bannedBy?.id ?? "unknown"): \(ban.reason ?? "no reason")")
}
// Bans a specific moderator created, newest first, expired ones left out.
// Filter keys: userId, bannedById, createdAt, reason. Only createdAt is sortable.
let query = BannedUserListQuery(
filter: .equal(.bannedById, to: moderatorId),
sort: [.init(key: .createdAt, isAscending: false)],
pagination: Pagination(pageSize: 20),
excludeExpiredBans: true
)
let recentBans = try await chat.queryBannedUsers(with: query)
// The same query with controllers
chatClient.channelController(for: channelId).queryBannedUsers(with: query) { result in
// Handle the Result<[BannedUser], Error>
}QueryBannedUsersPayload payload = QueryBannedUsersPayload.builder()
.filterConditions(Map.of("user_id", Map.of("$eq", "user_to_ban")))
.sort(List.of(SortParamRequest.builder().field("created_at").direction(-1).build()))
.limit(20)
.excludeExpiredBans(true)
.build();
QueryBannedUsersResponse response = client
.queryBannedUsers(QueryBannedUsersRequest.builder().payload(payload).build())
.execute()
.getData();Request parameters
| key | required | type | description |
|---|---|---|---|
| filter_conditions | true | object | Filter conditions. Supported keys include user_id, banned_by_id, channel_cid, reason, created_at, expires. Operators: $eq, $in, $gt, $gte, $lt, $lte. |
| sort | false | array | Sort parameters (max 1). Typically sort by created_at. |
| limit | false | number | Number of records to return (max 300). |
| offset | false | number | Number of records to skip (max 10000). |
| exclude_expired_bans | false | boolean | When true, omits bans whose expires timestamp is in the past. |
| user_id | false | string | Server-side actor making the request. |
Response
| key | type | description |
|---|---|---|
| bans | array | List of bans. Each entry contains the fields described below. |
Each entry in bans:
| key | type | description |
|---|---|---|
| user | object | The banned user. |
| banned_by | object | The moderator who applied the ban (the answer to "who banned this user?"). |
| reason | string | Reason supplied when the ban was created. |
| expires | string | Expiration timestamp for temporary bans. Absent for permanent bans. |
| shadow | bool | true for shadow bans. |
| channel | object | Present for channel-scoped bans; contains the channel CID and metadata. |
| created_at | string | When the ban was applied. |
Query Channel Bans
List every user banned from one channel in a single cursor-paginated call, with the scope of each ban. An entry's scopes is ["channel"] for a ban on the channel itself, ["future_channel"] for a future channel ban issued by the channel's creator, or both. Each user appears once, so you do not need to page queryBannedUsers and queryFutureChannelBans separately and merge the lists yourself. Use it to render a channel's ban list, search it by user ID, and show per-scope counts.
QueryChannelBansResponse response = client.chat()
.queryChannelBans("messaging", "general", QueryChannelBansRequest.builder()
.filterConditions(Map.of("user_id", Map.of("$autocomplete", "jo")))
.excludeExpiredBans(true)
.includeTotal(true)
.limit(25)
.build())
.execute()
.getData();The Python SDK does not expose this endpoint yet; its method arrives with the next stream-py release.
Request parameters
| key | required | type | description |
|---|---|---|---|
| filter_conditions | false | object | Only user_id is supported, with $eq, $in (up to 100 IDs) and $autocomplete (case-insensitive prefix of up to 80 characters). Omit it, or pass {}, to list every banned user. |
| exclude_expired_bans | false | boolean | When true, omits bans whose expires timestamp is in the past. |
| include_total | false | boolean | When true, the response includes total and total_by_scope, counted over the whole list regardless of paging. |
| limit | false | number | Entries per page, 1 to 100. Defaults to 25. |
| next | false | string | Cursor from a previous response, to fetch the page after it. |
| prev | false | string | Cursor from a previous response, to fetch the page before it. Cannot be combined with next. |
| user_id | false | string | Server-side actor making the request. |
Entries are ordered newest ban first: by created_at descending, then by user_id descending. Every entry is a distinct user, so the next and prev cursors never skip or repeat an entry, even when bans are added or removed between pages. A user banned by both scopes is placed by the channel ban's created_at.
Response
| key | type | description |
|---|---|---|
| bans | array | One entry per banned user, described below. |
| next | string | Cursor for the next page. Absent on the last page. |
| prev | string | Cursor for the previous page. Absent on the first page. |
| total | number | Number of distinct banned users. Only present when include_total is true. |
| total_by_scope | object | channel and future_channel counts. A user with both scopes is counted in each. Only present when include_total is true. |
Each entry in bans:
| key | type | description |
|---|---|---|
| user | object | The banned user. Absent when the user has since been hard-deleted. |
| scopes | array | channel, future_channel, or both. |
| created_at | string | When the ban was created. The channel ban's timestamp when the user has one, otherwise the future channel ban's. |
| channel_ban | object | Details of the ban on this channel. Present when scopes contains channel. |
| future_channel_ban | object | Details of the channel creator's future channel ban against this user. Present when scopes contains future_channel. |
channel_ban and future_channel_ban share one shape:
| key | type | description |
|---|---|---|
| banned_by | object | The user who issued the ban. |
| reason | string | Reason supplied when the ban was created. |
| shadow | boolean | true for shadow bans. |
| expires | string | Expiration timestamp for temporary bans. Absent for permanent bans. |
| created_at | string | When this ban was created. |
App-wide bans are not part of this list; use Query Banned Users for those. Totals are counted per scope with a cap of 100,000 each, and both total fields are omitted when counting times out, while the page itself is still returned. Client-side requests need the BanChannelMember permission on the channel, which channel moderators have by default.
Future Channel Bans
A future channel ban keeps a user out of channels that do not exist yet. It is scoped to a channel creator: whenever that creator adds the banned user to a channel they own, or creates a channel with the user as a member, the user is banned in that channel automatically. Existing memberships are left alone.
Typical uses:
- Marketplace sellers keeping a problematic buyer out of every product channel they open later.
- Community owners who keep creating new channels and want a known offender excluded from all of them.
- Creators enforcing one moderation decision across every channel they make.
Create a Future Channel Ban
Pass ban_from_future_channels: true to a channel-scoped ban. This bans the user from the channel right away and records a future channel ban owned by the channel's creator. reason, shadow and timeout apply to both.
client.moderation().ban(BanRequest.builder()
.targetUserID("user_to_ban")
.bannedByID(moderatorId)
.channelCID("messaging:general")
.banFromFutureChannels(true)
.reason("Repeated harassment")
.build()).execute();Request parameters
| key | required | type | description |
|---|---|---|---|
| ban_from_future_channels | false | boolean | Also ban the user from channels the channel's creator makes in the future. |
| channel_cid | false | string | The channel to ban from. The future channel ban is scoped to this channel's creator. Without it, the user is banned app-wide and the future channel ban is scoped to the moderator in banned_by_id. |
| timeout | false | number | Minutes until the channel ban and the future channel ban expire. Omit for a permanent ban. |
| shadow | false | boolean | Make the channel ban, and every ban later derived from the future channel ban, a shadow ban. |
| reason | false | string | Reason, copied to every ban derived from the future channel ban. |
ban_from_future_channels without channel_cid cannot be combined with ip_ban. From the review queue, set the same flag on a ban action; see Ban from Future Channels.
How Future Channel Bans Are Applied
When the creator adds members to a channel they created, or creates a channel with members, each new member is checked against the creator's active future channel bans. A match marks the member as banned in that channel and creates a channel ban that inherits reason and shadow. The derived channel ban has no expiration of its own, so it stays until the user is unbanned from that channel.
- Only the channel creator's future channel bans apply. If another user creates a channel and adds the target, nothing happens.
- Members already in a channel are not affected. A future channel ban is not retroactive.
- A ban on the channel itself takes precedence when both exist.
Channel member objects show the link. ban_from_future_channels is true on a banned member who is also covered by an active future channel ban from the channel's creator, and future_channel_ban_expires carries that ban's expiration when it has one. Both fields appear on channel state and query members responses, not on events or webhooks.
Future Channel Ban Expiration
timeout applies to the future channel ban itself, and to the channel ban created with it. Once it expires, no new channel picks up the ban, but channel bans already derived from it stay in place until removed.
Remove a Future Channel Ban
Submit an unban action with remove_future_channels_ban: true. The action removes the future channel ban owned by the acting user, so user_id must be the channel creator the ban is scoped to. Pass channel_cid to also lift the ban on that channel. Channel bans derived from the future channel ban in other channels are left in place and need their own unban.
client.moderation().submitAction(SubmitActionRequest.builder()
.actionType("unban")
.unban(UnbanActionRequest.builder()
.targetUserID("user_to_unban")
.channelCID("messaging:general")
.removeFutureChannelsBan(true)
.build())
.userId(channelCreatorId)
.build()).execute();The Unban User endpoint only lifts the channel or app-wide ban. It does not remove future channel bans.
Query Future Channel Bans
List the future channel bans a creator has issued, or every future channel ban against one user. To see a channel's bans across both scopes, use Query Channel Bans instead.
QueryFutureChannelBansPayload payload = QueryFutureChannelBansPayload.builder()
.userID(creatorId)
.excludeExpiredBans(true)
.includeTotal(true)
.limit(50)
.build();
QueryFutureChannelBansResponse response = client.chat()
.queryFutureChannelBans(QueryFutureChannelBansRequest.builder().payload(payload).build())
.execute()
.getData();Request parameters
| key | required | type | description |
|---|---|---|---|
| user_id | false | string | The creator whose future channel bans to list. Server-side, one of user_id or target_user_id is required. Client-side requests are always scoped to the authenticated user. |
| target_user_id | false | string | The banned user. With user_id, narrows that creator's bans to this user. On its own, lists every creator's bans against this user. |
| exclude_expired_bans | false | boolean | When true, omits future channel bans whose expires timestamp is in the past. |
| include_total | false | boolean | When true, the response includes total: the number of bans matching the filter regardless of paging, capped at 100,000. |
| limit | false | number | Number of records to return (max 300). |
| offset | false | number | Number of records to skip (max 10000). |
Response
| key | type | description |
|---|---|---|
| bans | array | List of future channel bans. Each entry contains the fields below. |
| total | number | Matching bans, ignoring paging. Only present when include_total is true. |
Each entry in bans:
| key | type | description |
|---|---|---|
| user | object | The banned user. |
| banned_by | object | The creator who owns the future channel ban. |
| reason | string | Reason supplied when the ban was created. |
| shadow | boolean | true for shadow bans. |
| expires | string | Expiration timestamp for temporary bans. Absent for permanent bans. |
| created_at | string | When the future channel ban was created. |
Events
Creating a future channel ban sends the usual user.banned event for the channel ban. The future channel ban itself has no event of its own.
When a future channel ban is applied to a new channel, the membership event (member.added, or channel.created for a new channel) is followed by a user.banned event for that channel. Its created_by is the channel creator, reason and shadow come from the future channel ban, and it carries no expiration.
Removing a future channel ban sends no event. Channel bans derived from it that you remove later send their own user.unbanned events.
Block
Blocking only takes effect inside Stream Chat, Feeds, and Video, and it is not a moderation method. Call blockUsers on the common client (client.blockUsers in Node, client.block_users in Python, client.common.block_users in Ruby, client.BlockUsers in Go, client.blockUsers in Java and PHP). The body is blocked_user_id and user_id.
client.blockUsers(BlockUsersRequest.builder()
.blockedUserID("user_to_block")
.userID(blockingUserId)
.build()).execute();Bypass Moderation
Enable or disable moderation bypass for a specific user, allowing them to skip content moderation checks. This is a server-side only operation intended for trusted users whose content should not be subject to automated moderation. Bypass applies to the user's Stream Chat and Feeds content and to Check calls for any entity type, where entity_creator_id is the user. See Bypass Moderation for permission-based bypass in Stream Chat.
client.moderation().bypass(BypassRequest.builder()
.targetUserID("trusted_user_id")
.enabled(true)
.build()).execute();Request parameters
| key | required | type | description |
|---|---|---|---|
| target_user_id | true | string | User ID to enable or disable bypass for. |
| enabled | true | boolean | true to enable bypass, false to disable. |