Actions
Submit Action
submit_action applies a moderator's decision to a review queue item: delete content, ban a user, mark the item reviewed, restore content, and more.
Actions also run without a moderator. Each policy rule sets the action an engine applies when content matches, such as flag, remove, bounce, or mask. See Automatic actions.
POST /api/v2/moderation/submit_action
await client.SubmitActionAsync(new SubmitActionRequest
{
ActionType = "mark_reviewed",
ItemID = "review_queue_item_id",
UserID = moderatorId,
});Request Parameters
| Key | Required | Type | Description |
|---|---|---|---|
| action_type | true | string | The action to take. See supported action types below. |
| item_id | false | string | Review queue item ID. Required for most actions. |
| user_id | false | string | User ID of the moderator performing the action. |
| ban | false | object | Ban-specific parameters (used with ban action type). |
| unban | false | object | Unban-specific parameters (used with unban action type). |
| delete_message | false | object | Delete message parameters. |
| delete_activity | false | object | Delete activity parameters. |
| delete_poll | false | object | Delete poll parameters: reason. |
| delete_poll_option | false | object | Delete poll option parameters: reason, option_id. |
| delete_poll_answer | false | object | Delete poll answer parameters: reason. |
| mark_reviewed | false | object | Mark reviewed parameters: reason, decision_reason. |
| restore | false | object | Restore parameters: reason, decision_reason. |
| unblock | false | object | Unblock parameters. Deprecated. Use restore. |
| custom | false | object | Custom action parameters (used with custom action type). |
| reject_appeal | false | object | Reject appeal parameters. |
| appeal_id | false | string | Appeal ID (for appeal-related actions). |
| decision_reason | false | string | Not a top-level field. Set it inside restore, unban, mark_reviewed, or reject_appeal. |
Response
| Key | Type | Description |
|---|---|---|
| item | object | The updated review queue item. |
Supported Action Types
Some actions only work on content stored in Stream Chat, Stream Feeds, or Stream Video. Actions marked Any also work for your own (standalone) entity types.
| Action Type | Works with | Description |
|---|---|---|
| mark_reviewed | Any | Mark the item as reviewed without taking further action (content is safe). |
| flag | Any | Flag an entity from the review action. Custom entity types need server-side auth. |
| ban | Any | Ban the content creator. Supports shadow, IP, and temporary bans. Channel bans are Stream Chat only. |
| unban | Any | Unban a previously banned user. Also accepts appeals automatically. |
| bypass | Any | Turn moderation bypass on or off for the user. |
| escalate | Any | Escalate the item for higher-level review. |
| de_escalate | Any | De-escalate a previously escalated item. |
| reject_appeal | Any | Reject a user's appeal against a moderation decision. |
| custom | Any | Trigger a custom action (sends a webhook to your server). See Custom Action. |
| delete_message | Stream Chat | Delete a flagged chat message. |
| delete_user_messages | Stream Chat | Delete messages created by the user. |
| delete_poll | Stream polls | Delete the poll from the review item filed by its creator. See Moderate poll text. |
| delete_poll_option | Stream polls | Delete one option of a poll and keep the rest. The option must still hold the text that was checked. |
| delete_poll_answer | Stream Chat | Delete a free-text poll answer on a chat message. Refused when that text has changed, or the poll is closed. |
| delete_activity | Stream Feeds | Delete a flagged activity. |
| delete_comment | Stream Feeds | Delete a flagged comment. |
| delete_reaction | Stream Feeds | Delete a flagged reaction (Feeds v2). |
| restore | Stream Chat, Feeds | Restore previously deleted content. Also accepts appeals automatically. |
| unblock | Stream Chat, Feeds | Deprecated. Use restore. |
| shadow_block | Stream Chat, Feeds | Shadow-block content (hidden from others but visible to the creator). |
| unmask | Stream Chat, Feeds | Clear a mask applied to the content. |
| delete_user | Stream users | Delete the user who created the content. |
| kick_user | Stream Video | Remove a user from a call. |
| end_call | Stream Video | End an active video call. |
Delete Message
Stream Chat only. Delete a flagged chat message from the review queue.
await client.SubmitActionAsync(new SubmitActionRequest
{
ActionType = "delete_message",
ItemID = "review_queue_item_id",
UserID = moderatorId,
});Ban User
Ban the creator of flagged content. timeout is minutes. A channel ban (Stream Chat only) sets channel_cid and channel_ban_only: true. delete_messages is soft, pruning, or hard. Rule-builder bans are different: ban_options.duration is seconds.
A ban without channel_cid works for any user, including users of your own app. 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.
await client.SubmitActionAsync(new SubmitActionRequest
{
ActionType = "ban",
ItemID = "review_queue_item_id",
Ban = new BanActionRequestPayload
{
Reason = "Repeated harassment",
Timeout = 1440,
ChannelCid = "messaging:general",
},
UserID = moderatorId,
});Ban from Future Channels
Stream Chat only. Set ban_from_future_channels: true on a ban action to also auto-apply the ban to any new channels created later by the same user who created the channel the target is being banned from. This is useful when a community owner repeatedly creates new channels and you want a known offender kept out without re-banning them every time. The ban is applied automatically as the channel creator opens additional channels and remains in effect until removed via an unban with remove_future_channels_ban: true.
await client.SubmitActionAsync(new SubmitActionRequest
{
ActionType = "ban",
ItemID = "review_queue_item_id",
Ban = new BanActionRequestPayload
{
Reason = "Repeated harassment",
BanFromFutureChannels = true,
},
UserID = moderatorId,
});To remove a previously applied future-channels ban, submit an unban action with remove_future_channels_ban: true:
await client.SubmitActionAsync(new SubmitActionRequest
{
ActionType = "unban",
ItemID = "review_queue_item_id",
Unban = new UnbanActionRequest
{
RemoveFutureChannelsBan = true,
},
UserID = moderatorId,
});Restore Content
Restore previously deleted content. This is typically used when content was incorrectly removed. If the item has an associated appeal, the appeal is automatically accepted.
Restore works for Stream Chat messages and Stream Feeds activities, comments, and reactions. Stream doesn't store standalone content, so it can't restore it.
await client.SubmitActionAsync(new SubmitActionRequest
{
ActionType = "restore",
ItemID = "review_queue_item_id",
Restore = new RestoreActionRequestPayload
{
Reason = "Removed by mistake, context checked",
DecisionReason = "Content was incorrectly flagged",
},
UserID = moderatorId,
});reason is an internal moderator note. decision_reason is shown to the user only when the item has an appeal. See Moderator notes.
Moderator notes
Attach a free-text note (reason) to an action to record why a moderator took it. This is how a custom moderation dashboard keeps moderator comments next to each decision. The field is optional, and requests without it behave as before.
Set reason inside the action's own object:
| Action type | Field |
|---|---|
delete_message, delete_activity, delete_comment, delete_reaction, delete_poll, delete_poll_option, delete_poll_answer, delete_user, delete_user_messages |
<action>.reason |
ban |
ban.reason |
flag |
flag.reason |
shadow_block |
shadow_block.reason |
escalate |
escalate.reason (a category) plus escalate.notes |
mark_reviewed |
mark_reviewed.reason |
restore |
restore.reason |
unban, unblock (deprecated), custom, kick_user, end_call, unmask, de_escalate and bypass don't take a note. reject_appeal takes only decision_reason.
await client.SubmitActionAsync(new SubmitActionRequest
{
ActionType = "mark_reviewed",
ItemID = "review_queue_item_id",
MarkReviewed = new MarkReviewedRequestPayload
{
Reason = "Checked context, sarcasm between friends",
},
UserID = moderatorId,
});There is no length limit on reason.
reason and decision_reason
mark_reviewed, restore and unban also accept decision_reason. The two fields are stored in different places:
| Field | Stored on | Who sees it |
|---|---|---|
reason |
The moderation action log | Moderators (logs, review queue, webhooks) |
decision_reason |
The appeal, only when the item has one (appeal_id) |
The user who appealed |
When mark_reviewed accepts an appeal and the content is restored automatically, the action is logged as restore and keeps mark_reviewed.reason.
Read the note back
The note is returned wherever action logs are:
- Query Moderation Logs:
logs[].reason. - Query Review Queue: each item's
actions[].reason. - The
review_queue_item.updatedwebhook:action.reason.
For escalate, reason is the escalation category; the free-text note is in custom.notes.
Bulk actions
POST /api/v2/moderation/bulk_submit_action takes one top-level reason and writes it to every action log the call creates. ban and delete_user fall back to ban.reason and delete_user.reason when the top-level one is empty. A bulk restore with nothing to reverse is logged as mark_reviewed and keeps the reason.
Custom Action
Trigger a custom action that sends a moderation.custom_action webhook to your server. Use this to integrate your own moderation workflows. The event carries action_id (the custom.id you sent), action_options, and the review_queue_item. For Stream Chat messages it also includes message.
await client.SubmitActionAsync(new SubmitActionRequest
{
ActionType = "custom",
ItemID = "review_queue_item_id",
Custom = new CustomActionRequestPayload
{
ID = "notify_user",
Options = new Dictionary<string, object>
{
{ "notification", "Your content has been reviewed" }
},
},
UserID = moderatorId,
});Remove standalone content
Stream doesn't store your own content (a listing title, a profile bio), so the delete actions can't remove it. They only accept Stream Chat, Feeds, and Video entity types. To remove standalone content from the review queue:
- Send a
customaction, for example withid: "delete_listing". - Handle the
moderation.custom_actionwebhook and delete the content in your own system, usingreview_queue_item.entity_id. - Send
mark_reviewedfor the item so it leaves the queue.
Customize review queue buttons
The action buttons in the dashboard review queue are action_config records, one per button and entity type. Create or update one with POST /api/v2/moderation/action_config. List them with GET /api/v2/moderation/action_config, delete one with DELETE /api/v2/moderation/action_config/{id}, or send a batch with POST /api/v2/moderation/action_config/bulk and POST /api/v2/moderation/action_config/bulk_delete.
await client.moderation.upsertActionConfig({
entity_type: "stream:chat:v1:message",
order: 1,
action: "delete_message",
description: "Delete message",
});entity_type and action are required. description is the button label. order is 0-100. Omit id to create a button; send id to update one. A button can use the custom action, which is how you add a button for your own entity types (for example, a "Delete listing" button that fires moderation.custom_action).
Automatic actions
Each rule in a policy sets the action an engine applies when content matches. These are not submit_action types. They run at moderation time, before a moderator sees anything.
| Action | What happens | Review queue |
|---|---|---|
keep |
Nothing. The content is published. | No |
flag |
The content is published and sent to moderators. | Yes |
remove |
The content is blocked. Other users never see it. | Yes |
shadow |
The content is stored but shown only to its author, who doesn't know it was hidden. | Yes |
bounce |
The content is returned to the sender to edit and is not stored. Sending the same content again bounces it again. | No |
bounce_flag |
Like bounce, but if the sender sends the same content again, it is published and flagged. |
On resend |
bounce_remove |
Like bounce, but if the sender sends the same content again, it is removed. |
On resend |
mask |
Matched words are replaced with asterisks and the content is published. Blocklist and regex rules only. | No |
mask_flag |
Like mask, and the content is also flagged so moderators can see the original. |
Yes |
Stream Chat and Feeds apply these actions for you. With the Check API, Stream returns the action as recommended_action and your app applies it.
Bounce
Bounce gives the sender a chance to fix a message instead of sending it to moderators. A bounced message is returned with an explanation, the sender edits it, and the edited message is moderated again. Bounced messages are not stored and don't appear in the dashboard.
Use bounce_flag for borderline content: the sender can still send it unchanged, and a moderator reviews it. Use bounce_remove for content that should never be published.
Set a bounce action on AI Text, blocklist, or regex rules in the dashboard Policies page.
The Stream Chat UI SDKs show a warning next to a bounced message. Tapping it lets the sender edit, send anyway (if permitted), or delete the message. These SDK versions handle bounced messages out of the box:
- React Chat SDK (≥ v12.6.0)
- React Native Chat SDK (≥ v5.43.0)
- iOS Chat SDK (≥ 4.41.0)
- Android Chat SDK (≥ v5.5.0)
- Flutter Chat SDK (≥ 9.7.0)
Without a UI SDK, read the moderation object on the send message response. When action is bounce, show original_text back to the sender so they can edit it:
{
"message": {
"type": "error",
"text": "The message original text",
"user_id": "user_id",
// other message data
"moderation": {
"action": "bounce",
"original_text": "The message original text"
}
}
}Mask
Mask hides sensitive text, such as phone numbers, while the content stays published. With mask_flag, readers see the masked text while moderators review the original.
For example, add a regex blocklist that matches phone numbers ((?m)(\+?\d{1,2}\s?)?(\(?\d{3}\)?[\s.-]?)?\d{3}[\s.-]?\d{4}) and set its action to mask_flag:
| Original message | Stored and shown to everyone |
|---|---|
Text me at 555-123-4567 after 6pm |
Text me at ************ after 6pm |
The moderator sees the original message in the review queue and can remove it, or use Unmask to restore the original text.
Mask works for Stream Chat messages and Stream Feeds v3 activities and comments. It is not supported for Feeds v2. Add the mask_flag rule to your policy's block_list_config:
await client.UpsertConfigAsync(new UpsertConfigRequest
{
Key = "chat",
BlockListConfig = new BlockListConfig
{
Enabled = true,
Rules = new List<BlockListRule>
{
new BlockListRule { Name = blocklistName, Action = "mask_flag", Team = "" },
},
},
});For Feeds, use the same rule on the feeds policy (or feeds:{group_id} / feeds:{group_id}:{feed_id}). When an activity or comment matches the blocklist:
- The stored
textis replaced with the masked version, so every reader, including the author, sees the censored text. - The original text is returned in
moderation.original_textin the response of the add or update call. Moderators can always see the original text in the review queue. - With
mask_flag, the item is flagged (moderation_action: "flag"on the activity,moderation.action: "flag"on the comment) and shows up in the review queue.
Moderators can use the Unmask action in the review queue to restore the original text.