Skip to content
Fluxer API

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.

TermMeaning
kindWhat the decision did: warning, suspension, closure, feature_limit, content_removal, profile_reset, or community_removal
reason codeOne of the enforcement deletion reasons. USER_REQUESTED, OTHER and INACTIVITY are refused
ruleThe policy section the reason code points to. The notice links it
handlingstandard, protected or restricted, fixed by the reason code. It decides how much detail the notice gives
evidenceA snapshot of the subject’s own messages, or for a community the messages posted in it, taken when the decision is made
notesInternal staff notes. The X-Audit-Log-Reason header of the request that made the decision is the first note
noticeThe statement of reasons the holder receives by email, system DM, or at sign-in
HandlingReason codesNotice detail
standardevery code not listed belowThe rule, where and when it happened, and the note to the user
protected7, 10, 11, 15, 23The same, except that messages in direct and group conversations show the month only
restricted9, 13, 24The 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.

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 whose ends_at has passed, before Fluxer ends it
  • replaced: an active suspension or closure that the account’s enforcement_decision_id no 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.

FieldTypeDescription
typestringenforcement, holder_request or housekeeping
decision?1enforcement decision input objectWhy 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 broken
  • holder_request: the account holder or community owner asked
  • housekeeping: nobody’s conduct is involved, such as inactivity or cleanup

Each route accepts these bases:

RouteBases
Schedule user deletionenforcement, holder_request, housekeeping
Clear user profile fieldsenforcement, holder_request, housekeeping
Shred user messagesenforcement, holder_request, housekeeping
Delete all user messagesenforcement, holder_request, housekeeping
Delete messageholder_request, housekeeping
Delete guildenforcement, holder_request

The audit entry of each route records the basis in a basis metadata key.

Each feature limit decision limits one feature, so ending or expiring one never touches another.

  • direct_messages sets SPAMMER and is allowed for reason codes 3, 6, 8, 18 and 21. It has no end.
  • reports sets REPORT_BANNED and is allowed for reason code 29. It ends after 90 days by default and after at most 365.
  • purchases sets PURCHASE_DISABLED and 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.

  • 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 evidence is 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.

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.

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.

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.

The decision object every restricting route takes.

FieldTypeDescription
reason_codeintegerAn enforcement deletion reason
sourcestringreport when staff reviewed this after a report, own_review otherwise
automationstringflagged when automated tools flagged it before staff decided, none otherwise
report_ids?1array[snowflake]Reports about the subject that the decision resolves (at most 20)
evidence?2array[object]channel_id and message_id of messages the decision relies on (at most 50)
statement?3stringNote to the user, shown in the notice (1-500 characters)
rights_holder?4stringThe rights holder or the right concerned (1-200 characters)
confirmations?5array[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.

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: the X-Audit-Log-Reason header is missing
  • ENFORCEMENT_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 messages
  • ENFORCEMENT_LIMIT_NOT_ALLOWED: the reason code does not allow this feature limit
  • ENFORCEMENT_LIMIT_END_INVALID: a reports limit ends in the past or more than 365 days from now, or another limit is given an end
  • ENFORCEMENT_REPORT_REQUIRED: source is report and no report is linked
  • ENFORCEMENT_REPORT_MISMATCH: a linked report does not exist or is about someone else
  • ENFORCEMENT_EVIDENCE_NOT_ALLOWED: a shred is given evidence messages, because its entries are the evidence
  • ENFORCEMENT_EVIDENCE_NOT_FOUND: an evidence message does not exist
  • ENFORCEMENT_EVIDENCE_NOT_SUBJECT: an evidence message is not the subject’s own, or not posted in the community
  • ENFORCEMENT_EVIDENCE_DM_UNREPORTED: an evidence message is in a conversation no linked report covers
  • ENFORCEMENT_FACTS_REQUIRED: there is no evidence message and no statement
  • ENFORCEMENT_EVIDENCE_REQUIRED: a restricted reason has no evidence, or remove_evidence has nothing to remove
  • ENFORCEMENT_STATEMENT_NOT_ALLOWED: a restricted reason has a statement
  • ENFORCEMENT_PERMANENT_REQUIRED: a restricted reason is given a temporary suspension
  • ENFORCEMENT_CONFIRMATIONS_REQUIRED: a restricted reason is missing a confirmation
  • ENFORCEMENT_RIGHTS_HOLDER_REQUIRED: code 31 has no rights_holder
  • ENFORCEMENT_RIGHTS_HOLDER_NOT_ALLOWED: another code has a rights_holder
  • ENFORCEMENT_NETWORK_BLOCK_NOT_ALLOWED: block_networks is given with a temporary suspension or a closure that is not for a broken rule
  • ENFORCEMENT_NETWORK_UNKNOWN: the account did not use an address in block_networks in the last 30 days
  • ENFORCEMENT_NETWORK_SHARED: another account that is not suspended or closed was last active on an address in block_networks
  1. Fluxer validates the input and snapshots the evidence.
  2. It stores the decision with notice_status pending, together with its evidence and first note.
  3. It applies the restriction. A suspension writes enforcement_decision_id in the same account write as temp_banned_until, and a closure in the same write as pending_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.
  4. It resolves the linked reports.
  5. 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.

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.

FieldTypeDescription
decision_idsnowflakeThe ID of the decision, which the holder quotes in an appeal
subject_typestringuser or guild
subject_idsnowflakeThe ID of the account, bot or community
subject_name?stringThe bot username or community name when the decision was made, null for a person’s own account
recipient_user_id?snowflakeWho receives the notice, or null when nobody can
kindstringWhat the decision did
reason_codeintegerDeletion reason
sourcestringreport or own_review
automationstringnone, flagged or automated
statement?stringNote to the user
rights_holder?stringThe rights holder or the right concerned
ends_at?ISO8601 timestampWhen a suspension or feature limit ends, null when permanent
deletion_at?ISO8601 timestampWhen a closed account is deleted
limit?stringdirect_messages, reports or purchases for a feature limit
profile_fieldsarray[string]The fields a profile reset cleared
removed_message_count?integerMessages the decision removed
removed_all_messagesbooleanWhether every message of the account was removed
payment_at?ISO8601 timestampThe charge a payment decision is about
report_ids1array[snowflake]Linked reports
decided_bysnowflakeThe staff member who decided, or 0 for an automated decision
created_atISO8601 timestampWhen the decision was made
statusstringactive or ended
end_outcome?stringlifted, reversed or expired
ended_at?ISO8601 timestampWhen the decision ended
ended_by?snowflakeWho ended it
end_statement?stringNote to the user sent with the ended email
notice_statusstringpending, sent or undelivered
notice_channelsarray[string]email, system_dm and sign_in
notice_locale?stringThe locale the notice was written in
notice_text?stringThe plain text that was sent
notice_sent_at?ISO8601 timestampWhen the holder received the notice. The 60-day appeal window counts from here
expiredbooleanSee Lifecycle
replacedbooleanSee Lifecycle

1 Empty without report:view

Get enforcement decision adds these fields.

FieldTypeDescription
notesarray[object]Internal notes in order, each with note_id, author_id, body and created_at
evidence1array[object]Snapshot messages, each with message_id, channel_id, timestamp, content, attachments and attachment_count
profile_values1?objectProfile values before a reset
network_blocks2array[object]The decision’s network blocks that have not expired, each with ip and expires_at
noticeobjectThe 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

GET/v1/admin/enforcement-decisions

Lists the decisions about one account or community, newest first, without notes or evidence, or the review queue. Requires enforcement:view.

FieldTypeDescription
review_queue?1booleanList the automated decisions waiting for a person instead of one subject
subject_type?stringuser or guild
subject_id?snowflakeThe ID of the account or community
limit?integerMaximum number of decisions (1-100, default 50)
before?snowflakeReturn decisions older than this decision

1 Without review_queue=true, subject_type and subject_id are required

FieldTypeDescription
decisionsarray[enforcement decision object]The decisions, without the fields only the single read returns
StatusBodyCondition
200response bodyThe decisions were listed

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.

200 requests per minute for each authenticated user, on the admin:lookup bucket.

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.

FieldTypeDescription
decision_idsnowflakeThe ID of the decision
StatusBodyCondition
200enforcement decision objectThe decision was found
404error responseUNKNOWN_ENFORCEMENT_DECISION, because no decision has this ID

The operation records one Admin audit entry with action view_enforcement_decision, target type enforcement_decision, and the metadata keys decision_id and kind.

200 requests per minute for each authenticated user, on the admin:lookup bucket.

POST/v1/admin/enforcement-decisions/{decision_id}/notesAudit reason

Appends an internal note. Requires enforcement:annotate. Notes are staff only and never change.

FieldTypeDescription
decision_idsnowflakeThe ID of the decision
FieldTypeDescription
bodystringThe note (1-2000 characters)
FieldTypeDescription
note_idsnowflakeThe ID of the note
author_idsnowflakeThe staff member who wrote it
bodystringThe note
created_atISO8601 timestampWhen it was written
StatusBodyCondition
200response bodyThe note was added
404error responseUNKNOWN_ENFORCEMENT_DECISION, because no decision has this ID

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.

100 requests per minute for each authenticated user, on the admin:user:modify bucket.

POST/v1/admin/enforcement-decisions/{decision_id}/endAudit reason

Ends 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.

FieldTypeDescription
decision_idsnowflakeThe ID of the decision
FieldTypeDescription
outcome1stringlifted or reversed
statement?stringNote 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

StatusBodyCondition
200enforcement decision objectThe decision ended
400error responseINVALID_FORM_BODY with ENFORCEMENT_OUTCOME_NOT_ALLOWED, because a warning was given lifted
404error responseUNKNOWN_ENFORCEMENT_DECISION, because no decision has this ID
409error responseENFORCEMENT_DECISION_ENDED, because the decision already ended, or ended as expired and was given lifted
409error responseENFORCEMENT_DECISION_REPLACED, because a later suspension or closure replaced this one
409error responseENFORCEMENT_DELETION_STARTED, because the deletion of a closed account has started

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.

100 requests per minute for each authenticated user, on the admin:user:modify bucket.