---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Admin users
description: Reading and editing accounts, including contact fields, flags, bans, sessions, deletions, and archives.
---
import RouteHeader from '@/components/RouteHeader.astro';
These routes read and edit any account on the instance. An account has more fields here than the public [Users](/http-api/users/) resource returns, including its contact details, its network addresses, and its lifecycle state.
Each mutable field group has its own route, its own [ACL](/admin-api/#acl-registry), and its own [audit action](/admin-api/#audit-actions), so there is no combined update.
On an instance where members sign in with a username, [Change user email](#change-user-email), [Verify user email](#verify-user-email), [Resend verification email](#resend-verification-email) and [Send password reset](#send-password-reset) return 400 `EMAIL_UNAVAILABLE_ON_INSTANCE`. [Create password reset link](#create-password-reset-link) is the way to reset a password there, and it returns 400 `USERNAME_SIGN_IN_ONLY` on an email instance.
Every operation on this page records an [Admin audit entry](/admin-api/#admin-audit-entry-object), reads included. [Get current Admin user](#get-current-admin-user) is the exception and records nothing. `POST /v1/admin/users/{user_id}/avatar-block` addresses a user path but belongs to [Blocklists](/admin-api/blocklists/#block-a-users-current-avatar).
:::caution[A missing PII ACL nulls the protected fields]
A caller without `user:view:email` receives a null `email`. A caller without `user:view:dob` receives a null `date_of_birth`, and a caller without `user:view:ip` receives null `last_active_ip`, `last_active_ip_reverse`, and `last_active_location`. The read still succeeds, so a null field is no evidence that the account has none.
:::
## Admin user object
The complete administrative view of one account. It has every stored flag, the private lifecycle fields, and the contact and network fields that the public [user object](/http-api/users/#user-object) never exposes.
When the caller lacks `user:view:email`, `user:view:dob`, or `user:view:ip`, Fluxer redacts the fields that the missing ACL protects and still returns every key, so the object shape is identical for every caller.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| id | snowflake | The ID of the account |
| username | string | The username of the account, unique only together with its discriminator (1-32 characters) |
| discriminator1 | integer | Discriminator from 0 to 9999, unpadded |
| global_name | ?string | The display name of the account, or null when unset |
| bot | boolean | Whether the account is a bot |
| system | boolean | Whether the account is an official system account |
| flags2 | string | [Account flags](#account-flags) as an unsigned 64-bit decimal string |
| premium_flags | integer | [Premium flags](#premium-flags) |
| avatar | ?string | The avatar hash, or null when the account has none |
| banner | ?string | The profile banner hash, or null when the account has none |
| bio | ?string | The profile biography, or null when unset |
| pronouns | ?string | The profile pronouns, or null when unset |
| accent_color | ?integer | The profile accent colour packed into an integer, or null when unset |
| email3 | ?string | The registered email address, or null when the account has none |
| email_verified3 | boolean | Whether the registered address has been verified |
| email_bounced3 | boolean | Whether delivery to the registered address hard bounced |
| date_of_birth4 | ?string | The date of birth in `YYYY-MM-DD` form, or null when none is stored |
| locale | ?string | The saved [locale](/topics/locales/#supported-locales), or null when the account has never set one |
| premium_type | ?integer | [Premium type](#premium-types) |
| premium_since | ?ISO8601 timestamp | The time the premium subscription began, or null when the account has none |
| premium_until | ?ISO8601 timestamp | The time the premium subscription expires, or null when the account has none |
| premium_grace_ends_at | ?ISO8601 timestamp | The time the payment grace period ends, or null when no grace period is running |
| premium_lifetime_sequence | ?integer | The sequence number of the lifetime purchase, or null when the account holds none |
| temp_banned_until5 | ?ISO8601 timestamp | The time the ban expires, or null when no ban stands |
| pending_deletion_at | ?ISO8601 timestamp | The time the scheduled deletion runs, or null when none is scheduled |
| pending_bulk_message_deletion_at6 | ?ISO8601 timestamp | The time the account's own scheduled bulk message deletion runs |
| deletion_reason_code | ?integer | [Deletion reason](#deletion-reasons) |
| deletion_public_reason | ?string | The reason shown to the account holder, or null when none was supplied |
| deletion_audit_log_reason | ?string | The private reason stored with the pending deletion, and null without `audit_log:view` |
| deletion_scheduled_by | ?snowflake | The ID of the account that scheduled the pending deletion, or null when it was not recorded |
| deletion_scheduled_at | ?ISO8601 timestamp | The time the pending deletion was scheduled, or null when it was not recorded |
| acls7 | array[string] | Effective [Admin ACLs](/admin-api/#acl-registry), with at most 106 entries |
| traits8 | array[string] | The free-form operator labels set on the account, with at most 100 entries |
| has_totp9 | boolean | Whether a TOTP authenticator is registered |
| authenticator_types9 12 | array[integer] | Registered [authenticator types](/http-api/users/#authenticator-types), with at most 10 entries |
| last_active_at | ?ISO8601 timestamp | The time of the last recorded activity, or null when none is recorded |
| last_active_ip10 | ?string | The IP address the account was last active from |
| last_active_ip_reverse10 11 | ?string | The reverse DNS name of that IP address |
| last_active_location10 11 | ?string | The approximate location of that IP address |
1 An unpadded JSON number here, unlike the zero-padded string `discriminator` of the [Admin user summary](/admin-api/#admin-user-summary-object) and the [Admin resolved user](#admin-resolved-user-object) embedded in other Admin objects. On an instance with the `none` tag style every account that is not a bot has `0`, and its username is unique on its own
2 Serialised as a decimal string
3 Requires `user:view:email`. Without it `email` is null while `email_verified` and `email_bounced` are both false regardless of the stored state, so an unverified account and a redacted one are indistinguishable
4 Requires `user:view:dob`, and without it the field is null
5 Set by [Ban user](#ban-user) for both ban modes. An expiry that has already passed is reported as null, so an elapsed temporary ban reads the same as no ban
6 Written when the account holder schedules its own bulk message deletion, and cleared by [Cancel scheduled message deletion](#cancel-scheduled-message-deletion)
7 The set written by [Set user ACLs](#set-user-acls), returned in stored order with any value outside the [ACL registry](/admin-api/#acl-registry) left out. This set alone decides whether the account can reach the Admin API, and the `STAFF` [account flag](#account-flags) plays no part in that
8 Sorted in ascending order, unlike `acls`
9 Never redacted, and returned in full to any caller the operation admitted
10 Requires `user:view:ip`, and without it the field is null and no network lookup is attempted
11 Resolved live from `last_active_ip` for each response, and null when the lookup fails or returns nothing. The reverse DNS result is cached for one day
12 `WEBAUTHN` is present only while the account chose passkeys as a second factor. A registered credential does not add it, so `has_totp` false with an empty array still describes an account holding passkeys
### Example
```json
{
"id": "1493004551122763776",
"username": "subject",
"discriminator": 7,
"global_name": null,
"bot": false,
"system": false,
"flags": "8796093022208",
"premium_flags": 0,
"avatar": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
"email": "subject@example.com",
"email_verified": true,
"email_bounced": false,
"date_of_birth": "1998-03-21",
"temp_banned_until": null,
"pending_deletion_at": null,
"deletion_reason_code": null,
"acls": [],
"traits": ["early-signup"],
"has_totp": true,
"authenticator_types": [1, 2],
"last_active_at": "2026-08-30T21:04:02.771000+00:00",
"last_active_ip": "203.0.113.24",
"last_active_ip_reverse": "host24.example.com",
"last_active_location": "Stockholm, Sweden"
}
```
## Account flags
The `flags` field of the [Admin user object](#admin-user-object) is a 64-bit bitfield serialised as an unsigned decimal string. It has every stored flag, including the ones the [HTTP API](/http-api/users/#public-user-flags) leaves out of its public subset.
| Value | Name | Description |
| --- | --- | --- |
| 1 << 0 | STAFF | Account is a staff member |
| 1 << 2 | PARTNER | Account is a partner |
| 1 << 3 | BUG_HUNTER | Account is a bug hunter |
| 1 << 4 | FRIENDLY_BOT | Bot accepts friend requests from users |
| 1 << 5 | FRIENDLY_BOT_MANUAL_APPROVAL | Bot requires manual approval for friend requests |
| 1 << 6 | SPAMMER | Account is flagged as a spammer |
| 1 << 33 | HIGH_GLOBAL_RATE_LIMIT | Account has elevated global rate limits |
| 1 << 34 | DELETED | Account has been deleted |
| 1 << 36 | SELF_DELETED | Account was self-deleted |
| 1 << 38 | DISABLED | Account is disabled |
| 1 << 39 | HAS_SESSION_STARTED | Account has started a session |
| 1 << 47 | RATE_LIMIT_BYPASS | Account can bypass rate limits |
| 1 << 48 | REPORT_BANNED | Account is banned from reporting |
| 1 << 49 | VERIFIED_NOT_UNDERAGE | Account is verified as not underage |
| 1 << 50 | ACCOUNT_LIMITED | Account is [limited](/http-api/users/#account-limitation) |
| 1 << 51 | HAS_DISMISSED_PREMIUM_ONBOARDING | Account has dismissed premium onboarding |
| 1 << 53 | APP_STORE_REVIEWER | Account belongs to an app store reviewer |
| 1 << 57 | STAFF_HIDDEN | Staff status is hidden from public flags |
| 1 << 60 | AGE_VERIFIED_ADULT | Account has verified its age as an adult through card verification |
| 1 << 62 | LIMIT_EXEMPT | Account is never limited |
## Premium flags
| Value | Name | Description |
| --- | --- | --- |
| 1 << 0 | DISCRIMINATOR | Account has a premium discriminator |
| 1 << 1 | BADGE_HIDDEN | Premium badge is hidden |
| 1 << 2 | BADGE_MASKED | Premium badge is masked |
| 1 << 3 | BADGE_TIMESTAMP_HIDDEN | Premium badge timestamp is hidden |
| 1 << 4 | BADGE_SEQUENCE_HIDDEN | Premium badge sequence is hidden |
| 1 << 5 | PERKS_SANITIZED | Premium perks are sanitised |
| 1 << 6 | PURCHASE_DISABLED | Premium purchase is disabled |
| 1 << 7 | ENABLED_OVERRIDE | Premium status has an enabled override |
| 1 << 8 | PERKS_DISABLED | Premium perks are temporarily disabled |
## Premium types
| Value | Name | Description |
| --- | --- | --- |
| 0 | NONE | No premium subscription |
| 1 | SUBSCRIPTION | Active premium subscription |
| 2 | LIFETIME | Lifetime premium subscription |
## Deletion reasons
| Value | Name | Description |
| --- | --- | --- |
| 1 | USER_REQUESTED1 | Account holder requested deletion |
| 2 | OTHER | Other reason |
| 3 | SPAM | Spam |
| 4 | CHEATING_OR_EXPLOITATION | Cheating or exploitation |
| 5 | COORDINATED_RAIDING | Coordinated raiding |
| 6 | AUTOMATION_OR_SELFBOT | Automation or selfbot use |
| 7 | NONCONSENSUAL_SEXUAL_CONTENT | Non-consensual sexual content |
| 8 | SCAM_OR_SOCIAL_ENGINEERING | Scam or social engineering |
| 9 | CHILD_SEXUAL_CONTENT | Child sexual content |
| 10 | PRIVACY_VIOLATION_OR_DOXXING | Privacy violation or doxxing |
| 11 | HARASSMENT_OR_BULLYING | Harassment or bullying |
| 12 | PAYMENT_FRAUD | Payment fraud |
| 13 | CHILD_SAFETY_VIOLATION | Child safety violation |
| 14 | BILLING_DISPUTE_OR_ABUSE | Billing dispute or abuse |
| 15 | UNSOLICITED_EXPLICIT_CONTENT | Unsolicited explicit content |
| 16 | GRAPHIC_VIOLENCE | Graphic violence |
| 17 | BAN_EVASION | Ban evasion |
| 18 | TOKEN_OR_CREDENTIAL_SCAM | Token or credential scam |
| 19 | INACTIVITY | Inactivity |
| 20 | HATE_SPEECH_OR_EXTREMIST_CONTENT | Hate speech or extremist content |
| 21 | MALICIOUS_LINKS_OR_MALWARE | Malicious links or malware |
| 22 | IMPERSONATION_OR_FAKE_IDENTITY | Impersonation or fake identity |
1 The one value that lowers the minimum scheduling delay to 14 days and suppresses the identifier blocklisting and report auto-resolution described under [Schedule user deletion](#schedule-user-deletion)
A `reason_code` request value outside this registry fails body validation with 400 `INVALID_FORM_BODY`.
## Profile fields
The values [Clear user profile fields](#clear-user-profile-fields) accepts in `fields`.
| Value | Description |
| --- | --- |
| avatar | Profile avatar image |
| banner | Profile banner image |
| bio | Profile biography |
| pronouns | Profile pronouns |
| global_name | Display name |
## Relationship categories
| Value | Description |
| --- | --- |
| friend | A mutual friendship |
| incoming_request | A friend request the account has received |
| outgoing_request | A friend request the account has sent |
| blocked | An account this account has blocked |
Removing a friendship or friend request removes it for both accounts. Removing a block affects only the account that set it.
## Password reset link object
A password reset link an administrator hands to the account holder.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| url1 | string | The web app reset link, the app base URL followed by `/reset#token=` and the token |
| expires_at | ISO8601 timestamp | When the link stops working |
1 The token is in the URL fragment, which a browser never sends to a server. It is the same 64-character token [reset a password](/http-api/authentication/#reset-a-password) accepts, and it expires one hour after it is issued
## Admin user session object
One entry for each authentication session of an account. Terminated sessions remain listed with `deleted_at` set. No operation returns the session token.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| session_id_hash | string | The base64url-encoded hash of the session ID (8-256 characters) |
| created_at | ISO8601 timestamp | The time the session was created |
| approx_last_used_at | ISO8601 timestamp | The approximate time the session was last used |
| client_ip1 | string | The IP address the session was created from |
| client_ip_reverse1 2 | ?string | The reverse DNS name of that IP address |
| client_os3 | ?string | The client operating system derived from the stored user agent |
| client_platform3 | ?string | The client platform derived from the stored user agent |
| client_location1 2 | ?string | The approximate location of that IP address |
| deleted_at | ?ISO8601 timestamp | The time the session was terminated, or null while it is still active |
1 Requires `user:view:ip`. Without it `client_ip` is the literal string `[redacted]`, both derived fields are null, and no network lookup is attempted
2 Resolved live for each response, and null for that entry when its own lookup fails
3 Derived from the stored user agent at read time, so a session recorded without one reports null for both
### Example
```json
{
"session_id_hash": "Zm9vYmFyLXNlc3Npb24taGFzaC1leGFtcGxl",
"created_at": "2026-07-02T11:31:09.400000+00:00",
"approx_last_used_at": "2026-08-30T20:58:14.120000+00:00",
"client_ip": "203.0.113.24",
"client_ip_reverse": "host24.example.com",
"client_os": "Windows",
"client_platform": "Desktop",
"client_location": "Stockholm, Sweden",
"deleted_at": null
}
```
## Admin resolved user object
The account summary embedded in the Admin direct message channel object and the Admin relationship object. It is the [Admin user summary](/admin-api/#admin-user-summary-object) that audit entries embed, with `avatar` added.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| id | snowflake | The ID of the account |
| username | string | The username of the account |
| discriminator1 | string | The four-digit discriminator of the account |
| global_name | ?string | The display name of the account, or null when unset |
| avatar | ?string | The avatar hash, or null when the account has none |
1 Left-padded with zeroes to four digits, unlike the unpadded integer `discriminator` of the [Admin user object](#admin-user-object)
### Example
```json
{
"id": "1493004551122763776",
"username": "kelsier",
"discriminator": "0042",
"global_name": "Kelsier",
"avatar": "a1b2c3d4e5f60718293a4b5c6d7e8f90"
}
```
## Admin direct message channel object
One entry for each direct message or group direct message channel the account has taken part in. The field names differ from the public [channel object](/http-api/channels/#channel-object).
### Structure
| Field | Type | Description |
| --- | --- | --- |
| channel_id | snowflake | The ID of the channel |
| channel_type | ?integer | [Channel type](/http-api/channels/#channel-types), or null when the channel record is unavailable |
| channel_nsfw | ?boolean | Whether the channel is age-restricted, or null when the channel record is unavailable |
| guild_nsfw_level1 | ?integer | [NSFW level](/http-api/guilds/#nsfw-levels) of the owning guild |
| recipient_ids | array[snowflake] | The IDs of the channel recipients, with at most 100 entries |
| recipients2 | array[[Admin resolved user](#admin-resolved-user-object) object] | The resolved recipient accounts, with at most 100 entries |
| last_message_id | ?snowflake | The ID of the latest message, or null when the channel is empty |
| is_open | boolean | Whether the channel is open in the target account's client |
| name | ?string | The group name, or null when unnamed |
| icon | ?string | The group icon hash, or null when the group has none |
| owner_id | ?snowflake | The ID of the group owner, or null for a one-to-one direct message |
1 Always null
2 Contains only the recipients whose accounts still exist, so it can be shorter than `recipient_ids`
### Example
```json
{
"channel_id": "1496118820043030528",
"channel_type": 3,
"channel_nsfw": false,
"guild_nsfw_level": null,
"recipient_ids": ["1493004551122763776", "1489200013322551296"],
"recipients": [],
"last_message_id": "1500992244771848192",
"is_open": true,
"name": "Weekend plans",
"icon": null,
"owner_id": "1493004551122763776"
}
```
## Admin relationship object
### Structure
| Field | Type | Description |
| --- | --- | --- |
| target_user_id | snowflake | The ID of the other account |
| category | string | [Relationship category](#relationship-categories) |
| nickname | ?string | The nickname the account assigned to the other party |
| since | ?ISO8601 timestamp | The time the relationship was established, or null when unrecorded |
| target1 | ?[Admin resolved user](#admin-resolved-user-object) object | The resolved other account |
1 Null when the other account no longer exists, while `target_user_id` remains present
### Example
```json
{
"target_user_id": "1489200013322551296",
"category": "friend",
"nickname": null,
"since": "2026-04-18T14:22:51.006000+00:00",
"target": {
"id": "1489200013322551296",
"username": "operator",
"discriminator": "0042",
"global_name": "Operator"
}
}
```
## Admin user change log object
One recorded change to the account's email address or its username and discriminator. The account holder's own changes appear here, and so do [Change user username](#change-user-username) and [Change user email](#change-user-email).
### Structure
| Field | Type | Description |
| --- | --- | --- |
| event_id1 | string | The ID of the change entry as an unsigned 64-bit decimal string |
| field | string | The field that changed, either `email` or `fluxer_tag` |
| old_value2 | ?string | The value before the change, or null when the field was unset |
| new_value2 | ?string | The value after the change, or null when the field was cleared |
| reason3 | ?string | The recorded reason for the change, or null when unrecorded |
| actor_user_id | ?snowflake | The ID of the acting Admin, or null for a user-originated change |
| event_at | ISO8601 timestamp | The time the change was recorded |
1 Sent back as `page_token` to continue the walk. An entry stored without its own event ID reports the account ID here instead
2 An `email` entry read without `user:view:email` keeps the entry and replaces both value fields with the literal string `[redacted]`. An absent value is null
3 An Admin change records the literal value `admin_action`
### Example
```json
{
"event_id": "1500992244771848192",
"field": "email",
"old_value": "[redacted]",
"new_value": "[redacted]",
"reason": "admin_action",
"actor_user_id": "1489200013322551296",
"event_at": "2026-08-14T07:55:31.882000+00:00"
}
```
## Admin store purchase object
One [in-app purchase](/http-api/in-app-purchases/) with the store state behind it. It has more fields than the public [store purchase](/http-api/in-app-purchases/#store-purchase-object) object, and still never exposes a purchase token or the account token.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| id | snowflake | The ID of the store purchase |
| user_id | ?snowflake | The ID of the linked account, or null when unlinked |
| provider | string | Either `app_store` or `google_play` |
| kind | string | Either `subscription` or `gift` |
| slot | string | [Store slot](/http-api/in-app-purchases/#store-slots) |
| environment | string | Either `production` or `sandbox` |
| app_id | string | Bundle ID or package name of the purchasing app |
| product_id | string | Store product ID |
| base_plan_id | ?string | Google Play base plan ID, or null |
| latest_transaction_id1 | ?string | Latest store transaction or order ID |
| ownership_type2 | ?string | Store ownership type, or null |
| state | string | [Store purchase state](/http-api/in-app-purchases/#store-purchase-states) |
| store_state3 | ?string | Raw state the store reported, or null |
| entitled | boolean | Whether the purchase grants premium now |
| expires_at | ?ISO8601 timestamp | End of the paid period |
| grace_ends_at | ?ISO8601 timestamp | End of the store grace period |
| auto_renew | ?boolean | Whether the subscription renews |
| started_at | ?ISO8601 timestamp | The time the subscription or purchase started |
| purchased_at | ?ISO8601 timestamp | The time of the latest payment |
| revoked_at | ?ISO8601 timestamp | The time the store refunded or revoked it |
| revocation_reason | ?string | Reason recorded for the revocation, or null |
| superseded | boolean | Whether a newer Google Play purchase replaced it |
| acknowledged4 | boolean | Whether Fluxer acknowledged it with the store |
| gift_code | ?string | Gift code minted by a gift purchase, or null |
| bound_at | ?ISO8601 timestamp | The time the purchase was linked to its account |
| last_event_at | ?ISO8601 timestamp | Time of the latest store event applied |
| synced_at | ?ISO8601 timestamp | The time Fluxer last read it from the store |
| created_at | ISO8601 timestamp | The time Fluxer first saw the purchase |
| updated_at | ISO8601 timestamp | The time the record last changed |
1 The App Store transaction ID, or the Google Play order ID
2 The App Store value, such as `PURCHASED` or `FAMILY_SHARED`. Null for Google Play
3 The App Store subscription status number as a string, or the Google Play state name. Null for an App Store gift that was never revoked
4 Always true for the App Store. For a Google Play gift, true once the purchase was consumed
### Example
```json
{
"id": "1501203318237184000",
"user_id": "1489200013322551296",
"provider": "app_store",
"kind": "subscription",
"slot": "monthly",
"environment": "production",
"app_id": "com.fluxer",
"product_id": "com.fluxer.plutonium.monthly",
"base_plan_id": null,
"latest_transaction_id": "2000000712345678",
"ownership_type": "PURCHASED",
"state": "active",
"store_state": "1",
"entitled": true,
"expires_at": "2026-10-29T09:00:00.000Z",
"grace_ends_at": null,
"auto_renew": true,
"started_at": "2026-09-29T09:00:00.000Z",
"purchased_at": "2026-09-29T09:00:00.000Z",
"revoked_at": null,
"revocation_reason": null,
"superseded": false,
"acknowledged": true,
"gift_code": null,
"bound_at": "2026-09-29T09:00:02.000Z",
"last_event_at": "2026-09-29T09:00:00.000Z",
"synced_at": "2026-09-29T09:00:02.000Z",
"created_at": "2026-09-29T09:00:02.000Z",
"updated_at": "2026-09-29T09:00:02.000Z"
}
```
## Message shred entry object
### Structure
| Field | Type | Description |
| --- | --- | --- |
| channel_id | snowflake | The ID of the channel the message was sent in |
| message_id | snowflake | The ID of the message to shred |
### Example
```json
{
"channel_id": "1496118820043030528",
"message_id": "1500992244771848192"
}
```
## List users
Lists and searches accounts. Requires `user:lookup`, and `user:view:email` or `user:view:ip` for the selectors marked below. The response has an array of [Admin user](#admin-user-object) objects and a total.
Fluxer honours exactly one selector, in this precedence order: `user_id`, `resolve`, `email`, `last_active_ip`, then the indexed `q` search. A lower-precedence parameter sent alongside a higher-precedence one is ignored, and the request still succeeds. A request with no selector runs the indexed search with an empty query.
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id?1 | array[snowflake] | Exact account IDs, with at most 100 values |
| resolve?2 | string | One exact identifier to resolve (1-1024 characters) |
| email?3 | string | Exact registered email address (1-320 characters) |
| last_active_ip?4 | string | Exact last active IP address (1-64 characters) |
| q?5 | string | Free-text indexed query (1-1024 characters) |
| limit?6 | integer | Maximum accounts to return (1-200, default 50) |
| offset?6 | integer | Number of results to skip (0-100000, default 0) |
1 Repeat the parameter to pass more than one value. The response is unordered and has no entry for an ID that matches no account, so a client matches each returned account by its `id`
2 Resolves at most one account, and a value containing `@` requires `user:view:email`. An unmatched value returns an empty array
3 Matched exactly after trimming, returns at most one account, and requires `user:view:email`
4 Matched exactly after trimming against the account's stored last active address, so a subnet or a partial address matches nothing. Requires `user:view:ip`
5 Served by the user search index. An instance with no search backend configured returns 403 `FEATURE_TEMPORARILY_DISABLED`
6 Honoured by the `last_active_ip` and `q` selectors. The `user_id`, `resolve`, and `email` selectors ignore both
Fluxer chooses the `resolve` lookup from the shape of the value. A `username#discriminator` value resolves the tag, a value that is entirely digits resolves the account ID, and a value containing `@` resolves the email address. Every other value resolves a Stripe subscription ID.
A `q` value that is entirely digits also resolves that exact account ID and places it first, even when the search index did not match it, provided `offset` is zero. That direct hit raises `total` by one when the index did not already return it.
### Response body
| Field | Type | Description |
| --- | --- | --- |
| users | array[[Admin user](#admin-user-object) object] | The accounts in this page |
| total1 | integer | The number of accounts the selector matched |
1 The `user_id` and `resolve` selectors report the length of `users`, so `total` never exceeds the returned page for those two
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 2001 | response body | Accounts were returned |
| 403 | [error response](/admin-api/#error-response) | `MISSING_ACL`, because the honoured selector requires `user:view:email` or `user:view:ip` |
| 403 | [error response](/admin-api/#error-response) | `FEATURE_TEMPORARILY_DISABLED`, because the indexed search ran on an instance with no search backend |
1 A selector that matches nothing returns an empty `users` array and a `total` of zero
### Side effects
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `search_users`, target type `user`, and target ID `0`. The metadata keys are `selector`, `result_count` for the accounts in `users`, and `total`. `selector` is the honoured selector, one of `user_id`, `resolve`, `email`, `last_active_ip`, or `search` for the indexed search.
The `user_id` selector adds `user_id_count` for the number of IDs supplied, and `user_id` when exactly one ID was supplied. The `last_active_ip` and `search` selectors add `limit` and `offset`. The indexed search adds `has_query` as `true` when `q` is not blank. The `resolve`, `email`, `last_active_ip`, and `q` values are not recorded. A failed request records nothing.
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.
## Get current Admin user
Returns the account the request authenticated as, wrapped in a `user` member. Requires `admin:authenticate`.
Every Admin credential holds `admin:authenticate`, so every account that can reach the Admin API can read this route. `email`, `date_of_birth`, and the IP fields are returned unredacted whatever the acting credential holds. `acls` reports the ACL set stored on the account. For an Admin API key credential that set can be wider than what the key itself can use.
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The account the request authenticated as |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | Current Admin was returned |
| 403 | [error response](/admin-api/#error-response) | Credential type is refused, or the account does not hold `admin:authenticate` |
### Side effects
The operation records no [Admin audit entry](/admin-api/#admin-audit-entry-object).
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.
## Get user
Returns one account as a single-element array. Requires `user:lookup`.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| users1 | array[[Admin user](#admin-user-object) object] | The requested account, or an empty array |
1 The response is the same envelope [List users](#list-users) returns for its `user_id` selector, without the `total` member
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 2001 | response body | The lookup completed |
1 This operation does not answer 404. An ID with no matching account returns 200 with an empty `users` array, so a client checks the array length
An account with a pending or completed deletion is still returned, with its lifecycle state visible through `pending_deletion_at`, `deletion_reason_code`, and `deletion_public_reason`.
### Side effects
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `get_user`, target type `user`, the path `user_id` as target ID, and a metadata key `found`. `found` is `true` when `users` has the account and `false` when it is empty.
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.
## Change user username
Replaces the username, allocates or claims a discriminator, and returns the resulting account. Requires `user:update:username`.
A target account may hold a custom discriminator on a self-hosted instance in the `everyone` [premium mode](/admin-api/instance/#premium-modes), and on any other instance only when the `feature_custom_discriminator` limit resolves to a value above zero for that account.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| username1 | string | Replacement username (1-32 characters) |
| discriminator?2 | string | Requested discriminator of one to four decimal digits |
1 Trimmed before validation, restricted to letters, digits, and underscores, and rejected when it lowercases to `everyone` or `here` or contains `fluxer` or `system message`
2 Honoured only when the target account may hold a custom discriminator. In every other case, including an omitted field, Fluxer allocates a free discriminator for the submitted username. On an instance with the `none` tag style an account that is not a bot always gets `0`, any other value returns the field code `DISCRIMINATOR_NOT_SUPPORTED_ON_INSTANCE`, and a username another account holds in any letter case returns `USERNAME_ALREADY_TAKEN`
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | Username was replaced |
| 400 | [error response](/admin-api/#error-response) | Path or body validation fails, or the requested tag is taken and the request returns `TAG_ALREADY_TAKEN` |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
`TAG_ALREADY_TAKEN` also covers a submitted username with no free discriminator left, and an allocation lock the operation cannot get within its 10 second wait.
### Side effects
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions, and each guild the account is a member of receives [Guild Member Update](/gateway/events/#guild-member-update). Fluxer reindexes the member in each indexed guild in the background, so a member search result can briefly report the previous tag.
The change is recorded in the contact change log and is returned by [List user change log](#list-user-change-log) with `reason` set to `admin_action` and `actor_user_id` set to the acting Admin.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `change_username`, target type `user`, and metadata keys `old_username`, `new_username`, and `discriminator`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Change user email
Replaces the account's email address and returns the resulting account. Requires `user:update:email`.
The replacement address is stored unverified. Use [Verify user email](#verify-user-email) afterwards to mark it verified, or [Resend verification email](#resend-verification-email) to let the account holder verify it.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| email1 | string | Replacement email address (1-254 characters) |
1 Normalised and validated as an email address, but the operation itself does no uniqueness check
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | Email address was replaced |
| 400 | [error response](/admin-api/#error-response) | `EMAIL_UNAVAILABLE_ON_INSTANCE`, because members sign in with a username |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
### Side effects
`email_verified` is set to false. The stored bounce marker survives the change, so an address replaced on a bounced account remains marked bounced until a verification clears it.
No notice is sent to the previous or the replacement address. [User Update](/gateway/events/#user-update) is emitted to the account's own sessions, and no [Guild Member Update](/gateway/events/#guild-member-update) follows.
The change is recorded in the contact change log and is returned by [List user change log](#list-user-change-log), where both values are replaced with `[redacted]` for a caller without `user:view:email`.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `change_email`, target type `user`, and metadata keys `old_email` and `new_email`. The audit log is not redacted on read, so both addresses are visible to any holder of `audit_log:view`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Verify user email
Marks the account's current email address as verified without a confirmation link, and returns the resulting account. Requires `user:update:email`.
The operation accepts no request body and never clears verification, so the one way to mark an address unverified is to replace it with [Change user email](#change-user-email).
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 2001 | response body | Email address was marked verified |
| 400 | [error response](/admin-api/#error-response) | `EMAIL_UNAVAILABLE_ON_INSTANCE`, because members sign in with a username |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
1 An account that has no stored email address is still accepted, and `email_verified` is set to true while `email` stays null
### Side effects
`email_verified` becomes true and `email_bounced` becomes false.
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `verify_email`, target type `user`, and a metadata key `email` with the address as it stood before the write, or the literal `null` when the account had none.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Resend verification email
Requests a new verification email for the account. Requires `user:update:email`. Returns an empty 204 response.
:::caution[An already verified account gets no email]
When the account's email is already verified, Fluxer returns 204 without storing a token or sending anything.
:::
:::note[Delivery is dropped silently]
Nothing is sent when the instance email transport is disabled or when the address is marked hard bounced. Neither case changes the status code, and the audit entry is recorded either way.
:::
Each address can receive at most three verification emails in fifteen minutes, independently of the Admin rate limit. Further requests return 429 without issuing a token.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | empty | The request was accepted, whether or not an email was sent |
| 4001 | [error response](/admin-api/#error-response) | The path parameter is not a snowflake, or the account has no email address |
| 403 | [error response](/admin-api/#error-response) | Credential type or ACL evaluation denies the request, or `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED` when the target is a bot |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
1 A missing address returns `INVALID_FORM_BODY` with the validation code `USER_DOES_NOT_HAVE_AN_EMAIL_ADDRESS`
### Side effects
A single-use verification token is stored against the account ID and its current address, and the link is emailed to that address. The account's verification state is unchanged until the link is used.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `resend_verification_email`, target type `user`, and a metadata key `email`. No Gateway Dispatch is emitted.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Send password reset
Creates a password reset token and emails it to the account holder. Requires `user:update:email`. Returns an empty 204 response.
:::caution[A dropped reset email still stores the token]
Delivery is silently dropped when the instance email transport is disabled and when the address is marked hard bounced. The token is stored either way, so a dropped delivery leaves a usable reset token that nobody received.
:::
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | empty | The request was accepted, whether or not an email was sent |
| 4001 | [error response](/admin-api/#error-response) | The path parameter is not a snowflake, or the account has no email address |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
1 A missing address returns `INVALID_FORM_BODY` with the validation code `USER_DOES_NOT_HAVE_AN_EMAIL_ADDRESS`
Unlike [Resend verification email](#resend-verification-email), this operation has no limit of three emails per address in fifteen minutes, and it accepts a bot account.
### Side effects
A single-use password reset token is stored against the account ID and its current email address, and the token is emailed to that address. The account's password, sessions, and other credentials are unchanged until the account holder completes the reset.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `send_password_reset`, target type `user`, and a metadata key `email`. No Gateway Dispatch is emitted.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Create password reset link
Creates a password reset token for the account and returns the link that redeems it, with nothing emailed. Requires `user:create:password_reset_link` and every ACL the target account holds. A target holding an ACL the caller lacks returns 403 `MISSING_ACL`, and a bot target returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`. Returns a [password reset link](#password-reset-link-object) object.
Only an instance where members sign in with a username serves this operation, and an email instance returns 400 `USERNAME_SIGN_IN_ONLY`. The account holder opens the link in the web app, which calls [validate a password reset token](/http-api/authentication/#validate-a-password-reset-token) and then [reset a password](/http-api/authentication/#reset-a-password).
:::caution[The link is a credential]
Anyone holding the link can set the account's password until it expires. Hand it to the account holder over a channel you trust. The response is the only place the link appears.
:::
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [password reset link](#password-reset-link-object) object | The token was stored and its link returned |
| 400 | [error response](/admin-api/#error-response) | The path parameter is not a snowflake, or the instance signs members in with email |
| 403 | [error response](/admin-api/#error-response) | ACL evaluation denies the request, the target holds an ACL the caller lacks, or the target is a bot |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
### Side effects
Every earlier password reset token for the account is deleted, so a link created before this one stops working. The account's recovery kit is deleted too, so its key stops working. A single-use password reset token is then stored against the account ID with no email address. The account's password, sessions, and other credentials are unchanged until the account holder completes the reset.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `create_password_reset_link` and target type `user`. No Gateway Dispatch is emitted.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Revoke user recovery kit
Deletes the account's recovery kit, so its recovery key stops working. Requires `user:delete:recovery_kit` and every ACL the target account holds, and a target holding an ACL the caller lacks returns 403 `MISSING_ACL`. Returns no body.
Only an instance where members sign in with a username serves this operation, and an email instance returns 400 `USERNAME_SIGN_IN_ONLY`. The account holder can issue a new kit from their settings.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | none | The kit was deleted, or the account had none |
| 400 | [error response](/admin-api/#error-response) | The path parameter is not a snowflake, or the instance signs members in with email |
| 403 | [error response](/admin-api/#error-response) | ACL evaluation denies the request, or the target holds an ACL the caller lacks |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
### Side effects
The account's password, sessions, and other credentials are unchanged. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `revoke_recovery_kit` and target type `user`. No Gateway Dispatch is emitted.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Change user date of birth
Replaces the stored date of birth and returns the resulting account. Requires `user:update:dob`.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| date_of_birth1 | string | Replacement date in `YYYY-MM-DD` form |
1 Exactly ten characters matching `YYYY-MM-DD`. The field is required, so there is no way to clear a stored date of birth through this operation
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | Date of birth was replaced |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
### Side effects
The new date changes what age-restricted content the account may reach. [User Update](/gateway/events/#user-update) is emitted to the account's own sessions.
The change is not written to the contact change log. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `change_dob`, target type `user`, and metadata keys `old_dob` and `new_dob`, where an absent previous value is the literal `null`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Clear user profile fields
Clears the named profile fields and returns the resulting account. Requires `user:update:profile`.
Clearing is the only profile change on this resource. No route sets a biography, display name, avatar, or banner to a new value.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| fields1 | array[string] | [Profile fields](#profile-fields) to clear, with at most 10 entries |
1 Required. An empty array is accepted, clears nothing, and still records an audit entry. A value outside the registry fails body validation
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The named fields were cleared |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
### Side effects
Clearing `avatar` or `banner` schedules the previous asset for deletion.
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions, and each guild the account is a member of receives [Guild Member Update](/gateway/events/#guild-member-update) when the operation changes `avatar` or `global_name`.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `clear_fields`, target type `user`, and a metadata key `fields` with the submitted names joined by commas.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Set user bot status
Marks the account as a bot or as an ordinary account, and returns the resulting account. Requires `user:update:bot_status`.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| bot1 | boolean | Whether the account is a bot |
1 Required. Setting it to false also clears `system` in the same write
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | Bot status was set |
| 4031 | [error response](/admin-api/#error-response) | Credential type or ACL evaluation denies the request. `ACCESS_DENIED` when the target holds an Admin ACL and `bot` is true |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
1 A staff account cannot be converted into a bot. Clear its ACL set with [Set user ACLs](#set-user-acls) first
### Side effects
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions, and each guild the account is a member of receives [Guild Member Update](/gateway/events/#guild-member-update) when the write changes `bot` or `system`.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `set_bot_status`, target type `user`, and a metadata key `bot`. Clearing `system` as a side effect records no second entry.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Set user system status
Marks the account as an official system account or removes that marker, and returns the resulting account. Requires `user:update:bot_status`, the same ACL as [Set user bot status](#set-user-bot-status).
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| system1 | boolean | Whether the account is a system account |
1 Required. Setting it to true on an account that is not already a bot fails validation
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | System status was set |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
1 The non-bot case is `INVALID_FORM_BODY` with the validation code `USER_MUST_BE_A_BOT_TO_BE_MARKED_AS_A_SYSTEM_USER` on the `system` path
### Side effects
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions, and each guild the account is a member of receives [Guild Member Update](/gateway/events/#guild-member-update) when the write changes `system`.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `set_system_status`, target type `user`, and a metadata key `system`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Set user ACLs
Replaces the Admin ACL set held by the account and returns the resulting account. Requires `acl:set:user`.
The submitted array replaces the stored set outright. An empty array removes every ACL, which also removes the account's Admin API access and leaves every Admin API key the account created unable to satisfy any operation.
:::caution[A grant cannot exceed the acting Admin]
A submitted set naming an ACL the acting credential does not hold fails with 403 `MISSING_ACL` on the first offending value. A wildcard holder is exempt. An Admin API key is compared against the set stored on the key.
:::
:::caution[An Admin cannot write its own ACL set]
A path naming the acting account fails with 403 `ACCESS_DENIED` before the account is resolved.
:::
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| acls1 | array[string] | Replacement [Admin ACLs](/admin-api/#acl-registry), with at most 106 values |
1 Required. A value outside the [ACL registry](/admin-api/#acl-registry) fails body validation, and a repeated value is collapsed
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The ACL set was replaced |
| 400 | [error response](/admin-api/#error-response) | Path or body validation fails, including an `acls` value outside the [ACL registry](/admin-api/#acl-registry) |
| 403 | [error response](/admin-api/#error-response) | Credential type or ACL evaluation denies the request, or `ACCESS_DENIED` when the path names the acting account |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
The grant bound is evaluated after the account is resolved, so an unknown ID fails first with 404.
### Side effects
The stored ACL set is replaced and appears in the next [Admin user object](#admin-user-object). The account can reach the Admin API exactly when the new set holds `admin:authenticate` or `*`. Narrowing the set narrows every existing session and Admin API key of the account without rotating any credential.
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `set_acls`, target type `user`, and a metadata key `acls` with the submitted values joined by commas.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Set user traits
Replaces the account's trait set and returns the resulting account. Requires `user:update:traits`.
Traits are free-form operator labels that have no ACL meaning and grant no capability.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| traits1 | array[string] | Replacement trait set, with at most 100 values of 1 to 128 characters |
1 Required. An empty array clears the stored set entirely, and a repeated value is collapsed
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The trait set was replaced |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
### Side effects
The stored trait set is replaced and is returned sorted by the next [Admin user object](#admin-user-object). [User Update](/gateway/events/#user-update) is emitted to the account's own sessions.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `set_traits` and target type `user`. A non-empty request has a metadata key `traits` with the submitted values joined by commas, and a request that clears the set has no metadata at all.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Update user flags
Adds and removes [account flags](#account-flags) and returns the resulting account. Requires `user:update:flags`.
Additions are applied before removals, so a flag named in both arrays ends up cleared. A bit outside the [account flag](#account-flags) registry is applied unchanged.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| add_flags?1 | array[string] | [Account flag](#account-flags) values to add, with at most 64 entries (default empty) |
| remove_flags?1 | array[string] | [Account flag](#account-flags) values to remove, with at most 64 entries (default empty) |
1 One 64-bit flag value as an unsigned decimal string, such as `64`. A value with several bits is applied as a mask
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | Flags were updated |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
[Bulk jobs](/admin-api/bulk-jobs/) applies the same change to up to 1,000 accounts as a queued `update_user_flags` task.
### Side effects
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions. A change to the account's public flags also sends [Guild Member Update](/gateway/events/#guild-member-update) to the account's guilds.
Removing `ACCOUNT_LIMITED` or adding `LIMIT_EXEMPT` also lifts any [new conversation limit](/http-api/users/private-channels/#new-conversation-limit) on the account.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `update_flags`, target type `user`, and the metadata keys `add_flags`, `remove_flags`, and `new_flags`. An empty array is omitted from the metadata map.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Update user premium flags
Adds and removes [premium flags](#premium-flags) and returns the resulting account. Requires `user:update:flags`, the same ACL as [Update user flags](#update-user-flags).
Premium flags control badge display, the premium override, the purchase block, and perk sanitisation. They do not grant or revoke a subscription.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| add_flags?1 | array[integer] | [Premium flag](#premium-flags) values to add, with at most 64 entries (default empty) |
| remove_flags?1 | array[integer] | [Premium flag](#premium-flags) values to remove, with at most 64 entries (default empty) |
1 Each entry is one 32-bit flag value, such as `2`. Additions are applied before removals
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | Premium flags were updated |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
### Side effects
Premium badge display reflects the new flags. [User Update](/gateway/events/#user-update) is emitted to the account's own sessions.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `update_premium_flags`, target type `user`, and the metadata keys `add_flags`, `remove_flags`, and `new_flags`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## List user store purchases
Lists the [in-app purchases](/http-api/in-app-purchases/) linked to an account, newest first. Requires `user:lookup`. A self-hosted instance never serves this route.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| purchases | array[[Admin store purchase](#admin-store-purchase-object) object] | The linked purchases |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The purchases were listed |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
### Side effects
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `list_user_store_purchases`, target type `user`, and the metadata key `result_count`.
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.
## Refresh user store purchases
Reads every purchase linked to an account again from its store, applies the result to the account, and returns the updated purchases. Requires `user:update:flags`, the same ACL as [Update user premium flags](#update-user-premium-flags). A self-hosted instance never serves this route.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| purchases | array[[Admin store purchase](#admin-store-purchase-object) object] | The linked purchases after the refresh |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The purchases were refreshed |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
| 503 | [error response](/admin-api/#error-response) | `STORE_BILLING_UNAVAILABLE`, because a store could not be reached |
### Side effects
A subscription the store no longer recognises becomes `expired` and stops granting premium. A changed premium state sends [User Update](/gateway/events/#user-update) to the account's own sessions.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `refresh_store_purchases`, target type `user`, and the metadata key `purchase_count`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Ban user
Disables an account temporarily or permanently and returns the resulting account. Requires `user:temp_ban`.
:::note[A permanent ban is a distant expiry]
Both modes add the `DISABLED` [account flag](#account-flags) and set `temp_banned_until`, a permanent ban to 100 years out. The field cannot tell the modes apart, and [Unban user](#unban-user) clears either one.
:::
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| duration_hours1 | integer | Whole-hour ban length, where `0` bans permanently (0-8760) |
| reason?2 | string | Statement of reasons sent to the banned account holder (at most 512 characters) |
1 Required. The expiry is computed from the moment the ban is applied, so no absolute-expiry field is accepted. The 8760 ceiling is one year
2 Included in the temporary ban email and in the audit entry, but not stored on the account, so no read operation returns it
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The account was banned |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
:::caution[The ban signs the account out everywhere]
Every authentication session of the account is deleted. [Unban user](#unban-user) restores account access, but it does not restore any deleted session.
:::
### Side effects
`DISABLED` is added to the account flags and `temp_banned_until` is set to the resolved expiry. Every authentication session is then deleted, so the account is signed out on every device.
An authentication attempt while the ban stands fails with 403 `ACCOUNT_SUSPENDED_TEMPORARILY`. An attempt after the expiry has passed clears the `DISABLED` flag and `temp_banned_until` in the same request, so a temporary ban ends without an Admin operation.
When the account has an email address and `duration_hours` is greater than zero, the account holder is emailed the duration, the expiry, and the supplied `reason`. A permanent ban sends no email.
[User Update](/gateway/events/#user-update) is emitted after the sessions are deleted, so no connection of the account remains to receive it. `DISABLED` is not a public account flag, and no other account observes the change.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `temp_ban`, target type `user`, and the metadata keys `duration_hours`, `reason`, and `banned_until`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Unban user
Clears the account ban and returns the resulting account. Requires `user:temp_ban`.
:::caution[The audit reason reaches the account holder]
The notification email quotes the `X-Audit-Log-Reason` value verbatim as the reason the ban was lifted, and falls back to the literal text `administrative action` when the header is absent or resolves to null.
:::
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 2001 | response body | The ban was cleared |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
1 The operation checks no precondition. On an account that was never banned it writes `temp_banned_until` as null, clears `DISABLED`, and still emails the account holder
### Side effects
`temp_banned_until` is cleared and `DISABLED` is removed from the account flags.
Deleted sessions are not restored. The account holder is emailed when the account has an email address. [User Update](/gateway/events/#user-update) is emitted to the account's own sessions.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `unban`, target type `user`, and no metadata.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Add a note to a user ban
Appends a note to the ban that currently stands on the account. Requires `user:temp_ban`. The ban entry and earlier notes are never changed.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| ban_audit_log_id1 | snowflake | The ID of the `temp_ban` audit entry of the current ban |
| note | string | The note (1-512 characters) |
1 The entry must target this account, and its `banned_until` metadata must equal the current `temp_banned_until`
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | none | The note was recorded |
| 400 | [error response](/admin-api/#error-response) | `INVALID_FORM_BODY`, because `ban_audit_log_id` does not name a ban of this account |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
| 409 | [error response](/admin-api/#error-response) | `CONFLICT`, because the named ban is no longer the current ban |
### Side effects
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `annotate_ban`, target type `user`, the note as `audit_log_reason`, and a metadata key `ban_audit_log_id`. The `X-Audit-Log-Reason` header is not read.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Schedule user deletion
Creates an account deletion schedule and returns the resulting account. Requires `user:delete`. A pending schedule is replaced only when the request names it in `replace_pending_deletion_at`, and a schedule cannot be changed once erasure starts.
Fluxer raises the submitted delay to the minimum for the [deletion reason](#deletion-reasons), so a request for one day under any reason other than `USER_REQUESTED` is stored as 60 days.
The `X-Audit-Log-Reason` value is also stored on the account as the private deletion reason and reused as the blocklist reason for the identifier bans below.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| reason_code1 | integer | [Deletion reason](#deletion-reasons) |
| public_reason?2 | string | Statement of reasons shown to the account holder (at most 512 characters) |
| days_until_deletion?3 | integer | Requested whole-day delay (1-365, default 60) |
| replace_pending_deletion_at?4 | ISO8601 timestamp | The `pending_deletion_at` of the schedule this request replaces |
1 Required, and validated against the [deletion reason](#deletion-reasons) registry. `USER_REQUESTED` selects the 14-day minimum, and every other value selects the 60-day minimum
2 Stored on the account and included in the deletion email. Omitting it stores null
3 The stored deadline is the request instant plus the greater of this value and the minimum for the reason code. There is no absolute-timestamp field
4 Required when the account already has a pending deletion, including one the caller scheduled. It must equal the current `pending_deletion_at`
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The deletion schedule was stored |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
| 409 | [error response](/admin-api/#error-response) | `CONFLICT`, because a pending deletion was not named, erasure has started, or the deletion state changed |
`user:delete` also allows scheduling deletion of the acting Admin or an account with broader permissions.
### Side effects
The account can no longer authenticate, and its existing authentication sessions and OAuth2 access and refresh tokens are deleted. The acting Admin and the time are stored as `deletion_scheduled_by` and `deletion_scheduled_at`. Erasure is scheduled for the resulting deadline.
Fluxer cancels a Stripe subscription on the account without proration and refunds the charge behind its latest invoice as fraudulent. When the cancellation or refund fails, Fluxer logs the failure and still keeps the deletion schedule.
The account holder is emailed the deadline and the supplied `public_reason` when the account has an email address.
For every reason other than `USER_REQUESTED`, Fluxer also blocks the account's email address and resolves the pending reports against the account. These enforcement steps are best-effort and can fail without cancelling the deletion schedule.
[User Update](/gateway/events/#user-update) is emitted after the sessions have already been deleted. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `schedule_deletion`, target type `user`, and the metadata keys `days`, `reason_code`, and `pending_deletion_at`. A replacement adds `replaced_pending_deletion_at`, `replaced_scheduled_by`, `replaced_scheduled_at`, and `replaced_reason_code` for the schedule it replaced. The identifier bans record their own blocklist entries, and when Fluxer resolves at least one report, it records a second entry with action `auto_resolve_reports_on_deletion` and a metadata key `resolved_count`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Cancel user deletion
Cancels the scheduled deletion named in the request and returns the resulting account. Requires `user:delete`. Erasure cannot be cancelled once it starts.
:::caution[Cancellation does not undo enforcement]
Clearing the deadline permits the account to authenticate again, but it does not restore deleted sessions, reinstate a cancelled subscription, lift the email and IP blocklist entries the schedule wrote, or reopen the reports it resolved.
:::
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| expected_pending_deletion_at1 | ISO8601 timestamp | The `pending_deletion_at` of the deletion being cancelled |
| notify_user? | boolean | Whether to email the account holder (default false) |
1 Required. It must equal the current `pending_deletion_at`, so a cancel written against one schedule never clears a different one
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The deletion was cancelled |
| 400 | [error response](/admin-api/#error-response) | `NO_PENDING_DELETION`, because no deletion is scheduled |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
| 409 | [error response](/admin-api/#error-response) | `CONFLICT`, because a different deletion is pending, erasure has started, or the deletion state changed |
### Side effects
The deletion schedule, its reasons, and the recorded scheduler are cleared, allowing the account to authenticate again unless another restriction applies.
When `notify_user` is true and the account has an email address, Fluxer emails the account holder. The email never includes the `X-Audit-Log-Reason` value.
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `cancel_deletion`, target type `user`, the metadata keys `cancelled_pending_deletion_at`, `cancelled_scheduled_by`, `cancelled_scheduled_at`, and `cancelled_reason_code` for the cancelled schedule, and `notify_user`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## List user guilds
Lists the guilds the account is a member of, in ascending guild ID order. Requires `user:list:guilds`. Both cursors apply to the account's own sorted membership list.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| before?1 | snowflake | Return guilds ordered before this guild ID |
| after?1 | snowflake | Return guilds ordered after this guild ID |
| limit? | integer | Maximum guilds to return (1-200, default 200) |
| with_counts?2 | boolean | Whether to resolve live member and presence counts (default false) |
1 A cursor naming a guild the account is not a member of is ignored, and the request still succeeds. `after` is applied before `before`, and the two combine to bound a window
2 Only the exact strings `true`, `True`, and `1` count as true
### Response body
| Field | Type | Description |
| --- | --- | --- |
| guilds1 | array[[Admin guild](/admin-api/guilds/#admin-guild-object) object] | The guilds in this page that the account is a member of |
1 `approximate_member_count` and `approximate_presence_count` are present only when `with_counts` resolved to true. `owner_username`, `owner_global_name`, and `owner_discriminator` are resolved by this operation and are null only for an owner account that no longer exists
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The membership page was returned |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
The response has no paging state. Derive the next cursor from the last returned guild ID, and treat a short page as the end of the list.
### Side effects
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `list_user_guilds`, target type `user`, the path `user_id` as target ID, and the metadata keys `limit`, `with_counts`, and `guild_count` for the guilds returned. A supplied cursor adds `before_guild_id` or `after_guild_id`. A failed request records nothing.
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.
## List user direct message channels
Lists the direct message channels the account has taken part in. Requires `user:list:dm_channels`. The read does not open, reopen, or acknowledge a channel for the target account.
:::note[The channel classes are separate reads]
`type` selects which channel class is returned. The default returns one-to-one direct message channels, and group direct messages need a second request with `type=group_dm`.
:::
The group branch ignores `before`, `after`, and `limit`, and returns every group the account is a recipient of in one response.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| type? | string | Channel class, either `dm` or `group_dm` (default `dm`) |
| before?1 | snowflake | Return channels with IDs lower than this channel ID |
| after?1 | snowflake | Return channels with IDs higher than this channel ID |
| limit? | integer | Maximum channels to return (1-200, default 50) |
1 Mutually exclusive. Supplying both fails validation even when `type` is `group_dm` and neither would be used
### Response body
| Field | Type | Description |
| --- | --- | --- |
| channels1 | array[[Admin direct message channel](#admin-direct-message-channel-object) object] | The channels in this page |
1 Bounded at 200 entries for `dm` and at 500 entries for `group_dm`
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The channel page was returned |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
The response has no paging state. Derive the next cursor from the last returned channel ID.
### Side effects
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `list_user_dm_channels`, target type `user`, the path `user_id` as target ID, and the metadata keys `type` and `channel_count` for the channels returned. A `dm` request also records `limit`, and `before_channel_id` or `after_channel_id` when a cursor is supplied. A 404 records nothing.
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.
## List user applications
Lists the OAuth2 applications the account owns. Requires `application:lookup` or `application:list:by_owner`, the same pair [List applications](/admin-api/applications/#list-applications) accepts.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the owning account |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| applications1 | array[[Admin application](/admin-api/applications/#admin-application-object) object] | The applications the account owns |
1 Returned in full in one response. The operation accepts no query string parameters and defines no cursor
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The applications were returned |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
### Side effects
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `list_user_applications`, target type `user`, the path `user_id` as target ID, and a metadata key `application_count`. A 404 records nothing.
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.
## List user relationships
Lists the friends, friend requests, and blocked accounts of the account, split into one array for each [relationship category](#relationship-categories). Requires `user:list:relationships`.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| friends1 | array[[Admin relationship](#admin-relationship-object) object] | The mutual friendships of the account |
| incoming_requests1 | array[[Admin relationship](#admin-relationship-object) object] | The friend requests the account has received |
| outgoing_requests1 | array[[Admin relationship](#admin-relationship-object) object] | The friend requests the account has sent |
| blocked1 | array[[Admin relationship](#admin-relationship-object) object] | The accounts this account has blocked |
1 A stored [relationship type](/http-api/users/relationships/#relationship-types) outside the categories is dropped from the response
The operation accepts no cursor, category filter, or limit, and returns every category in full.
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The relationships were returned |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
### Side effects
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `list_user_relationships`, target type `user`, the path `user_id` as target ID, and the metadata keys `friend_count`, `incoming_request_count`, `outgoing_request_count`, and `blocked_count`. A 404 records nothing.
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.
## Clear user relationships
Removes every relationship of the account in one [relationship category](#relationship-categories) and returns the removed count. Requires `user:remove:relationship`.
`category` is required, so there is no request shape that clears every category at once.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| category1 | string | [Relationship category](#relationship-categories) to clear |
1 Required. There is no confirmation parameter and no dry-run mode
### Response body
| Field | Type | Description |
| --- | --- | --- |
| removed_count1 | integer | The number of relationships that were removed |
1 Counted from the target account's perspective, so a friendship, which both accounts store, contributes one
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 2001 | response body | Matching relationships were removed |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
1 A category with no matching relationship succeeds with a `removed_count` of zero, and still records the audit entry
Removals run one at a time, so a failure partway through leaves the earlier removals applied and answers 500.
### Side effects
Friendships and friend requests are removed for both accounts. Blocks are removed only for the target account.
When a friendship or friend request is removed, both accounts receive [Relationship Remove](/gateway/events/#relationship-remove) naming the other account. A `blocked` removal dispatches only to the target account, and ordinary delivery from the unblocked account resumes.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `remove_relationships_by_category`, target type `user`, and the metadata keys `category` and `removed_count`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Remove user relationship
Removes one relationship row in one category. Requires `user:remove:relationship`. Returns an empty 204 response.
The operation addresses one category, so an account that is both a former friend and currently blocked needs two requests.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the account that owns the relationship |
| target_user_id | snowflake | The ID of the other account |
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| category | string | [Relationship category](#relationship-categories) to remove |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | empty | The relationship was removed |
| 4041 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
1 The same code covers an owning account that does not exist and a relationship that does not exist in the named category, so a 404 does not distinguish the two
### Side effects
A friendship or friend request is removed for both accounts. A block is removed only for the owning account.
When a friendship or friend request is removed, both accounts receive [Relationship Remove](/gateway/events/#relationship-remove) naming the other account. A `blocked` removal dispatches only to the owning account.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `remove_relationship`, target type `user`, and the metadata keys `target_user_id` and `category`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## List user sessions
Lists the account's active and terminated authentication sessions. Requires `user:list:sessions`. IP, reverse DNS and location fields also require `user:view:ip`.
:::caution[Listing sessions records an audit entry]
The entry stores the `X-Audit-Log-Reason` value whether or not the caller holds `user:view:ip`, so a listing with redacted addresses is recorded too.
:::
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| sessions1 | array[[Admin user session](#admin-user-session-object) object] | The active and terminated sessions of the account |
1 Active sessions are listed before terminated ones, and each group is ordered by `created_at` descending. The operation accepts no cursor or limit and returns the complete list in one response
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The sessions were returned |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
| 5031 | [error response](/admin-api/#error-response) | `SERVICE_UNAVAILABLE` when at least one session exists and every location lookup fails |
1 Reachable only with `user:view:ip`, because no lookup is attempted without it. The audit entry is written after the lookups, so a request that fails this way records nothing
Without `user:view:ip` the derived fields are null and `client_ip` is the literal string `[redacted]`, so a redacted address is distinguishable from a failed lookup. With it, a partial reverse DNS or location failure yields null for that field alone.
### Side effects
This read does not mutate session state and emits no Gateway Dispatch. It makes outbound reverse DNS and geolocation lookups for each session address when the caller holds `user:view:ip`.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `list_user_sessions`, target type `user`, and a metadata key `session_count` counting active and terminated sessions together.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket. The other lists on this resource use the `admin:lookup` read bucket.
## Terminate user sessions
Terminates every active authentication session of the account. Requires `user:update:flags`, the same ACL as [Update user flags](#update-user-flags).
There is no operation that revokes one session. The account's Admin API keys, bot token, and OAuth2 tokens are separate credentials and are not affected.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 2001 | [terminate sessions response](#terminate-sessions-response) | Active sessions were terminated |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
1 An account with no live session is accepted, returns `terminated_count` 0, and still records the audit entry
#### Terminate sessions response
| Field | Type | Description |
| --- | --- | --- |
| terminated_count | integer | The number of sessions that were terminated |
:::caution[A terminated session cannot be restored]
[List user sessions](#list-user-sessions) still lists the terminated session with `deleted_at` set. The session no longer authenticates any request, and no operation reactivates it.
:::
### Side effects
Every affected client is disconnected and must authenticate again. Terminated sessions remain listed with `deleted_at` set.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `terminate_sessions`, target type `user`, and no metadata.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## List user WebAuthn credentials
Lists the passkeys and security keys registered on the account, as a bare JSON array. Requires `user:update:mfa`, the same ACL as [Disable user MFA](#disable-user-mfa).
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 2001 | array[[WebAuthn credential](/http-api/users/mfa/#webauthn-credential-object) object] | The credentials were returned |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
1 An account with no credential receives an empty array
The complete credential set is returned in one response, bounded by the ten credentials an account may register. [Replaced passkeys](/http-api/users/mfa/#replaced-passkeys) are left out, and `credential_count` counts only the returned ones. No public key, attestation object, or signature counter is exposed.
### Side effects
This read does not mutate authenticator state and emits no Gateway Dispatch. It records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `list_webauthn_credentials`, target type `user`, and a metadata key `credential_count`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Delete user WebAuthn credential
Deletes one passkey or security key from the account. Requires `user:update:mfa`. Returns an empty 204 response.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
| credential_id | string | WebAuthn credential ID (1-512 characters) |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | empty | The credential was deleted |
| 4041 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
1 A credential that does not exist on the account also answers `UNKNOWN_USER`, so a 404 with that code does not distinguish an unknown account from an unknown credential. A [replaced passkey](/http-api/users/mfa/#replaced-passkeys) answers `UNKNOWN_WEBAUTHN_CREDENTIAL` and records no audit entry
:::caution[A deleted passkey cannot be restored]
The passkey or security key stops authenticating immediately and cannot be restored from its deleted record. The account holder must register it again to use it later.
:::
### Side effects
The credential record is deleted, together with the [replaced passkeys](/http-api/users/mfa/#replaced-passkeys) it took over from, and every replaced passkey goes once no listed credential remains. When it was the account's final WebAuthn credential and the account had enabled passkeys as a second factor, Fluxer removes the `WEBAUTHN` [authenticator type](/http-api/users/#authenticator-types) from the account, emits [User Update](/gateway/events/#user-update) to the account's own sessions, and copies the account's authenticator types onto the bot user of every application the account owns. An account that never enabled passkeys as a second factor holds no `WEBAUTHN` type to remove.
[WebAuthn Credentials Update](/gateway/events/#webauthn-credentials-update) is emitted to the target account with its remaining credentials.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `delete_webauthn_credential`, target type `user`, and a metadata key `credential_id`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Disable user MFA
Clears the account's TOTP authenticator, its registered authenticator type set, and its backup codes. Requires `user:update:mfa`. Returns an empty 204 response.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | empty | MFA state was cleared |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
:::caution[Stored WebAuthn credentials survive this operation]
Clearing the authenticator type set turns passkeys off as a second factor, but the credential records remain and no [WebAuthn Credentials Update](/gateway/events/#webauthn-credentials-update) is emitted. They still answer a sudo mode challenge and a passwordless login. Remove those with [Delete user WebAuthn credential](#delete-user-webauthn-credential).
:::
### Side effects
The account's TOTP secret, authenticator type set, and every multi-factor backup code are deleted. Fluxer clears the authenticator types of the bot user of every application the account owns. Sessions and credentials are not revoked.
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `disable_mfa`, target type `user`, and no metadata.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Delete all user messages
Counts every message attributed to the account, and queues their deletion when the request is not a dry run. Requires `message:delete_all`.
The request counts messages before responding, so accounts with more messages take longer. A misspelled `dry_run` parameter leaves the request a dry run.
The operation does not resolve the target account, so it never answers `UNKNOWN_USER`. An ID with no matching account is accepted and matches no message.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the message author |
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| dry_run?1 | string | Whether the operation only counts matching messages (default `true`) |
1 Whitespace is trimmed and only the exact values `false`, `False`, and `0` count as false. Every other string, including `no` and `FALSE`, counts as true
### Response body
| Field | Type | Description |
| --- | --- | --- |
| success | boolean | Whether the request succeeded, always true |
| dry_run | boolean | Whether the request only counted messages |
| channel_count1 | integer | The number of channels the account has messages in |
| message_count | integer | The number of messages the walk matched |
| job_id?2 | string | The ID of the queued shred job |
1 Summed for each page of 200, so a channel spanning pages is counted once per page. Treat it as an upper bound
2 Present only when `dry_run` resolved to false and `message_count` is above zero
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The count completed, and the deletion job was queued when applicable |
:::danger[A non-dry run permanently deletes messages]
The response only confirms that the job was queued. When `dry_run` resolves to false, the background job deletes every matching message, and no operation restores them.
:::
The queued job is an ordinary shred job, so its progress, totals, skips, and failures are read through [Get message shred job](/admin-api/messages/#get-message-shred-job).
### Side effects
A dry run reads and counts only. A deletion run also queues one shred job with every matched message identity, and the request returns before any message is processed.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with target type `message_deletion`, the target account ID as its target ID, and the metadata keys `user_id`, `channel_count`, `message_count`, and `dry_run`. The action is `delete_all_user_messages_dry_run` for a dry run and `delete_all_user_messages` otherwise. A deletion run also records the `queue_message_shred` entry described under [Shred user messages](#shred-user-messages).
### Rate limit
100 requests per minute for each authenticated user, on the `admin:message:operation` bucket.
## Shred user messages
Queues irreversible deletion of an explicit list of message identities and returns a job ID read through [Get message shred job](/admin-api/messages/#get-message-shred-job). Requires `message:shred`.
The operation does not resolve the target account, so it never answers `UNKNOWN_USER`.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the message author |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| entries1 | array[[message shred entry](#message-shred-entry-object) object] | Explicit message identities, with 1 to 1000 values |
1 Required and non-empty. The entries are not checked against the target account at submission time
### Response body
| Field | Type | Description |
| --- | --- | --- |
| success | boolean | Whether the request succeeded, always true |
| job_id | string | The ID of the queued shred job |
| requested1 | integer | The number of submitted entries |
1 An upper bound on the messages the job shreds
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The shred job was queued |
:::danger[Shredding deletes messages permanently]
A shred deletes every matching message with its reactions, purges the attachments it owned, and removes it from search. Only a preceding [Create user archive](#create-user-archive) retains the original text.
:::
A failed job is not retried automatically.
### Side effects
The request returns before any message is processed. The job checks each referenced message again before it deletes it, and skips a missing message or one no longer authored by the target account.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `queue_message_shred`, target type `message_shred`, the target account ID as its target ID, and the metadata keys `user_id`, `job_id`, and `requested_entries`.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:message:operation` bucket.
## Cancel scheduled message deletion
Cancels the bulk message deletion the account holder scheduled for itself and returns the resulting account. Requires `user:cancel:bulk_message_deletion`.
[Delete all user messages](#delete-all-user-messages) queues a separate Admin job over the same messages, and cancelling this schedule leaves that job running.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| user | [Admin user](#admin-user-object) object | The resulting account |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 2001 | response body | The scheduled deletion was cancelled, or the account had none |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
1 The operation is idempotent. Calling it on an account with no scheduled deletion succeeds, returns the account unchanged, and still records the audit entry
### Side effects
`pending_bulk_message_deletion_at` is cleared together with the stored channel and message counts, and the account is removed from the bulk deletion queue, so no further message is deleted. Messages already deleted by an earlier run are not restored.
No [User Update](/gateway/events/#user-update) is emitted, unlike every other mutating user operation on this page, so a connected client learns of the cancellation only on its next read. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `cancel_bulk_message_deletion`, target type `user`, and no metadata.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:user:modify` bucket.
## Create user archive
Creates an asynchronous archive of the account's data and returns the created [archive](/admin-api/archives/#archive-object) object. Requires `archive:trigger:user`.
Archive progress, download, and expiry are documented under [Archives](/admin-api/archives/).
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the archive subject |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| include_attachments?1 | boolean | Whether uploaded files are included (default false) |
1 Including attachment binaries makes the archive larger and slower to build, and only an archive that includes them retains the original uploaded media
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 2001 | [archive](/admin-api/archives/#archive-object) object | The archive was queued |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, and no archive record or build job is created |
1 The archive starts with `progress_percent` at zero and `progress_step` set to `Queued`
### Side effects
Fluxer creates the archive record and queues a build job, so the response returns before any data is collected. Generating the archive reads the account's data without changing it.
No Gateway Dispatch is emitted.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `trigger_user_archive`, target type `user`, the path `user_id` as target ID, and the metadata keys `archive_id` for the created archive and `include_attachments` as `true` or `false`.
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.
## List user change log
Lists the identity and contact field changes recorded for the account, newest first. Requires `user:view:contact_log`. Email values also require `user:view:email`.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| user_id | snowflake | The ID of the target account |
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| limit? | integer | Maximum entries to return (1-200, default 50) |
| page_token?1 | string | Cursor returned as `next_page_token` by the previous page (1-64 characters) |
1 Continues the newest-first walk, so it returns entries recorded before the cursor
### Response body
| Field | Type | Description |
| --- | --- | --- |
| entries1 | array[[Admin user change log](#admin-user-change-log-object) object] | The identity and contact changes in this page |
| next_page_token2 | ?string | The cursor to send as `page_token`, or null when this is the last page |
1 Bounded at 200 entries, which is also the maximum `limit`
2 Set only when the page returned exactly `limit` entries, so a final page that happens to be full reports a cursor whose next page is empty
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 2001 | response body | The change page was returned |
1 This operation does not resolve the account, so it never answers `UNKNOWN_USER`. An ID with no matching account returns an empty page
Without `user:view:email`, an `email` entry is still returned with both value fields replaced by the literal string `[redacted]`, so the fact and the time of the change stay visible while the addresses do not.
### Side effects
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `list_user_change_log`, target type `user`, the path `user_id` as target ID, and the metadata keys `limit` and `entry_count` for the entries returned. A supplied `page_token` adds `has_page_token` as `true`, and the cursor value is not recorded.
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.