Webhooks
Webhooks provide a powerful way to implement custom logic for moderation. Delivery retries, signature verification and the IP allowlist are covered in the webhooks guide.
Configure webhook URL
You can configure the webhook URL from the Stream dashboard.
- Go to the dashboard
- Select the app for which you want to receive webhook events
- Click on "Preferences" under the "Moderation" section in the left navigation
- Set the webhook URL on that page
Webhook events
The webhook url will receive following events
review_queue_item.new- This event notifies you of new content available for review. The content could be a message, activity, reaction, or even a user profile.
- This event is triggered when content is flagged for the first time. Consequently, you'll also receive the associated flags as part of the payload.
review_queue_item.updated- This event notifies you when existing flagged content receives additional flags or when a moderator performs an action on flagged content.
- The payload includes the new flags or details of the action performed.
moderation_check.completed- This event notifies you when the Moderation Check has been completed.
- This event is triggered for both flag and non-flag actions.
- This event is fired when the moderation check for an entity has finished (after sync and async checks). It is emitted even when no flags were raised.
- The payload includes the entity identifiers and the final
recommended_action.
moderation_rule.triggered- This event notifies you when a moderation rule from the Rule Builder is triggered.
- This event is triggered when rule conditions are met and actions are executed.
- Available for
user,content, andcallrule types. - For call rules, includes
violation_numberto track escalation sequences. - The payload includes rule information, triggered actions, and entity details.
- Aggregation rules (keyframes, closed captions, keyframe OCR) include
matched_contentswhen the request suppliedcontent_ids. - Use this event to get notified only for the harms or conditions you configured on a rule. See Rule-Triggered Webhooks.
moderation.text_analysis.complete- Fired on every Analyze call that included text.
- Carries the same per-text verdicts as the Analyze HTTP response (
actionorerrorper label). - For that Analyze origin, this event replaces
review_queue_item.*andmoderation_check.completed.
moderation.image_analysis.complete- Fired on every Analyze call that included images.
- Carries the same per-image verdicts as the Analyze HTTP response.
- For that Analyze origin, this event replaces
review_queue_item.*andmoderation_check.completed.
moderation.analysis.failed- Fired only after Analyze has already returned
201pendingand Stream could not finish the listed work. - Carries
content_idsonly. Provider-level field errors stay on*.completeaserror, not on this event. - Treat the listed ids as unscreened and resubmit them.
- Fired only after Analyze has already returned
moderation.flagged- Fired when content is flagged.
moderation.custom_action- Fired when a moderator runs a custom action from the review queue.
moderation.mark_reviewed- Fired when a moderator marks an item reviewed.
moderation.intent_detected- Fired when an intent topic on the policy matches.
appeal.created- Fired when a user submits an appeal.
appeal.accepted- Fired when a moderator accepts an appeal.
appeal.rejected- Fired when a moderator rejects an appeal.
call.moderation_warning- Video event. Fired when a call rule warns a participant.
call.moderation_blur- Video event. Fired when a call rule blurs a participant's video.
export.moderation_logs.successandexport.moderation_logs.error- Fired when a moderation-log export finishes or fails.
export.review_queue.success- Fired when a review-queue export finishes.
export.bulk_image_moderation.success- Fired when a bulk image moderation job finishes.
review_queue_item.new
Sent when content is flagged for the first time, by AI, a blocklist, a user, or a moderator. If the same content is flagged again, you receive review_queue_item.updated. New flags are in the payload's flags property. For Stream Chat, this replaces the legacy message.flagged event.
The review_queue_item.new event payload is structured as follows:
{
"review_queue_item": {
--> See ReviewQueueItem shape in the table below
},
"flags": [
--> See ModerationEventFlag shape in the table below
],
"action": {
--> See ModerationEventActionLog shape in the table below
},
"type": "review_queue_item.new",
"created_at": "timestamp",
"received_at": "timestamp"
}review_queue_item.updated
The review_queue_item.updated event payload is structured as follows:
{
"review_queue_item": {
--> See ReviewQueueItem shape in the table below
},
"flags": [
--> See ModerationEventFlag shape in the table below
],
"action": {
--> See ModerationEventActionLog shape in the table below
},
"type": "review_queue_item.updated",
"created_at": "timestamp",
"received_at": "timestamp"
}moderation_check.completed
The moderation_check.completed event payload is structured as follows:
{
"type": "moderation_check.completed",
"created_at": "timestamp",
"entity_id": "user123-iyVcizkX4KhoiOJf2YOS5",
"entity_type": "stream:chat:v1:message",
"recommended_action": "flag",
"review_queue_item_id": "03eab71c-d2bf-418b-982a-15efcfdebb47"
}This event contains the following fields:
| Key | Type | Description |
|---|---|---|
type |
string | The event type, which is always "moderation_check.completed" |
created_at |
datetime | Timestamp when the moderation check was completed |
entity_id |
string | Unique identifier of the entity that was moderated |
entity_type |
string | Type of the entity that was moderated (e.g., "stream:chat:v1:message") |
recommended_action |
string | Action recommended by the moderation system ("flag" or "remove") |
review_queue_item_id |
string | Unique identifier of the associated review queue item |
moderation_rule.triggered
The moderation_rule.triggered event payload is structured as follows:
{
"type": "moderation_rule.triggered",
"created_at": "2026-01-20T23:45:04.485362361Z",
"rule": {
"id": "0450e7d6-9386-4eb9-a720-f3999979989c",
"name": "Nudity or hate speech not allowed from men",
"type": "call",
"description": "Detects inappropriate content in video calls"
},
"violation_number": 1,
"entity_id": "default:default_test-4013",
"entity_type": "stream:v1:call",
"user_id": "test-user-2025b49e-ef45-473f-bb90-4f8f679c8581",
"triggered_actions": ["mute_video", "call_warning"],
"review_queue_item_id": "abc123"
}This event contains the following fields:
| Key | Type | Description | Possible Values |
|---|---|---|---|
type |
string | The event type, which is always "moderation_rule.triggered" | "moderation_rule.triggered" |
created_at |
datetime | Timestamp when the rule was triggered | |
rule |
object | Object containing rule information | |
rule.id |
string | Unique identifier for the rule | |
rule.name |
string | Human-readable rule name | |
rule.type |
string | Rule type | "call", "user", or "content" |
rule.description |
string | Rule description | |
violation_number |
int | (Call rules only) The violation number that triggered this event (1st violation, 2nd violation, etc.) | Only present for call rules. For user and content rules, this field is not included. |
entity_id |
string | The ID of the entity that triggered the rule | For call rules: call CID (e.g., "default:default_test-4013"). For content rules: message ID. |
entity_type |
string | The type of entity that triggered the rule | For call rules: "stream:v1:call". For content rules: "stream:chat:v1:message". |
user_id |
string | The ID of the user who triggered the rule | |
triggered_actions |
array | Array of action types that were executed | Examples: ["mute_video", "call_warning"], ["ban"], ["flag"] |
review_queue_item_id |
string | (Optional) The review queue item ID if the violation was flagged for review | Only present if the rule action includes flagging for review |
app_id |
int | Application ID the rule belongs to | |
scope |
array | (Optional) The rule's config_keys |
Omitted when the rule is application-wide |
threshold |
int | (Optional) The count configured on the condition that fired, not the live counter | Omitted for conditions without a count |
time_window |
string | (Optional) The configured window of that condition | For example "10m" |
category |
string | (Optional) The category sent on the Labels API request |
Only present on Labels API events (entity_type "moderation:label") |
matched_user_ids |
array | (Optional) Users who sent the content since the previous webhook | Only on cross-user webhook_only flood rules |
Note: Webhook events are sent once per rule per violation number per entity. For call rules with action sequences, if multiple keyframes or closed captions trigger the same rule with the same violation number, only one webhook event is sent.
Flood rules with webhook_only send one event each time the count reaches a multiple of the threshold. See Rule-Triggered Webhooks.
On the Labels API, matched_contents entries are typed message or username (up to the 20 newest, text cut at 300 characters), and flood rules carry the triggering content as one entry. Flood rules with webhook_only send one event per threshold multiple.
When an aggregation rule fires, matched_contents lists the items that crossed the threshold. Each entry has type (image or text), id (from content_ids), and published_at. Image rows carry classifications and confidence (or ocr_classifications and severity for OCR rules). Text rows carry classifications and severity.
moderation.text_analysis.complete
Fired once per async Analyze request that included texts. The texts map uses the same shape as the Analyze HTTP response.
{
"type": "moderation.text_analysis.complete",
"entity_type": "listing",
"entity_id": "listing_42",
"entity_creator_id": "user_42",
"config_key": "catalog-v1",
"review_queue_item_id": "rqi_abc",
"texts": {
"caption": {
"id": "cap_42",
"action": "flag",
"classifications": [{ "name": "INSULT", "severity": "high" }],
"severity": "high",
"language": "en"
}
},
"custom": { "vendor_id": "v_acme" }
}| Key | Type | Description |
|---|---|---|
type |
string | Always "moderation.text_analysis.complete" |
entity_type |
string | Canonical entity type from the request. A subtype such as external:call:keyframe is not echoed |
entity_id |
string | Echo of the request entity_id |
entity_creator_id |
string | Echo of the request entity_creator_id |
config_key |
string | Policy that was applied |
review_queue_item_id |
string | Review-queue row id when one was created |
texts |
object | Per-label verdicts. Each value is an Analyze text field (id, action or error, classifications) |
custom |
object | Echo of the request custom metadata |
moderation.image_analysis.complete
Fired once per async Analyze request that included images.
{
"type": "moderation.image_analysis.complete",
"entity_type": "listing",
"entity_id": "listing_42",
"entity_creator_id": "user_42",
"config_key": "catalog-v1",
"review_queue_item_id": "rqi_abc",
"images": {
"main_photo": {
"id": "img_42_main",
"action": "flag",
"confidence": 0.924,
"classifications": [
{
"name": "Explicit Nudity",
"confidence": 0.924,
"subclassifications": [
{ "name": "Female Nudity", "confidence": 0.91 }
]
}
]
}
},
"custom": { "vendor_id": "v_acme" }
}| Key | Type | Description |
|---|---|---|
type |
string | Always "moderation.image_analysis.complete" |
entity_type |
string | Canonical entity type from the request |
entity_id |
string | Echo of the request entity_id |
entity_creator_id |
string | Echo of the request entity_creator_id |
config_key |
string | Policy that was applied |
review_queue_item_id |
string | Review-queue row id when one was created |
images |
object | Per-label verdicts. Each value is an Analyze image field (id, action or error, classifications) |
custom |
object | Echo of the request custom metadata |
moderation.analysis.failed
Fired only after Analyze has already acknowledged the request with 201 pending. Stream could not emit a usable *.complete for the listed items. This is not a clean verdict.
{
"type": "moderation.analysis.failed",
"entity_type": "listing",
"entity_id": "listing_42",
"entity_creator_id": "user_42",
"config_key": "catalog-v1",
"content_ids": {
"caption": "cap_42",
"main_photo": "img_42_main"
},
"custom": { "vendor_id": "v_acme" }
}| Key | Type | Description |
|---|---|---|
type |
string | Always "moderation.analysis.failed" |
entity_type |
string | Canonical entity type from the request |
entity_id |
string | Echo of the request entity_id |
entity_creator_id |
string | Echo of the request entity_creator_id |
config_key |
string | Policy the request targeted |
content_ids |
object | Echo of the request content_ids for the unscreened work. May be modality-scoped if only texts or images failed |
custom |
object | Echo of the request custom metadata |
Retry only the listed content_ids. A sibling *.complete can still arrive for the other modality.
Rule-triggered webhooks
Use the Rule Builder to receive a webhook only when a chosen condition matches, for example a specific harm label. Stream sends moderation_rule.triggered for that rule. Other messages do not produce this event.
This is the scoped alternative to subscribing to every review-queue item. Pair it with webhook_only when you want a notification, or with a user/content action when you also want Stream to enforce something.
How it works
- Create a rule whose conditions describe the harms (or other signals) you care about.
- Set the rule action to
webhook_only, or to a user/content action such asflag_user. - Subscribe an event hook to
moderation_rule.triggeredwith productmoderation.
When a check evaluates that rule and the conditions pass, Stream sends one moderation_rule.triggered event. Rule actions run in addition to the action already configured on the policy for that label.
Content rule: notify on a harm label
A content rule with text_content evaluates the current message, activity, or comment. It fires as soon as the listed labels are present.
{
"name": "High-profile harms",
"description": "Notify when selected harm labels are detected",
"rule_type": "content",
"enabled": true,
"config_keys": ["chat:messaging", "chat:livestream"],
"conditions": [
{
"type": "text_content",
"text_content_params": {
"harm_labels": ["UNDERAGE_USER", "SCAM"]
}
}
],
"action": {
"type": "webhook_only"
}
}| Field | Description |
|---|---|
rule_type |
"content" evaluates the current piece of content. |
config_keys |
Policies this rule applies to. An empty list applies the rule app-wide. |
text_content_params.harm_labels |
Labels that satisfy the condition. Any listed label is enough. |
text_content_params.severity |
Optional minimum severity (LOW, MEDIUM, HIGH, CRITICAL). |
action.type |
"webhook_only" sends the event and does not apply a further enforcement action. |
Create or update the rule with POST /api/v2/moderation/moderation_rule. Enable the Rule Builder on the same config:
{
"key": "chat:messaging",
"rule_builder_config": {
"enabled": true
}
}await client.moderation.upsertModerationRule({
name: "High-profile harms",
description: "Notify when selected harm labels are detected",
config_keys: ["chat:messaging", "chat:livestream"],
enabled: true,
rule_type: "content",
conditions: [
{
type: "text_content",
text_content_params: {
harm_labels: ["UNDERAGE_USER", "SCAM"],
},
},
],
action: { type: "webhook_only" },
});Other conditions on a content rule work the same way: blocklist_match, contains_url, image harm_labels, and so on. See Rules.
User rule: act on the account
A user rule with text_rule counts matching labels for one user over a time window. Use this when the signal should follow the account rather than a single piece of content.
{
"name": "Repeat high-severity insult",
"rule_type": "user",
"enabled": true,
"config_keys": ["chat:messaging"],
"conditions": [
{
"type": "text_rule",
"text_rule_params": {
"threshold": 1,
"time_window": "1h",
"harm_labels": ["INSULT"],
"severity": "HIGH"
}
}
],
"action": {
"type": "flag_user"
}
}threshold: 1 fires on the first matching message. Increase it to require a repeated pattern. The same moderation_rule.triggered event is sent when the rule fires.
Flood rule: notify on repeated copy
A flood rule with webhook_only notifies you about repeated identical or similar text without acting on the messages. It fires once each time the count reaches a multiple of threshold (with a threshold of 100: at copy 100, 200, 300...), not on every copy past it. Flood rules with a content action (flag, remove, shadow_block) are unchanged and still act on every copy at or past the threshold.
With track_across_users, the count is copies of the same text from anyone, and each webhook lists the accounts that sent it since the previous webhook in matched_user_ids. This catches coordinated spam from many accounts that each post only once or twice.
{
"name": "Scam ring",
"rule_type": "flood",
"enabled": true,
"config_keys": ["custom:game-chat-family", "custom:game-chat-standard"],
"conditions": [
{
"type": "flood_identical",
"flood_identical_params": {
"threshold": 20,
"time_window": "10m",
"track_across_users": true
}
}
],
"action": {
"type": "webhook_only"
}
}With this rule, 60 accounts posting the same text within 10 minutes produce three webhooks, each with the 20 accounts behind its copies. Accounts in a final run below the next multiple are not reported when the window ends.
Labels API: threshold webhooks
The Labels API also evaluates rules, so a per-player threshold can send a webhook while the API keeps returning labels synchronously. A request is evaluated when:
- it sends
user_idandpolicy, and is not adry_run; - the rule is enabled, uses
webhook_only, and lists that policy inconfig_keys(application-wide rules are not evaluated on this path); - every condition has a time window and is one of
text_rule,content_count_rule,user_identical_content_count,flood_identical, orflood_similar.
Counts are kept per user_id and per request category, and separately for messages and usernames (content_type: username). Policies listed on one rule share the count. A non-flood rule fires at most once per time window per player; flood rules fire once per threshold multiple as described above. Nothing is written to the user, the review queue, or bans, and the Labels response is never changed.
{
"name": "Repeated scam",
"rule_type": "user",
"enabled": true,
"config_keys": ["custom:game-chat-family", "custom:game-chat-standard"],
"conditions": [
{
"type": "text_rule",
"text_rule_params": {
"threshold": 3,
"time_window": "30m",
"harm_labels": ["SCAM"]
}
}
],
"action": {
"type": "webhook_only"
}
}A Labels API fire looks like this:
{
"type": "moderation_rule.triggered",
"created_at": "2026-10-26T15:20:11.104Z",
"rule": {
"id": "a021b779-4430-4139-b76a-843095c72735",
"name": "Scam ring",
"type": "flood",
"description": ""
},
"entity_id": "msg-1204",
"entity_type": "moderation:label",
"user_id": "1-234",
"category": "brawlstars",
"app_id": 12345,
"scope": ["custom:game-chat-family", "custom:game-chat-standard"],
"threshold": 20,
"time_window": "10m",
"triggered_actions": ["webhook_only"],
"matched_contents": [
{
"type": "message",
"id": "msg-1204",
"text": "Free gems at YT_Scammer",
"published_at": "2026-10-26T15:20:11.101Z"
}
],
"matched_user_ids": ["1-101", "1-102", "1-234"]
}Subscribe to rule events
Configure the hook in the dashboard (Moderation → Preferences → Webhook & SNS/SQS) or via event_hooks on the app:
{
"hook_type": "webhook",
"enabled": true,
"product": "moderation",
"event_types": ["moderation_rule.triggered"],
"webhook_url": "https://example.com/moderation/rules"
}product must be moderation (or all). If event_types omits moderation_rule.triggered, the event is dropped.
The payload is described under moderation_rule.triggered.
Shapes
ReviewQueueItem Shape
This shape contains the following fields:
| Key | Type | Description | Possible Values |
|---|---|---|---|
id |
string | Unique identifier of the review queue item | |
created_at |
datetime | Timestamp when the review queue item was created | |
updated_at |
datetime | Timestamp when the review queue item was last updated | |
entity_type |
string | Describes the type of entity under review | Default types include:
entity_type can be any unique string. |
entity_id |
string | The id of the message, activity or reaction | |
moderation_payload |
object | exact content which was sent for auto moderation E.g., { texts: ["fuck you"], images: ["https://sampleimage.com/test.jpg"], videos: [] } |
|
status |
string | Review queue item status. This is not the Check API status (completed / pending). |
|
recommended_action |
Action recommended by stream moderation engines for the entity/content. | Possible values are as following:
|
|
completed_at |
datetime | Timestamp when all the moderation engines finished assessing the content | |
languages |
array | ||
severity |
int | ||
ai_text_severity |
string | Possible values are as following:
|
|
reviewed_at |
datetime | Time at which the entity was reviewed | |
reviewed_by |
string | Id of the moderator who reviewed the item. This value is set when moderator takes an action on review queue item from dashboard | |
message (only applicable to Chat product) |
object | Chat message object under review | |
call (only applicable to Video call moderation) |
object | Call object from which moderated frames are taken | |
entity_creator |
object | The user who created the entity under review. In case of chat, its the user who sent the message under review. In case of activity feeds, its the actor of the activity/reaction | |
entity_creator_id |
string | The id of the user who created the entity under review. In case of chat, its the user who sent the message under review. In case of activity feeds, its the actor of the activity/reaction | |
flags |
array | List of reasons the item was flagged for moderation. | |
flags_count |
int | Count of flags returned | |
bans |
array | List of bans administered to the user | |
config_key |
string | Policy key for the moderation policy |
ModerationEventFlag Shape
This shape contains an array of flags associated with the moderation event. Each flag is represented as follows:
| Key | Type | Description | Possible Values |
|---|---|---|---|
type |
string | Type of the flag. This represents the name of moderation provider which created the flag. |
|
reason |
string | Description of the reason for the flag | |
created_at |
string | Timestamp when the flag was created | |
updated_at |
string | Timestamp when the flag was updated | |
labels |
array | Classification labels for the content under review from moderation engine (e.g., ai_text, ai_image etc) which created this flag | |
result |
object | Complete result object from moderation engine. | |
entity_type |
string | Describes the type of entity under review | Default types include:
entity_type can be any unique string. |
entity_id |
string | The id of the message, activity or reaction | |
moderation_payload |
object | exact content which was sent for auto moderation E.g., { texts: ["fuck you"], images: ["https://sampleimage.com/test.jpg"], videos: [] } |
|
review_queue_item_id |
string | Unique identifier of the review queue item | |
user_id |
string | Id of the user that reported the content |
ModerationEventActionLog Shape
This shape logs actions taken during the moderation event:
| Key | Type | Description | Possible Values |
|---|---|---|---|
id |
string | uuid for action log | Unique identifier for the action log |
created_at |
datetime | Timestamp when this action was performed | |
type |
string | Type of action performed on the review queue item. |
|
user_id |
string | Id of the user (or moderator) who performed the action | |
reason |
string | Reason attached by moderator for the action. This can be any string value | |
custom |
object | Additional data regarding the action. | In case of ban type action, custom object will contain following properties:
delete_user action, custom object will contain following properties:
delete_reaction or delete_message action, custom object will contain following properties:
|
target_user_id |
string | This is same as entity creator id of the review queue item. |
Moderation Check Completed Event Payload Structure
When a moderation check completes, the webhook payload is structured as follows:
{
"entity_id": "string",
"entity_type": "string",
"recommended_action": "keep | flag | remove",
"review_queue_item_id": "string",
"type": "moderation_check.completed",
"created_at": "timestamp",
"received_at": "timestamp"
}This shape contains the following fields:
| Key | Type | Description | Possible Values |
|---|---|---|---|
entity_id |
string | The id of the entity that was moderated (e.g., message id, activity id, reaction id, or user id). | |
entity_type |
string | Describes the type of entity that was moderated. | Default types include:
entity_type can be any unique string. |
recommended_action |
string | Final action recommended by Stream’s moderation engines for the entity/content. | Possible values are as following:
|
review_queue_item_id |
string | Unique identifier of the associated review queue item created for this moderation check. | |
type |
string | Event type | moderation_check.completed |
created_at |
datetime | Timestamp when the event was created | |
received_at |
datetime | Timestamp when the event was received by your webhook endpoint |