The Appeal API allows users to submit appeals against moderation decisions and enables moderators to review and decide on those appeals. Appeals can be submitted against review queue items (such as deleted messages, activities, or reactions) or against user bans (both global and channel-specific bans).
This endpoint allows users to submit an appeal against a moderation decision. You can appeal against:
Deleted Content
Blocked content
User Bans: Appeals against user bans (both global bans and channel-specific bans)
For each appeal, there must be an associated review queue item or a user ban. When appealing against content, only the user who created the content can submit an appeal. When appealing against a user ban, only the banned user can submit an appeal.
await client.moderation.appeal({ appeal_reason: "I believe my message was wrongfully blocked", entity_id: contentEntityId, entity_type: "stream:chat:v1:message", attachments: ["imageurl"], user_id: userId,});
client.moderation().appeal( appeal_reason="I believe my message was wrongfully blocked", entity_id=content_entity_id, entity_type="stream:chat:v1:message", attachments=["imageurl"], user_id=user_id,)
client.Moderation().Appeal(ctx, &getstream.AppealRequest{ AppealReason: "I believe my message was wrongfully blocked", EntityID: contentEntityId, EntityType: "stream:chat:v1:message", Attachments: []string{"imageurl"}, UserID: getstream.PtrTo(userId),})
client.moderation().appeal(AppealRequest.builder() .appealReason("I believe my message was wrongfully blocked") .entityId(contentEntityId) .entityType("stream:chat:v1:message") .attachments(List.of("imageurl")) .userId(userId) .build()).execute();
$client->moderation()->appeal(new AppealRequest( appealReason: 'I believe my message was wrongfully blocked', entityId: $contentEntityId, entityType: 'stream:chat:v1:message', attachments: ['imageurl'], userId: $userId,));
client.moderation.appeal(GetStream::Generated::Models::AppealRequest.new( appeal_reason: "I believe my message was wrongfully blocked", entity_id: content_entity_id, entity_type: "stream:chat:v1:message", attachments: ["imageurl"], user_id: user_id))
await client.Moderation.AppealAsync(new AppealRequest{ AppealReason = "I believe my message was wrongfully blocked", EntityId = contentEntityId, EntityType = "stream:chat:v1:message", Attachments = new List<string> { "imageurl" }, UserId = userId,});
Unique identifier of the entity being appealed. For user bans, this should be the user ID. For content, this should be the message/activity/reaction ID.
entity_type
true
string
Type of entity being appealed. Examples: stream:chat:v1:message, stream:feeds:v2:activity, stream:feeds:v2:reaction, stream:user (for user bans)
appeal_reason
true
string
Explanation for why the content is being appealed. Maximum 500 characters.
attachments
false
array
Array of attachment URLs (e.g., images) providing evidence for the appeal. Maximum 10 attachments.
This endpoint allows you to query appeals with filters, sorting, and pagination. When used from the client-side, querying appeals is automatically limited to appeals created by the authenticated user.
The Submit Action endpoint can be used to accept or reject appeals when taking moderation actions. Most moderation actions automatically accept an appeal if one exists for the review queue item.
The following actions automatically accept an appeal if one exists:
unban: Unbanning a user
mark_reviewed: Marking an item as reviewed
restore: Restoring deleted content
unblock: Unblocking blocked content
When performing these actions, you can optionally provide a decision_reason to explain why the appeal was accepted:
// Approve appeal while unbanning userawait client.moderation.submitAction({ action_type: "unban", appeal_id: "appeal_id", unban: { decision_reason: "I am unbanning a user", // channel_cids: ["cid1", "cid2"], // optional: for channel-specific unbans }, user_id: moderatorId,});// Approve appeal while unblocking contentawait client.moderation.submitAction({ action_type: "unblock", item_id: "item_id", unblock: {}, decision_reason: "I am unblocking content", user_id: moderatorId,});
# Approve appeal while unbanning userclient.moderation().submit_action( action_type="unban", appeal_id="appeal_id", unban={"decision_reason": "I am unbanning a user"}, user_id=moderator_id,)# Approve appeal while unblocking contentclient.moderation().submit_action( action_type="unblock", item_id="item_id", unblock={}, decision_reason="I am unblocking content", user_id=moderator_id,)
// Approve appeal while unbanning userclient.Moderation().SubmitAction(ctx, &getstream.SubmitActionRequest{ ActionType: "unban", AppealID: getstream.PtrTo("appeal_id"), Unban: &getstream.UnbanActionRequest{ DecisionReason: getstream.PtrTo("I am unbanning a user"), }, UserID: getstream.PtrTo(moderatorId),})// Approve appeal while unblocking contentclient.Moderation().SubmitAction(ctx, &getstream.SubmitActionRequest{ ActionType: "unblock", ItemID: getstream.PtrTo("item_id"), Unblock: &getstream.UnblockActionRequest{}, DecisionReason: getstream.PtrTo("I am unblocking content"), UserID: getstream.PtrTo(moderatorId),})
// Approve appeal while unbanning userclient.moderation().submitAction(SubmitActionRequest.builder() .actionType("unban") .appealId("appeal_id") .unban(UnbanActionRequest.builder() .decisionReason("I am unbanning a user") .build()) .userId(moderatorId) .build()).execute();// Approve appeal while unblocking contentclient.moderation().submitAction(SubmitActionRequest.builder() .actionType("unblock") .itemId("item_id") .unblock(UnblockActionRequest.builder().build()) .decisionReason("I am unblocking content") .userId(moderatorId) .build()).execute();
// Approve appeal while unbanning user$client->moderation()->submitAction(new SubmitActionRequest( actionType: 'unban', appealId: 'appeal_id', unban: new UnbanActionRequest( decisionReason: 'I am unbanning a user', ), userId: $moderatorId,));// Approve appeal while unblocking content$client->moderation()->submitAction(new SubmitActionRequest( actionType: 'unblock', itemId: 'item_id', unblock: new UnblockActionRequest(), decisionReason: 'I am unblocking content', userId: $moderatorId,));
# Approve appeal while unbanning userclient.moderation.submit_action(GetStream::Generated::Models::SubmitActionRequest.new( action_type: "unban", appeal_id: "appeal_id", unban: GetStream::Generated::Models::UnbanActionRequest.new( decision_reason: "I am unbanning a user" ), user_id: moderator_id))# Approve appeal while unblocking contentclient.moderation.submit_action(GetStream::Generated::Models::SubmitActionRequest.new( action_type: "unblock", item_id: "item_id", unblock: GetStream::Generated::Models::UnblockActionRequest.new, decision_reason: "I am unblocking content", user_id: moderator_id))
// Approve appeal while unbanning userawait client.Moderation.SubmitActionAsync(new SubmitActionRequest{ ActionType = "unban", AppealId = "appeal_id", Unban = new UnbanActionRequest { DecisionReason = "I am unbanning a user", }, UserId = moderatorId,});// Approve appeal while unblocking contentawait client.Moderation.SubmitActionAsync(new SubmitActionRequest{ ActionType = "unblock", ItemId = "item_id", Unblock = new UnblockActionRequest(), DecisionReason = "I am unblocking content", UserId = moderatorId,});
To reject an appeal, use the reject_appeal action type:
await client.moderation.submitAction({ action_type: "reject_appeal", appeal_id: "appeal_id", reject_appeal: { decision_reason: "I am rejecting your appeal", }, user_id: moderatorId,});
client.moderation().submit_action( action_type="reject_appeal", appeal_id="appeal_id", reject_appeal={"decision_reason": "I am rejecting your appeal"}, user_id=moderator_id,)
client.Moderation().SubmitAction(ctx, &getstream.SubmitActionRequest{ ActionType: "reject_appeal", AppealID: getstream.PtrTo("appeal_id"), RejectAppeal: &getstream.RejectAppealActionRequest{ DecisionReason: getstream.PtrTo("I am rejecting your appeal"), }, UserID: getstream.PtrTo(moderatorId),})
client.moderation().submitAction(SubmitActionRequest.builder() .actionType("reject_appeal") .appealId("appeal_id") .rejectAppeal(RejectAppealActionRequest.builder() .decisionReason("I am rejecting your appeal") .build()) .userId(moderatorId) .build()).execute();
$client->moderation()->submitAction(new SubmitActionRequest( actionType: 'reject_appeal', appealId: 'appeal_id', rejectAppeal: new RejectAppealActionRequest( decisionReason: 'I am rejecting your appeal', ), userId: $moderatorId,));
client.moderation.submit_action(GetStream::Generated::Models::SubmitActionRequest.new( action_type: "reject_appeal", appeal_id: "appeal_id", reject_appeal: GetStream::Generated::Models::RejectAppealActionRequest.new( decision_reason: "I am rejecting your appeal" ), user_id: moderator_id))
await client.Moderation.SubmitActionAsync(new SubmitActionRequest{ ActionType = "reject_appeal", AppealId = "appeal_id", RejectAppeal = new RejectAppealActionRequest { DecisionReason = "I am rejecting your appeal", }, UserId = moderatorId,});
submitted: The appeal has been submitted and is awaiting review
accepted: The appeal has been accepted by a moderator
rejected: The appeal has been rejected by a moderator
The decided_by field is automatically populated with the moderator's user ID when an appeal is accepted or rejected. This field is not shown to users but is tracked internally for audit purposes.
// Query items with submitted appealsawait client.Moderation.QueryReviewQueueAsync(new QueryReviewQueueRequest{ Filter = new Dictionary<string, object> { { "appeal", true }, { "appeal_status", "submitted" } }, Sort = new List<SortParam> { new SortParam { Field = "created_at", Direction = -1 } }, Limit = 20,});// Query items with a specific appealawait client.Moderation.QueryReviewQueueAsync(new QueryReviewQueueRequest{ Filter = new Dictionary<string, object> { { "appeal_id", new Dictionary<string, object> { { "$eq", "appeal_id" } } } }, Sort = new List<SortParam> { new SortParam { Field = "created_at", Direction = -1 } }, Limit = 20,});
Instead of moving review queue items from "Reviewed" to "Inbox" or vice versa, you can query review queue items with appeal status filters. This approach avoids potential problems that could arise from changing the status of review queue items and provides a more flexible way to organize your moderation workflow.
Appeal Reason: Provide clear and concise explanations in the appeal text (max 500 characters). This helps moderators understand the context quickly.
Attachments: Use attachments to provide evidence supporting your appeal (e.g., screenshots, context about why content was flagged incorrectly).
Decision Reasons: When moderators accept or reject appeals, always provide a decision_reason to help users understand the decision.
Filtering: Use appeal status filters in Query Review Queue to organize your moderation dashboard effectively, separating items with pending appeals from other items.
Client-Side Restrictions: Remember that client-side requests automatically filter appeals to only show appeals created by the authenticated user. Use server-side requests for moderator dashboards that need to see all appeals.