Enforcement decisions
An enforcement decision records why staff restricted an account. The route that applies the restriction records it in the same request, so a restriction never exists without its statement of reasons. Suspend user, Warn user, Limit user features and Schedule user deletion with a broken rule as its basis record decisions today, and so does Fluxer itself when a payment fraud signal arrives.
The decision holds the explanation. The restriction itself stays where it always was, so a suspension is still temp_banned_until and the DISABLED flag on the account, a closure is still pending_deletion_at and the DELETED flag, and a feature limit is still its account flag. The account’s enforcement_decision_id names the suspension or closure decision behind the current state.
Every decision sends its notice to the person it is about. There is no switch to withhold it.
Concepts
Section titled “Concepts”| Term | Meaning |
|---|---|
| kind | What the decision did: warning, suspension, closure, feature_limit, content_removal, profile_reset, or community_removal |
| reason code | One of the enforcement deletion reasons. USER_REQUESTED, OTHER and INACTIVITY are refused |
| rule | The policy section the reason code points to. The notice links it |
| handling | standard, protected or restricted, fixed by the reason code. It decides how much detail the notice gives |
| evidence | A snapshot of the subject’s own messages, or for a community the messages posted in it, taken when the decision is made |
| notes | Internal staff notes. The X-Audit-Log-Reason header of the request that made the decision is the first note |
| notice | The statement of reasons the holder receives by email, system DM, or at sign-in |
Handling
Section titled “Handling”| Handling | Reason codes | Notice detail |
|---|---|---|
| standard | every code not listed below | The rule, where and when it happened, and the note to the user |
| protected | 7, 10, 11, 15, 23 | The same, except that messages in direct and group conversations show the month only |
| restricted | 9, 13, 24 | The rule only. No locations and no note to the user |
A restricted reason allows only a permanent suspension and content removal, needs at least one evidence message and both confirmations, and refuses a note to the user. Its evidence keeps the message ID, channel ID, time and attachment count only.
Lifecycle
Section titled “Lifecycle”A decision is active until staff end it as lifted or reversed. A temporary suspension that runs out is ended by Fluxer as expired, with the system user (ID 0) as ended_by and its end as ended_at. That happens when the holder next signs in or in a scheduled task that runs every six hours, whichever comes first, and sends no notice. A suspension that ended as expired can still be ended as reversed, which sends the ended notice.
Two states are derived when it is read and never stored:
expired: an active suspension or feature limit whoseends_athas passed, before Fluxer ends itreplaced: an active suspension or closure that the account’senforcement_decision_idno longer names, because a later decision replaced it
A replacing suspension or closure sends its own notice and no ended email for the one it replaces. A closure replaces a suspension and lifts it in the same write.
Routes whose action is not always enforcement take a basis instead of a decision. Only the enforcement basis records a decision and sends its notice.
| Field | Type | Description |
|---|---|---|
| type | string | enforcement, holder_request or housekeeping |
| decision?1 | enforcement decision input object | Why the subject is restricted |
1 Required for the enforcement type and refused for the others. A bulk job takes a narrower decision, which Bulk jobs describes
enforcement: a rule was brokenholder_request: the account holder or community owner askedhousekeeping: nobody’s conduct is involved, such as inactivity or cleanup
Each route accepts these bases:
| Route | Bases |
|---|---|
| Schedule user deletion | enforcement, holder_request, housekeeping |
| Clear user profile fields | enforcement, holder_request, housekeeping |
| Shred user messages | enforcement, holder_request, housekeeping |
| Delete all user messages | enforcement, holder_request, housekeeping |
| Delete message | holder_request, housekeeping |
| Delete guild | enforcement, holder_request |
The audit entry of each route records the basis in a basis metadata key.
Feature limits
Section titled “Feature limits”Each feature limit decision limits one feature, so ending or expiring one never touches another.
direct_messagessetsSPAMMERand is allowed for reason codes 3, 6, 8, 18 and 21. It has no end.reportssetsREPORT_BANNEDand is allowed for reason code 29. It ends after 90 days by default and after at most 365.purchasessetsPURCHASE_DISABLEDand is allowed for reason codes 12 and 14. It has no end.
A reports limit expires on its own. The next report from the account after ends_at clears REPORT_BANNED and is accepted. A reporter under a reports limit is still told the outcome of reports filed before the limit started.
Removals
Section titled “Removals”- Content removal: Shred user messages snapshots the first 50 entries as the evidence, and every entry for restricted reasons, before it queues the shred job. The entries are the evidence, so
evidenceis refused there. Delete all user messages cannot snapshot an unbounded set first, so it refuses restricted reasons. To remove a single message for a rule, shred it. - Profile reset: the old values of the cleared fields are saved as the evidence’s
profile_values. The cleared fields are the facts, so neither evidence nor a statement is needed. - Community removal: the subject is the community and the owner receives the notice. Evidence must be posted in that community, and linked reports must name it. Restricted reasons are refused, because the removal deletes the content.
Removed content cannot be restored, so these decisions end only as reversed.
Reporter outcomes
Section titled “Reporter outcomes”A decision that resolves linked reports tells each reporter what happened to the item. removed covers a reported message the decision removed, a profile report met by a profile reset, and a community removal. restricted covers a suspension, closure or feature limit. action_taken covers a warning and tells the reporter that Fluxer acted on what they reported, without naming the action or saying the account was restricted. A content removal or profile reset that leaves the reported item in place leaves the report pending. A manual resolution through Update report names the outcome itself, and a duplicate takes the outcome stored on the report it repeats.
Network blocks
Section titled “Network blocks”A permanent suspension or a closure for a broken rule can also block networks the account used in the last 30 days, through block_networks on Suspend user and Schedule user deletion. List network block candidates shows which addresses qualify. The blocks last 90 days unless the request sets another length, belong to the decision, and are removed when it ends or the account’s deletion runs. Blocklists explains how these rows relate to plain IP blocks.
Fluxer never blocks an exempt address or a special-purpose range, such as a private, shared carrier, loopback, link-local or documentation range, and lists those in skipped_ips. It refuses an address on which another account that is not itself suspended or closed was last active, matching IPv6 addresses by their /64, so a block never shuts out people who share a network with the account.
The notice says the account’s networks were blocked and until when, and never names an address. The block screen shows the rule only for rules that read safely to a bystander on the same network: spam, service abuse, disruptive automation, raids, impersonation, adult content outside 18+ spaces, graphic violence, misuse of reporting and copyright. Every other block looks exactly like a plain one.
Automated decisions and the review queue
Section titled “Automated decisions and the review queue”A Stripe early fraud warning or chargeback records a purchases limit with reason code 12, automation automated, payment_at set to the time of the payment, and the system user (ID 0) as decided_by. Fluxer never suspends or closes an account, or blocks its email, because of a payment signal. The decision is placed in the review queue until a person ends it or records a warning, suspension or closure on the same account. A won dispute ends the decision as reversed, with the system user as ended_by.
Enforcement decision input
Section titled “Enforcement decision input”The decision object every restricting route takes.
| Field | Type | Description |
|---|---|---|
| reason_code | integer | An enforcement deletion reason |
| source | string | report when staff reviewed this after a report, own_review otherwise |
| automation | string | flagged when automated tools flagged it before staff decided, none otherwise |
| report_ids?1 | array[snowflake] | Reports about the subject that the decision resolves (at most 20) |
| evidence?2 | array[object] | channel_id and message_id of messages the decision relies on (at most 50) |
| statement?3 | string | Note to the user, shown in the notice (1-500 characters) |
| rights_holder?4 | string | The rights holder or the right concerned (1-200 characters) |
| confirmations?5 | array[string] | evidence_preserved and reports_filed_or_not_required |
1 Required when source is report. Each report must name the subject, the reported account or the reported community. Messages in a report’s snapshot that the subject wrote, or that were posted in the community, become evidence too, and other messages are dropped. Pending reports are resolved as actioned and their reporters are told the outcome
2 Every message must exist and be the subject’s own, or for a community be posted in it. A message in a direct or group conversation is accepted only when a linked report covers that conversation. Attachments are kept as file name, size and type, never as files
3 The decision needs at least one evidence message or a statement, except for a profile reset. Restricted reasons refuse it
4 Required for code 31 (COPYRIGHT) and refused for every other code
5 Both are required for restricted reasons. They are added to the first note
The X-Audit-Log-Reason header is required on every route that takes a decision and becomes its first note.
Validation
Section titled “Validation”Fluxer checks ACLs before it looks anything up, so a caller without the ACL learns nothing about the messages or reports it names. Evidence needs message:lookup, reports need report:view and report:resolve, and block_networks needs ban:ip:add and user:view:ip. Each other failure is a 400 INVALID_FORM_BODY whose error has one of these codes.
ENFORCEMENT_INTERNAL_NOTE_REQUIRED: theX-Audit-Log-Reasonheader is missingENFORCEMENT_KIND_NOT_ALLOWED: the reason code does not allow this kind, a bot is given anything other than a suspension or closure, or a restricted reason is used to delete all of an account’s messagesENFORCEMENT_LIMIT_NOT_ALLOWED: the reason code does not allow this feature limitENFORCEMENT_LIMIT_END_INVALID: a reports limit ends in the past or more than 365 days from now, or another limit is given an endENFORCEMENT_REPORT_REQUIRED:sourceisreportand no report is linkedENFORCEMENT_REPORT_MISMATCH: a linked report does not exist or is about someone elseENFORCEMENT_EVIDENCE_NOT_ALLOWED: a shred is given evidence messages, because its entries are the evidenceENFORCEMENT_EVIDENCE_NOT_FOUND: an evidence message does not existENFORCEMENT_EVIDENCE_NOT_SUBJECT: an evidence message is not the subject’s own, or not posted in the communityENFORCEMENT_EVIDENCE_DM_UNREPORTED: an evidence message is in a conversation no linked report coversENFORCEMENT_FACTS_REQUIRED: there is no evidence message and no statementENFORCEMENT_EVIDENCE_REQUIRED: a restricted reason has no evidence, orremove_evidencehas nothing to removeENFORCEMENT_STATEMENT_NOT_ALLOWED: a restricted reason has a statementENFORCEMENT_PERMANENT_REQUIRED: a restricted reason is given a temporary suspensionENFORCEMENT_CONFIRMATIONS_REQUIRED: a restricted reason is missing a confirmationENFORCEMENT_RIGHTS_HOLDER_REQUIRED: code 31 has norights_holderENFORCEMENT_RIGHTS_HOLDER_NOT_ALLOWED: another code has arights_holderENFORCEMENT_NETWORK_BLOCK_NOT_ALLOWED:block_networksis given with a temporary suspension or a closure that is not for a broken ruleENFORCEMENT_NETWORK_UNKNOWN: the account did not use an address inblock_networksin the last 30 daysENFORCEMENT_NETWORK_SHARED: another account that is not suspended or closed was last active on an address inblock_networks
Order of effects
Section titled “Order of effects”- Fluxer validates the input and snapshots the evidence.
- It stores the decision with
notice_statuspending, together with its evidence and first note. - It applies the restriction. A suspension writes
enforcement_decision_idin the same account write astemp_banned_until, and a closure in the same write aspending_deletion_at. A content removal queues its shred job only now, after the evidence is stored. When this step fails, the decision is deleted and nothing is sent. - It resolves the linked reports.
- It sends the notice and stores what it sent.
A failure after step 3 leaves the restriction and its decision in place with notice_status pending.
Notice delivery
Section titled “Notice delivery”Suspensions and closures are sent by email only. Every other kind is also sent as a system DM. The decision records undelivered when no channel reaches the holder, because the account has no email address or the instance has email turned off and the kind sends no system DM. A suspension or closure that was not sent is delivered when the holder next signs in. The sign-in records sent with the channel sign_in, the request locale, the text and the time, and the appeal deadline counts from then. A bot’s notice goes to its owner, and a community’s to its owner.
The email subject names the kind of decision, except for a restricted reason, where it is neutral. The system DM leaves out the greeting and sign-off and shows links as Markdown links.
On a self-hosted instance the notice has no appeal section and no out-of-court redress lines, and it uses the instance’s own name.
Enforcement decision object
Section titled “Enforcement decision object”Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| decision_id | snowflake | The ID of the decision, which the holder quotes in an appeal |
| subject_type | string | user or guild |
| subject_id | snowflake | The ID of the account, bot or community |
| subject_name | ?string | The bot username or community name when the decision was made, null for a person’s own account |
| recipient_user_id | ?snowflake | Who receives the notice, or null when nobody can |
| kind | string | What the decision did |
| reason_code | integer | Deletion reason |
| source | string | report or own_review |
| automation | string | none, flagged or automated |
| statement | ?string | Note to the user |
| rights_holder | ?string | The rights holder or the right concerned |
| ends_at | ?ISO8601 timestamp | When a suspension or feature limit ends, null when permanent |
| deletion_at | ?ISO8601 timestamp | When a closed account is deleted |
| limit | ?string | direct_messages, reports or purchases for a feature limit |
| profile_fields | array[string] | The fields a profile reset cleared |
| removed_message_count | ?integer | Messages the decision removed |
| removed_all_messages | boolean | Whether every message of the account was removed |
| payment_at | ?ISO8601 timestamp | The charge a payment decision is about |
| report_ids1 | array[snowflake] | Linked reports |
| decided_by | snowflake | The staff member who decided, or 0 for an automated decision |
| created_at | ISO8601 timestamp | When the decision was made |
| status | string | active or ended |
| end_outcome | ?string | lifted, reversed or expired |
| ended_at | ?ISO8601 timestamp | When the decision ended |
| ended_by | ?snowflake | Who ended it |
| end_statement | ?string | Note to the user sent with the ended email |
| notice_status | string | pending, sent or undelivered |
| notice_channels | array[string] | email, system_dm and sign_in |
| notice_locale | ?string | The locale the notice was written in |
| notice_text | ?string | The plain text that was sent |
| notice_sent_at | ?ISO8601 timestamp | When the holder received the notice. The 60-day appeal window counts from here |
| expired | boolean | See Lifecycle |
| replaced | boolean | See Lifecycle |
1 Empty without report:view
Get enforcement decision adds these fields.
| Field | Type | Description |
|---|---|---|
| notes | array[object] | Internal notes in order, each with note_id, author_id, body and created_at |
| evidence1 | array[object] | Snapshot messages, each with message_id, channel_id, timestamp, content, attachments and attachment_count |
| profile_values1 | ?object | Profile values before a reset |
| network_blocks2 | array[object] | The decision’s network blocks that have not expired, each with ip and expires_at |
| notice | object | The notice as the holder sees it now, in English |
1 Message content, attachment details and profile values are null without message:lookup. Evidence and notes are deleted a year after the decision, and the decision itself is kept
2 Each ip is masked to its /24 or /64 without user:view:ip
List enforcement decisions
Section titled “List enforcement decisions”GET/v1/admin/enforcement-decisionsLists the decisions about one account or community, newest first, without notes or evidence, or the review queue. Requires enforcement:view.
Query parameters
Section titled “Query parameters”| Field | Type | Description |
|---|---|---|
| review_queue?1 | boolean | List the automated decisions waiting for a person instead of one subject |
| subject_type? | string | user or guild |
| subject_id? | snowflake | The ID of the account or community |
| limit? | integer | Maximum number of decisions (1-100, default 50) |
| before? | snowflake | Return decisions older than this decision |
1 Without review_queue=true, subject_type and subject_id are required
Response body
Section titled “Response body”| Field | Type | Description |
|---|---|---|
| decisions | array[enforcement decision object] | The decisions, without the fields only the single read returns |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | response body | The decisions were listed |
Side effects
Section titled “Side effects”The operation records one Admin audit entry with action list_enforcement_decisions and the metadata keys list and result_count. A subject list has the subject as its target and list subject. The review queue has target type enforcement_decision, target ID 0 and list review_queue.
Rate limit
Section titled “Rate limit”200 requests per minute for each authenticated user, on the admin:lookup bucket.
Get enforcement decision
Section titled “Get enforcement decision”GET/v1/admin/enforcement-decisions/{decision_id}Returns one decision with its notes, its evidence and the notice the holder sees now. Requires enforcement:view.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| decision_id | snowflake | The ID of the decision |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | enforcement decision object | The decision was found |
| 404 | error response | UNKNOWN_ENFORCEMENT_DECISION, because no decision has this ID |
Side effects
Section titled “Side effects”The operation records one Admin audit entry with action view_enforcement_decision, target type enforcement_decision, and the metadata keys decision_id and kind.
Rate limit
Section titled “Rate limit”200 requests per minute for each authenticated user, on the admin:lookup bucket.
Add a note to an enforcement decision
Section titled “Add a note to an enforcement decision”POST/v1/admin/enforcement-decisions/{decision_id}/notesAppends an internal note. Requires enforcement:annotate. Notes are staff only and never change.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| decision_id | snowflake | The ID of the decision |
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| body | string | The note (1-2000 characters) |
Response body
Section titled “Response body”| Field | Type | Description |
|---|---|---|
| note_id | snowflake | The ID of the note |
| author_id | snowflake | The staff member who wrote it |
| body | string | The note |
| created_at | ISO8601 timestamp | When it was written |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | response body | The note was added |
| 404 | error response | UNKNOWN_ENFORCEMENT_DECISION, because no decision has this ID |
Side effects
Section titled “Side effects”The operation records one Admin audit entry with action annotate_enforcement_decision, target type enforcement_decision, and the metadata keys decision_id and note_id.
Rate limit
Section titled “Rate limit”100 requests per minute for each authenticated user, on the admin:user:modify bucket.
End an enforcement decision
Section titled “End an enforcement decision”POST/v1/admin/enforcement-decisions/{decision_id}/endEnds an active decision, or reverses a suspension that ended as expired, undoes what can be undone and emails the holder. Requires enforcement:view and the ACL of the route that made the decision, user:temp_ban for a suspension, user:delete for a closure, and user:update:flags for a warning or a feature limit, plus ban:ip:remove when the decision has network blocks. Returns the ended decision.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| decision_id | snowflake | The ID of the decision |
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| outcome1 | string | lifted or reversed |
| statement? | string | Note to the user, sent in the email (1-500 characters) |
1 lifted ends a restriction that was correct. reversed says the decision was wrong. Only suspensions, closures and feature limits can be lifted. Every other kind can only be reversed, because there is nothing to lift. A suspension that ended as expired can only be reversed
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | enforcement decision object | The decision ended |
| 400 | error response | INVALID_FORM_BODY with ENFORCEMENT_OUTCOME_NOT_ALLOWED, because a warning was given lifted |
| 404 | error response | UNKNOWN_ENFORCEMENT_DECISION, because no decision has this ID |
| 409 | error response | ENFORCEMENT_DECISION_ENDED, because the decision already ended, or ended as expired and was given lifted |
| 409 | error response | ENFORCEMENT_DECISION_REPLACED, because a later suspension or closure replaced this one |
| 409 | error response | ENFORCEMENT_DELETION_STARTED, because the deletion of a closed account has started |
Side effects
Section titled “Side effects”Ending a suspension clears temp_banned_until, enforcement_decision_id and the DISABLED flag, and lifts the new conversation limit, as Lift a suspension without a decision does for older suspensions. Ending a closure cancels the deletion, clears DELETED, deletion_reason_code and enforcement_decision_id, and unblocks the account’s email address when the closure blocked it. Ending either removes the decision’s network blocks. Ending a feature limit removes its flag unless another active decision holds the same limit, and takes the decision out of the review queue. Ending a warning changes nothing on the account.
The holder is emailed that the restriction ended or was reversed, with the statement when one is given. The ended notice of every kind except a suspension or closure is also sent as a system DM.
The operation records one Admin audit entry with action end_enforcement_decision, target type enforcement_decision, and the metadata keys decision_id, kind, outcome and subject_id.
Rate limit
Section titled “Rate limit”100 requests per minute for each authenticated user, on the admin:user:modify bucket.