Rules
Rules run an action when their conditions match. Scope a rule to specific policies with config_keys, or leave that list empty to apply it app-wide.
Upsert Rule
Create or update a moderation rule. This endpoint uses POST /api/v2/moderation/moderation_rule and allows you to define the rule name, conditions, actions, and other configuration in a single request.
await client.UpsertModerationRuleAsync(new UpsertModerationRuleRequest
{
Name = "ban_on_severe_harassment",
RuleType = "user",
ConfigKeys = new List<string> { "chat:messaging" },
Logic = "AND",
Action = new RuleBuilderAction
{
Type = "ban_user",
BanOptions = new BanOptions { Duration = 3600, Reason = "Severe harassment" },
},
Conditions = new List<RuleBuilderCondition>
{
new()
{
Type = "text_rule",
TextRuleParams = new TextRuleParameters
{
HarmLabels = new List<string> { "HATRED" },
Severity = "critical",
},
},
},
Enabled = true,
Description = "Auto-ban users for critical harassment",
});Request Parameters
| key | required | type | description |
|---|---|---|---|
| name | true | string | Unique name for the rule. Rules are keyed by name. There is no id field. |
| rule_type | true | string | user, content, call, or flood. |
| config_keys | false | array | Policy keys this rule applies to. Empty means every policy. |
| action | false | object | type is one of ban_user, flag_content, block_content, and the rest of the action enum. Ban details go in ban_options. |
| conditions | false | array | Typed conditions. Each item sets type and the matching *_params object. Flat label / severity keys are not a condition. |
| groups | false | array | Condition groups for complex logic. |
| enabled | false | boolean | Whether the rule is active. |
| description | false | string | Human-readable description of the rule. |
| cooldown_period | false | string | Cooldown period between rule triggers (e.g., "1h", "24h"). |
| team | false | string | Team identifier for multi-tenancy. |
Response
| key | type | description |
|---|---|---|
| rule | object | The created or updated moderation rule. |
Get Rule
Retrieve a single moderation rule by its ID. This endpoint uses GET /api/v2/moderation/moderation_rule/{id} and returns the full rule object including its conditions, actions, and metadata.
await client.GetModerationRuleAsync("rule_id");Request Parameters
| key | required | type | description |
|---|---|---|---|
| id | true | string | The unique identifier of the moderation rule. |
Response
| key | type | description |
|---|---|---|
| rule | object | The moderation rule. |
Delete Rule
Delete a moderation rule by its ID. This endpoint uses DELETE /api/v2/moderation/moderation_rule/{id} and permanently removes the rule from the system.
await client.DeleteModerationRuleAsync("rule_id");Request Parameters
| key | required | type | description |
|---|---|---|---|
| id | true | string | The unique identifier of the moderation rule to delete. |
Response
| key | type | description |
|---|---|---|
| duration | string | Request duration. |
Query Rules
Query moderation rules with optional filters, sorting, and pagination. This endpoint uses POST /api/v2/moderation/moderation_rules and supports standard query patterns for listing and searching rules.
await client.QueryModerationRulesAsync(new QueryModerationRulesRequest
{
Filter = new Dictionary<string, object> { { "enabled", true } },
Sort = new List<SortParam>
{
new() { Field = "created_at", Direction = -1 },
},
Limit = 10,
});Request Parameters
| key | required | type | description |
|---|---|---|---|
| filter | false | object | Filter conditions. |
| sort | false | array | Sort parameters. |
| limit | false | number | Maximum rules to return. |
| next | false | string | Cursor for pagination. |
Response
| key | type | description |
|---|---|---|
| rules | array | List of moderation rules. |
| next | string | Next cursor for pagination. |
Rule Actions Reference
The following table lists all available action types that can be assigned to a moderation rule. Each action defines what happens when the rule conditions are met.
| action type | description |
|---|---|
| flag_content | Flag the content for manual review. |
| block_content | Block the content. |
| shadow_content | Shadow the content so other users do not see it. |
| bounce_content | Bounce the message back to the sender. |
| bounce_flag_content | Bounce the message and flag it. |
| bounce_remove_content | Bounce the message and remove it. |
| ban_user | Ban the user. ban_options.duration is seconds (3600 is one hour); omit it for a ban that does not expire. shadow_ban hides the ban from the user. ip_ban also bans that IP. Live video rules use the actions on Live video and audio instead of this list. |
| flag_user | Flag the user. |
| webhook_only | Send moderation_rule.triggered without a further enforcement action. See Rule-Triggered Webhooks. |
Global vs policy-specific rules
Global rules
Global rules apply to all the content in your application, as long as it reaches the moderation system.
{
"name": "Global Spam Detection",
"description": "Applies to every policy",
"team": "moderation",
"config_keys": [], // Empty array = global rule
"rule_type": "user",
"enabled": true,
"cooldown_period": "24h",
"conditions": [
// ... conditions
],
"logic": "AND",
"action": {
// ... action definition
}
}Policy-specific rules
These rules only run for the policies you list in config_keys.
E.g., if you have a rule that only applies to chat, you can set the config_keys to ["chat:messaging", "chat:support"], then counters will only be counted for messages sent in the channel types mentioned.
{
"name": "Chat-Only Rule",
"description": "Only applies to chat policies",
"team": "moderation",
"config_keys": ["chat:messaging", "chat:support"], // These policies only
"rule_type": "user",
"enabled": true,
"cooldown_period": "12h",
"conditions": [
// ... conditions
],
"logic": "AND",
"action": {
// ... action definition
}
}Basic rule structure
Every rule has three main parts:
- Conditions: What behavior to watch for
- Threshold: How many violations before taking action
- Action: What to do when the threshold is reached
Example: Complete Rule Structure
{
"name": "Spam Detection",
"description": "Detects and bans users for spam behavior",
"team": "moderation",
"config_keys": ["chat:messaging", "chat:support"],
"rule_type": "user",
"enabled": true,
"cooldown_period": "24h",
"conditions": [
{
"type": "text_rule",
"text_rule_params": {
"threshold": 5,
"time_window": "1h",
"llm_harm_labels": {
"SCAM": "Fraudulent content, phishing attempts, or deceptive practices",
"PLATFORM_BYPASS": "Content that attempts to circumvent platform moderation systems"
}
}
},
{
"type": "content_count_rule",
"content_count_rule_params": {
"threshold": 50,
"time_window": "1h"
}
}
],
"logic": "OR",
"action": {
"type": "ban_user",
"ban_options": {
"duration": 3600,
"reason": "Spam behavior detected",
"shadow_ban": false,
"ip_ban": false
}
}
}This rule:
- Watches for spam and advertising content
- Triggers when a user posts 5+ spam messages within 1 hour or 50+ messages within 1 hour
- Bans the user for 1 hour when triggered
Condition types
These are the conditions you can put on a rule. Send them on Upsert Rule. A rule can have at most 5 conditions, and a policy at most 3 rules.
Every condition type. Conditions in one rule combine with AND or OR. The sections after the table document the types that take extra fields.
| type | What it matches |
|---|---|
text_rule |
NLP or LLM labels on text. |
image_rule |
Image labels. |
video_rule |
Video-file labels. |
text_content |
Text harm labels on one piece of content. |
image_content |
Image harm labels on one piece of content. |
video_content |
Video harm labels on one piece of content. |
ocr_content |
Text harm labels on text read out of one image. |
content_custom_property |
A custom field on the entity. Stream Chat and Feeds only. |
content_country_code |
The content's countries (country_codes on Check and Analyze). |
content_custom_property_count |
How often a user's content has a matching custom field. |
content_count_rule |
How many messages a user sent in a window. |
content_flag_count_rule |
How many times a user's content was flagged in a window. |
user_rule |
A user-level harm label. |
user_reaction_count |
How many reactions a user sent, or how many messages they reacted to. |
user_channel_count |
How many channels a user created in a window. |
user_channel_ban_count |
How many distinct channels have banned the user in a window. |
user_flag_count_rule |
How many times the user was flagged in a window. |
user_created_within |
The user's account is newer than the window. |
user_custom_property |
A custom field on the user. |
user_role |
The user's role. |
user_identical_content_count |
How many times a user sent the same text. |
user_identical_image_count |
How many times a user sent the same image bytes. |
channel_message_count_rule |
The channel's lifetime message count. |
flood_identical |
The same text repeated. Can count across users. |
flood_similar |
Near-duplicate text, per user. |
keyframe_rule |
Harm labels on consecutive live-video frames. |
keyframe_ocr_rule |
Harm labels on text read from consecutive frames. |
closed_caption_rule |
Harm labels on consecutive captions. |
call_type_rule |
The Stream call type. |
call_custom_property |
A custom field on the call. |
call_violation_count |
How many live-video rules the user has triggered. |
ip_content_count_rule |
How much content one IP sent. |
ip_flag_count_rule |
How many flags one IP received. |
intent_rule |
A conversation topic from intent_config scored at or above a threshold. |
Reaction velocity
user_reaction_count is a user rule. It fires when one user sends too many reactions in a window (app-wide, not per channel). A reaction has no text to score, so message rules never see this.
count picks the unit:
reactions(default): every reaction eventreacted_messages: each message they reacted to counts once. Stacking several reaction types on one post is still 1.
{
"type": "user_reaction_count",
"user_reaction_count_params": {
"threshold": 10,
"time_window": "1m",
"count": "reacted_messages"
}
}| Field | Required | Description |
|---|---|---|
threshold |
yes | Inclusive. Must be greater than 0. |
time_window |
yes | One of 15s, 30s, 1m, 5m, 30m, 1h, 24h, 7d, 30d. |
count |
no | reactions (default) or reacted_messages. |
Use a user action (ban, flag the user, or a webhook). This only runs when a reaction is moderated.
Identical images
user_identical_image_count is a user rule. It counts how many times the same user posts the same image. Identity comes from the image bytes Stream already downloads for moderation, not the CDN URL, so a re-upload still matches.
{
"type": "user_identical_image_count",
"user_identical_image_count_params": {
"threshold": 3,
"time_window": "5m",
"match": "exact"
}
}| Field | Required | Description |
|---|---|---|
threshold |
yes | Inclusive. Must be at least 1. |
time_window |
yes | One of 15s, 30s, 1m, 5m, 30m, 1h. |
match |
no | exact (default): same file bytes. similar: near-duplicate after resize or re-encode. |
similarity_distance |
no | Only with match: "similar". 1 to 20, default 10. Lower is stricter. |
Images that cannot be fetched are skipped (fail-open).
Flood identical and similar
rule_type: "flood" is for repeated copy. Conditions are flood_identical (exact text after normalization, or media when there is no text) and flood_similar (near-duplicate text). Both default to per user. Actions are the content actions (flag, remove, shadow_block) and webhook_only. There is no cooldown: with a content action, once the threshold is hit every further match is actioned. With webhook_only, the rule sends moderation_rule.triggered once each time the count reaches a multiple of threshold (100, 200, 300... for a threshold of 100), and cross-user rules list the senders since the previous webhook in matched_user_ids. See Rule-triggered webhooks.
{
"type": "flood_identical",
"flood_identical_params": {
"threshold": 5,
"time_window": "30s",
"track_across_users": true,
"min_text_length": 20,
"allowlist": ["gg"]
}
}| Field | Required | Description |
|---|---|---|
threshold |
yes | 2 to 100, inclusive. |
time_window |
yes | One of 15s, 30s, 1m, 2m, 3m, 5m, 10m, 15m, 30m, 1h. |
allowlist |
no | Phrases that do not count, after the same normalization. |
track_across_users |
no | Identical only. Default false is per user. true counts that same text from anyone (coordinated copy-paste). It is total occurrences, not distinct users. |
min_text_length |
no | Identical only. Skip shorter text (3 to 500). Omitted or 0 uses 3. Attachment-only messages still count. |
flood_similar uses the same threshold, time_window, and allowlist, plus similarity_distance (1 to 16). It has no across-users option.
This is separate from policy flood_config, which stays per user and has no track_across_users or min_text_length.
Custom properties (Stream Chat and Feeds only)
content_custom_property matches a key on the moderated entity. Stream Chat sends message.custom. Stream Feeds sends activity and comment custom. A rule on audience_rating can match a message, a post, or a comment. Other products do not send this field.
{
"type": "content_custom_property",
"content_custom_property_params": {
"property_key": "audience_rating",
"operator": "eq",
"expected_value": "kids"
}
}Operators: exists, not_exists, eq, ne, gt, gte, lt, lte, contains, not_contains.
To act on how often a user sends matching custom values, use content_custom_property_count with threshold and time_window.
Country
content_country_code matches the countries the content is aimed at: country_codes on Check and Analyze, or the deprecated country_code.
{
"type": "content_country_code",
"content_country_code_params": {
"operator": "in",
"country_codes": ["DE", "AT"]
}
}| Operator | Matches when |
|---|---|
eq, in |
At least one of the content's countries is in country_codes. |
ne, not_in |
None of the content's countries is in country_codes. Content with no country matches. |
eq and ne take exactly one code; in and not_in take one or more. country_codes is required, and every entry must be an ISO 3166-1 alpha-2 code.
Content with several countries matches when any of them qualifies, so ne with ["US"] does not fire for content aimed at ["US", "DE"]. Content with no text or media, such as a reaction, never matches.
Time windows
Counting conditions accept 15s, 30s, 1m, 5m, 30m, 1h, 24h, 7d, and 30d. Flood conditions use a shorter set: 15s, 30s, 1m, 2m, 3m, 5m, 10m, 15m, 30m, and 1h. Live video cooldowns are 5s, 10s, 1m, 5m, and 10m.
Cooldown Periods
The Rule Builder supports cooldown periods to prevent immediate re-triggering of rules after an action has been taken. This is particularly useful when users are banned and then unbanned by administrators.
When a rule with a cooldown period is triggered and an action is taken (like banning a user), the system records this action with an expiration time. During the cooldown period, the same rule will not trigger again for that user, even if they continue to violate the conditions.
Configuration
Add a cooldown_period field to the rule:
{
"name": "Spam Detection with Cooldown",
"description": "Spam detection rule with 24h cooldown",
"team": "moderation",
"config_keys": [],
"rule_type": "user",
"enabled": true,
"cooldown_period": "24h",
"conditions": [
// ... conditions
],
"action": {
"type": "ban_user",
"ban_options": {
"duration": 3600,
"reason": "Spam behavior detected",
"shadow_ban": false,
"ip_ban": false
}
}
}Example Scenario
- User violates rule: User posts 5 spam messages in 1 hour
- Rule triggers: User gets banned for 1 hour
- Admin unbans user: Administrator manually unbans the user
- User posts again: User immediately posts more spam messages
- Cooldown active: Rule does not trigger again due to 24-hour cooldown
- After cooldown: User can trigger the rule again after 24 hours