fix(tooling): repair test, i18n and docs verification gates (#3331)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-09 01:38:34 +02:00
1 parent ece622e800
commit f390e610b9
107 files changed
+5144 -3004

No files matched your search

@@ -262,7 +262,7 @@ Each field is a string, except `hashes` and `substrings`, which are `array[strin
| Field | Type | Description |
| --- | --- | --- |
| scope<sup>3</sup> | string | The [profile substring scope](#profile-substring-scopes) to store the rows under |
| scope?<sup>3</sup> | string | The [profile substring scope](#profile-substring-scopes) to store the rows under |
| category?<sup>4</sup> | string | The [content blocklist category](#content-blocklist-categories) (1-64 characters, default `manual`) |
| severity?<sup>4</sup> | integer | The [content blocklist severity](#content-blocklist-severities) (0-3, default 2) |
| match_subdomains?<sup>5</sup> | boolean | Whether the row also covers subdomains (default true) |
@@ -481,13 +481,17 @@ The body is selected by `list_type`.
| Field | Type | Description |
| --- | --- | --- |
| hashes<sup>1</sup> | array[string] | Avatar hashes to remove (1-1000 items of 8 through 10 characters matching `^(a_)?[0-9a-fA-F]{8}$`) |
| scope<sup>2</sup> | string | The [profile substring scope](#profile-substring-scopes) the substrings are stored under |
| substrings<sup>2</sup> | array[string] | Substrings to remove (1-1000 items of 1 through 500 characters) |
| hashes?<sup>1</sup> | array[string] | Avatar hashes to remove (1-1000 items of 8 through 10 characters matching `^(a_)?[0-9a-fA-F]{8}$`) |
| scope?<sup>2</sup> | string | The [profile substring scope](#profile-substring-scopes) the substrings are stored under |
| substrings?<sup>2</sup> | array[string] | Substrings to remove (1-1000 items of 1 through 500 characters) |
| reason?<sup>3</sup> | string | Accepted and ignored |
| notes?<sup>3</sup> | string | Accepted and ignored |
<sup>1</sup> `avatar-hash` only, where it is the whole body
<sup>1</sup> `avatar-hash` only, where it is required and is the whole body
<sup>2</sup> `profile-substring` only, where both are required. `reason` and `notes` are accepted but ignored
<sup>2</sup> `profile-substring` only, where both are required
<sup>3</sup> `profile-substring` only. Fluxer records neither in the audit entry
### Response
@@ -594,7 +598,7 @@ The body is the creation shape of the selected blocklist with the value field re
| Field | Type | Description |
| --- | --- | --- |
| scope<sup>1</sup> | string | The [profile substring scope](#profile-substring-scopes) to write the row under |
| scope?<sup>1</sup> | string | The [profile substring scope](#profile-substring-scopes) to write the row under |
| category?<sup>2</sup> | string | The [content blocklist category](#content-blocklist-categories) (1-64 characters, default `manual`) |
| severity?<sup>2</sup> | integer | The [content blocklist severity](#content-blocklist-severities) (0-3, default 2) |
| match_subdomains?<sup>3</sup> | boolean | Whether the row also covers subdomains (default true) |
@@ -112,6 +112,7 @@ The `task` discriminator selects one of these structures. Every ID array has an
| reason_code | integer | [Deletion reason](/admin-api/users/#deletion-reasons) recorded against every targeted account |
| public_reason? | string | The reason shown to each account holder in the deletion notice (0-512 characters after normalisation) |
| days_until_deletion?<sup>1</sup> | integer | The delay before deletion in whole days (1-365, default 60) |
| notify_user? | boolean | Whether to email each account holder about the scheduled deletion (default true) |
<sup>1</sup> Fluxer raises the value to the reason-specific minimum, 14 days for `USER_REQUESTED` and 60 days for every other code
@@ -186,7 +186,7 @@ Approves or rejects a pending application and returns the updated [discovery app
| Field | Type | Description |
| --- | --- | --- |
| status<sup>1</sup> | approved \| rejected | Decision to apply |
| reason<sup>2</sup> <sup>3</sup> | string | Reason stored on the application as `review_reason` (at most 500 characters) |
| reason?<sup>2</sup> <sup>3</sup> | string | Reason stored on the application as `review_reason` (at most 500 characters) |
<sup>1</sup> A value outside the two listed fails body validation. Neither `removed` nor `pending` is accepted
@@ -87,13 +87,49 @@ One entry for each node the request polled, including nodes that did not answer
| process_count | integer | The Erlang processes in use on this node |
| process_limit | integer | The Erlang process ceiling on this node |
| uptime_seconds | integer | The seconds since this node started |
| cluster_metrics?<sup>3</sup> | [gateway cluster metrics](#gateway-cluster-metrics-object) object | The clustering counters of this node |
<sup>1</sup> A node reports its `HOSTNAME` environment value when that value is a non-blank string, and its Erlang node name otherwise
<sup>2</sup> `healthy` for a node that answered and `unavailable` for one that did not. A node that did not answer reports null for every counter here and contributes zero to every cluster total
<sup>3</sup> Absent for a node that did not answer
A node entry can have further diagnostic members. A client treats a member it does not recognise as absent.
## Gateway cluster metrics object
How one node sees the Gateway cluster. Every total counts from the moment the node started, so a restart resets it to zero.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| gateway_cluster_member_count | integer | The cluster members this node currently counts as alive |
| gateway_cluster_discovery_resolve_failures_total | integer | The times this node failed to resolve its cluster peers |
| gateway_cluster_membership_transitions_total | [membership transition counts](#membership-transition-counts-object) object | The members this node saw join and leave |
| gateway_node_router_owner_resolutions_total | [owner resolution counts](#owner-resolution-counts-object) object | Where this node routed the work it had to place |
The object can have further diagnostic members. A client treats a member it does not recognise as absent.
### Membership transition counts object
#### Structure
| Field | Type | Description |
| --- | --- | --- |
| up | integer | Members that joined the alive set |
| down | integer | Members that left the alive set |
### Owner resolution counts object
#### Structure
| Field | Type | Description |
| --- | --- | --- |
| self | integer | Owner lookups that resolved to this node |
| peer | integer | Owner lookups that resolved to another node |
## Node memory object
Byte counts for one node, or for every polled node summed together. Each count is a decimal string because a JSON number cannot preserve a 64-bit byte count.
@@ -161,6 +161,8 @@ The guild state [Update guild](#update-guild) reads back after applying the requ
| member_count | integer | The recorded member count |
| nsfw_level | integer | The [NSFW level](/http-api/guilds/#nsfw-levels) |
The response schema also declares `nsfw`, `content_warning_level`, and `content_warning_text`, which this object never has. Read them back with [Get guild](#get-guild) instead.
## Admin guild expression object
A custom emoji or a sticker of a guild, with a resolvable media URL. The listings return the same shape.
@@ -312,6 +312,7 @@ An entry with any other action has `access` set to `write`.
| Value | Description |
| --- | --- |
| NCMEC Report | An attachment was submitted through the NCMEC reporting integration |
| annotate_ban | A note was appended to the current ban of an account |
| approve_discovery_application | A discovery application was approved |
| approve_registration | A pending registration was approved |
| auto_resolve_reports_on_deletion | The pending reports against an account were processed as part of its deletion |
@@ -411,7 +411,7 @@ Stripe billing for the premium tier. Each stored field is nullable, and null fal
| effective_tax_id_collection | boolean | Whether checkout collects a buyer tax ID |
| effective_terms_consent_required | boolean | Whether checkout requires terms of service consent |
| default_currency | ?string | Currency used when no country mapping applies, or null |
| prices<sup>1</sup> | ?object | Stored [billing price sets](#billing-price-set-object) keyed by currency, or null to use the deployment configuration |
| prices<sup>1</sup> | ?map[string, [billing price set](#billing-price-set-object) object] | Stored price sets keyed by currency, or null to use the deployment configuration |
| country_currencies | ?object | Stored map of two-letter country codes to currencies, or null |
| legacy_prices<sup>2</sup> | ?object | Stored map of legacy price slots to arrays of Stripe price IDs, or null to use the deployment configuration |
| billing_active<sup>3</sup> | boolean | Whether premium purchases are available |
@@ -195,6 +195,8 @@ The detail object extends the [Admin report object](#admin-report-object) with t
| message_context<sup>2</sup> | array[[report message context](#report-message-context-object) object] | The messages captured around the reported content |
| message_responses<sup>3</sup> | array[[message](/http-api/messages/#message-object) object] | The current state of the same messages, read live |
| reported_profile_snapshot?<sup>4</sup> | ?[report profile snapshot](#report-profile-snapshot-object) object | The reported profile as it was when the report was filed |
| legal_hold_until?<sup>5</sup> | ?ISO8601 timestamp | When the legal hold on the report ends |
| legal_hold_reason?<sup>5</sup> | ?string | Why the report is held |
<sup>1</sup> Set only for a USER report that has both a reporter and a reported account, and null when the two share no direct message channel
@@ -204,6 +206,8 @@ The detail object extends the [Admin report object](#admin-report-object) with t
<sup>4</sup> Always emitted. Null for a report filed before profile snapshots were stored, for a report about a message a webhook sent, and when the stored snapshot cannot be read
<sup>5</sup> Always emitted. Null when the report has no hold. [Set report legal hold](#set-report-legal-hold) places and clears the hold, and [Report retention](#report-retention) describes its effect
:::note[The live array can be shorter than the capture]
Fluxer skips an entry when neither the capture nor the report records its channel. It also omits a message that no longer exists, or whose channel can no longer be read.
:::
@@ -1410,11 +1410,14 @@ Both modes add the `DISABLED` [account flag](#account-flags) and set `temp_banne
| --- | --- | --- |
| duration_hours<sup>1</sup> | integer | Whole-hour ban length, where `0` bans permanently (0-8760) |
| reason?<sup>2</sup> | string | Statement of reasons sent to the banned account holder (at most 512 characters) |
| notify_user?<sup>3</sup> | boolean | Whether to email the account holder about a temporary ban (default true) |
<sup>1</sup> 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
<sup>2</sup> Included in the temporary ban email and in the audit entry, but not stored on the account, so no read operation returns it
<sup>3</sup> A permanent ban sends no email whatever this field says
### Response body
| Field | Type | Description |
@@ -1438,11 +1441,11 @@ Every authentication session of the account is deleted. [Unban user](#unban-user
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.
When `notify_user` is true, 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. A failed email is logged and the ban still applies.
[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`.
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`, `banned_until`, `notify_user`, and `notification_sent`.
### Rate limit
@@ -1454,16 +1457,21 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje
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 |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| notify_user? | boolean | Whether to email the account holder that the ban was lifted (default true) |
| public_reason?<sup>1</sup> | string | Statement shown to the account holder in the email (at most 512 characters) |
<sup>1</sup> Also recorded in the audit entry. The `X-Audit-Log-Reason` value is never emailed
### Response body
| Field | Type | Description |
@@ -1477,15 +1485,15 @@ The notification email quotes the `X-Audit-Log-Reason` value verbatim as the rea
| 200<sup>1</sup> | response body | The ban was cleared |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
<sup>1</sup> 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
<sup>1</sup> The operation checks no precondition. On an account that was never banned it writes `temp_banned_until` as null, clears `DISABLED`, and sends no email
### 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.
Deleted sessions are not restored. Fluxer emails the account holder, with `public_reason` when one is supplied, only when `notify_user` is true, the ban was still in force, the account has an email address, and the account is neither closed nor pending deletion. A failed email is logged and the ban is still cleared. [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.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `unban`, target type `user`, and the metadata keys `notify_user`, `notification_sent`, and `public_reason`.
### Rate limit
@@ -1553,6 +1561,7 @@ The `X-Audit-Log-Reason` value is also stored on the account as the private dele
| public_reason?<sup>2</sup> | string | Statement of reasons shown to the account holder (at most 512 characters) |
| days_until_deletion?<sup>3</sup> | integer | Requested whole-day delay (1-365, default 60) |
| replace_pending_deletion_at?<sup>4</sup> | ISO8601 timestamp | The `pending_deletion_at` of the schedule this request replaces |
| notify_user? | boolean | Whether to email the account holder about the scheduled deletion (default true) |
<sup>1</sup> 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
@@ -1584,11 +1593,11 @@ The account can no longer authenticate, and its existing authentication sessions
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.
The account holder is emailed the deadline and the supplied `public_reason` when `notify_user` is true and the account has an email address. The wording depends on `reason_code`: `USER_REQUESTED` and `INACTIVITY` get a neutral notice, and every other code gets an enforcement notice. A failed email is logged and the schedule is still stored.
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. For every reason other than `USER_REQUESTED`, Fluxer also records a second entry with action `auto_resolve_reports_on_deletion`, target type `user`, and the metadata keys `outcome`, `found_count`, `resolved_count`, `failed_count`, and `notified_count`. `outcome` is `completed`, `search_unavailable` when the instance has no report search backend and every report stays pending, or `enumeration_failed` when listing the pending reports failed part way and only those already found were resolved. Each resolved report sends its reporter the generic notice [Update report](/admin-api/reports/#update-report) describes, with no public comment.
[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`, `pending_deletion_at`, `notify_user`, and `notification_sent`. An attempted email adds `notification_template`. 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. For every reason other than `USER_REQUESTED`, Fluxer also records a second entry with action `auto_resolve_reports_on_deletion`, target type `user`, and the metadata keys `outcome`, `found_count`, `resolved_count`, `failed_count`, and `notified_count`. `outcome` is `completed`, `search_unavailable` when the instance has no report search backend and every report stays pending, or `enumeration_failed` when listing the pending reports failed part way and only those already found were resolved. Each resolved report sends its reporter the generic notice [Update report](/admin-api/reports/#update-report) describes, with no public comment.
### Rate limit
@@ -70,6 +70,7 @@ The public single sign-on state. The same object is embedded by the [instance di
| enforced<sup>2</sup> | boolean | Whether SSO is required for every user |
| display_name | ?string | The configured provider display name, or null when none is set |
| redirect_uri | string | The default OAuth2 redirect URI used for the provider callback |
| available? | boolean | Whether an SSO provider is configured for this instance |
<sup>1</sup> The value is true only when the operator has enabled SSO and the provider has an authorisation URL, a token URL, a client ID, and a JWKS URL or user info URL. Each URL is configured or discovered from the issuer. A provider that lacks any of them reports false
@@ -230,7 +231,7 @@ The result of checking a password reset token without consuming it.
## Account recovery response object
The result of [recover an account](#recover-an-account). It is an [authentication token response](#authentication-token-response) or an [MFA challenge response](#mfa-challenge-response), with the two fields below added to either shape.
The result of [recover an account](#recover-an-account). It extends an [authentication token response](#authentication-token-response) or an [MFA challenge response](#mfa-challenge-response) with the two fields below, which are added to either shape.
### Structure
@@ -608,6 +609,8 @@ There is no retry key. A repeated request with the same values creates a second
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
The response schema also declares the [MFA challenge response](#mfa-challenge-response), which registration never returns, because a new account has no second factor.
### Side effects
The operation applies the instance's registration, email-domain, breached-password, and regional policies before creating the account. It records the accepted terms and privacy policy, authorises the registering client IP address, and sends an email verification message when the instance sends email. An instance that sends no email marks the address verified at creation instead.
@@ -1205,6 +1208,8 @@ Fluxer checks the replacement password against the same breached-password corpus
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
The response schema also declares the [MFA challenge response](#mfa-challenge-response), which this operation never returns, because the reversion clears every second factor.
### Side effects
The previous email address becomes verified and the replacement password becomes the account password. All other sessions, second factors and authorised IP addresses are removed. Only the requesting IP address remains authorised.
@@ -240,6 +240,7 @@ Neither the lifetime block nor the existing subscription block applies here, so
| client_geoip_country_code? | string | Two-letter country the client previously observed for itself (2 characters) |
| eu_withdrawal_waiver_accepted? | boolean | Whether the digital content withdrawal waiver was explicitly accepted |
| is_business? | boolean | Whether to require a billing address for tax invoicing (default false) |
| payment_method? | string | Either `card`, `pix` or `upi`, which this operation ignores |
<sup>1</sup> Read only when Fluxer cannot geolocate the request, because the request-time geolocation country wins. The resolved country is required whenever the gift price is not in USD or EUR
@@ -60,6 +60,7 @@ Each value is an absolute URL supplied by the operator. A value can be a bare or
| invite | string | Invite link base URL |
| gift | string | Gift link base URL |
| webapp | string | Web application base URL |
| docs? | string | Documentation website base URL |
<sup>1</sup> Both fields are the same configured client API endpoint, which the first-party web application reads. `api_public` is the separately configured public API endpoint, which a bot, a library, or any other third-party client reads. A deployment can point the two at one origin, and the instance Fluxer hosts does not
@@ -144,6 +145,9 @@ Deployment-wide switches a client reads before it offers a feature, plus whether
| phone_verification_enabled | boolean | Deprecated. Always false |
| account_identity<sup>5</sup> | string | How members sign in, one of the [account identity modes](#account-identity-modes) |
| tag_style<sup>6</sup> | string | How usernames are tagged, one of the [tag styles](#tag-styles) |
| desktop_modules_enabled? | boolean | Whether desktop clients may load downloadable modules |
| account_deletion_grace_period_hours? | integer | Hours between a deletion request and the permanent deletion of the account (1-8,760) |
| max_background_gateway_connections? | integer | Maximum Gateway connections a client may keep open for background accounts (0-64) |
<sup>1</sup> The value is true only when email is switched on and the transport is completely configured. It is always false on a `username` deployment
@@ -248,6 +252,10 @@ This deployment's public Web Push identity.
| Field | Type | Description |
| --- | --- | --- |
| public_vapid_key<sup>1</sup> | ?string | The deployment's Web Push VAPID public key, or null |
| delivery_mode? | string | How push notifications reach devices, one of `direct`, `external_relay`, or `hybrid` |
| event_resolution_path? | string | API path a client calls to resolve the payload of a push event |
| external_relay_url? | ?string | URL of the external push relay, or null when there is none |
| relay_signing_key_hashes? | ?array[string] | Hashes of the keys the external push relay signs with, or null |
<sup>1</sup> Null when the deployment has configured no Web Push identity
@@ -69,7 +69,7 @@ A guild invite always has `guild`, `channel`, `member_count`, and `presence_coun
## Invite metadata object
The metadata object extends the invite with its creation and usage state. [Create channel invite](#create-channel-invite), [List channel invites](#list-channel-invites), and [List guild invites](#list-guild-invites) return it. [Get invite](#get-invite) never does.
The metadata object extends the [invite](#invite-object) with its creation and usage state. [Create channel invite](#create-channel-invite), [List channel invites](#list-channel-invites), and [List guild invites](#list-guild-invites) return it. [Get invite](#get-invite) never does.
### Structure
@@ -45,7 +45,6 @@ A message object is the full stored form of one post in a channel.
| embeds<sup>4</sup> | array[[embed](#embed-object) object] | The previews resolved or supplied for the message |
| attachments<sup>5</sup> | array[[message attachment](#message-attachment-object) object] | The files attached to the message |
| stickers | array[[sticker item](#sticker-item-object) object] | The stickers sent with the message |
| nsfw_emojis? | array[snowflake] | IDs of the custom emojis in the message that are classified as explicit |
| reactions? | array[[reaction](#reaction-object) object] | Reaction summaries |
| message_reference? | [message reference](#message-reference-object) object | Reply, forward, or published message reference |
| message_snapshots? | array[[message snapshot](#message-snapshot-object) object] | The immutable copies captured for a forward |
@@ -394,7 +393,6 @@ Each sticker item is the trimmed sticker record on a message.
| id | snowflake | The ID of the sticker |
| name | string | Sticker name |
| animated | boolean | Whether the sticker is animated |
| nsfw? | boolean | Whether the sticker is classified as explicit |
## Embed object
@@ -520,6 +518,8 @@ Only an embed the unfurler builds from a link has a provider. A rich embed input
| name | string | Provider name |
| url? | string | Provider URL |
The response schema also declares `icon_url` and `proxy_icon_url`, which a provider never has.
## Allowed mentions object
An allowed mentions object selects which mentions written in the message text become active mentions, meaning the ones that notify a user or role.
@@ -1339,6 +1339,7 @@ The body is read exactly as it is for [Create message](#create-message), so a `m
| allowed_mentions?<sup>2</sup> | ?[allowed mentions](#allowed-mentions-object) object | Replacement active mention policy |
| flags?<sup>3</sup> | integer | Replacement [message flags](#message-flags) |
| attachments?<sup>1</sup> | array[[pre-uploaded attachment](#pre-uploaded-attachment-object) object \| [existing attachment reference](#existing-attachment-reference-object) object] | Complete retained and added attachment collection |
| message_snapshots?<sup>4</sup> | array | Declared by the body schema and never accepted |
<sup>1</sup> Replaces the stored collection outright, so an existing embed or attachment that the array omits is removed. Both collections are bounded by the same resolved limits as message creation
@@ -1346,7 +1347,7 @@ The body is read exactly as it is for [Create message](#create-message), so a `m
<sup>3</sup> Only the bits in [message flags](#message-flags) are retained, and the message keeps every other stored bit unchanged
A request that supplies `message_snapshots` is rejected with 403 `MISSING_PERMISSIONS`, so snapshot metadata cannot be edited through this route.
<sup>4</sup> A request that supplies it is rejected with 403 `MISSING_PERMISSIONS`, so snapshot metadata cannot be edited through this route
An omitted field preserves its stored value, and the message's `edited_timestamp` advances only when `content` changes. The request itself must supply at least one of content with visible characters, an embed, an attachment, or `flags`, and one that supplies none of them fails with 400 `CANNOT_SEND_EMPTY_MESSAGE`.
@@ -156,11 +156,13 @@ A billing cycle change scheduled for the end of the current period. It is return
| Field | Type | Description |
| --- | --- | --- |
| schedule_id | string | The payment provider subscription schedule ID that has the change |
| change_kind<sup>4</sup> | string | The kind of scheduled change, always `billing_cycle` here |
| current_billing_cycle<sup>1</sup> | ?string | The current [billing cycle](#billing-cycles) |
| target_billing_cycle | string | [Billing cycle](#billing-cycles) that takes effect at `effective_at` |
| effective_at | ISO8601 timestamp | The start of the scheduled future phase |
| current_price_id | ?string | The price ID of the active subscription item, or null when it cannot be resolved |
| target_price_id | ?string | The price ID of the scheduled future phase, or null when it cannot be resolved |
| target_amount_minor | ?integer | The unit amount of the target price, in the minor unit of `currency`, or null when it cannot be resolved |
| currency | ?string | [Display currency](#display-currencies) of the target price, or null when it is unresolved or unsupported |
| initial_amount_minor<sup>2</sup> | ?integer | The amount of the first invoice after the change, in the minor unit of `currency` |
| recurring_amount_minor | ?integer | The amount of each later invoice, in the minor unit of `currency` |
@@ -172,6 +174,8 @@ A billing cycle change scheduled for the end of the current period. It is return
<sup>3</sup> Null unless the scheduled adjustment is a credit. A monthly to yearly change credits the smaller of the two amounts, each multiplied by the subscription item quantity, and no credit is scheduled during a trial
<sup>4</sup> A scheduled move to the list price within the same billing cycle is not returned here. The [list price switch](#list-price-switch-object) reports it as `pending`
The complete value is null in each of these cases.
| Condition | Detail |
@@ -189,11 +193,13 @@ The complete value is null in each of these cases.
```json
{
"schedule_id": "sub_sched_1QaBcDeFgHiJ",
"change_kind": "billing_cycle",
"current_billing_cycle": "monthly",
"target_billing_cycle": "yearly",
"effective_at": "2026-09-28T08:15:00.000Z",
"current_price_id": "price_1QaMonthlyUsd",
"target_price_id": "price_1QaYearlyUsd",
"target_amount_minor": 4999,
"currency": "USD",
"initial_amount_minor": 4500,
"recurring_amount_minor": 4999,
@@ -201,6 +207,31 @@ The complete value is null in each of these cases.
}
```
## List price switch object
Whether the subscription can move to the current list price with [switch subscription to the list price](#switch-subscription-to-the-list-price). It is returned in [Premium billing state](#premium-billing-state-object).
### Structure
| Field | Type | Description |
| --- | --- | --- |
| available | boolean | Whether the switch can be requested now |
| reason<sup>1</sup> | ?string | The [refusal reason](#refusal-reasons) the switch would get, or null |
| pending<sup>2</sup> | boolean | Whether a switch to the current list price is already scheduled |
| current_price_id | ?string | The price ID the subscription bills on today |
| current_amount_minor | ?integer | The amount billed today, in the minor unit of `currency` |
| list_price_id | ?string | The current list price ID for the same billing cycle and currency |
| list_amount_minor | ?integer | The current list price amount, in the minor unit of `currency` |
| currency | ?string | [Display currency](#display-currencies) of both amounts |
| billing_cycle | ?string | The [billing cycle](#billing-cycles) the switch applies to |
| effective_at<sup>3</sup> | ?ISO8601 timestamp | When a switch takes effect |
<sup>1</sup> Null when `available` is true, and null when `pending` is true. `switch_in_progress` and `cancellation_managed_by_schedule` are only returned by the switch operation itself
<sup>2</sup> True when the scheduled change targets the current list price. Any other scheduled change gives `conflicting_pending_change`
<sup>3</sup> The end of the current billing period, or the start of the scheduled change when the subscription has one
## Premium state object
The account's entitlement, the effective entitlement that premium feature checks read, and the billing and pricing data behind both. [Get premium state](#get-premium-state) and [set premium perks disabled](#set-premium-perks-disabled) both return it.
@@ -407,6 +438,7 @@ Billing details can lag behind changes made through the payment provider.
| stripe_customer_id | ?string | The payment provider customer ID, or null when none was ever created |
| current_subscription_price | ?[current subscription price](#current-subscription-price-object) object | The price the account is billed against |
| pending_subscription_change | ?[pending subscription change](#pending-subscription-change-object) object | The scheduled billing cycle change |
| list_price_switch | [list price switch](#list-price-switch-object) object | Whether the subscription can move to the current list price |
| subscription | ?[billing subscription](#billing-subscription-object) object | The mirrored subscription Fluxer selects by the rule below this table |
| invoices<sup>1</sup> | array[[billing invoice](#billing-invoice-object)] | The most recent invoices, newest first |
| invoices_has_more<sup>2</sup> | boolean | Whether the account has invoices beyond the returned page |
@@ -940,7 +972,7 @@ A subscription set to cancel has no next billing period, so the switch would hav
### Side effects
A scheduled switch becomes visible as `billing.pending_subscription_change` with `change_kind` `price` in [get premium state](#get-premium-state), and `billing.list_price_switch` reports `pending`. Every account session receives [User Update](/gateway/events/#user-update). A pending cancellation is never cleared by this route.
A scheduled switch becomes visible in [get premium state](#get-premium-state) as `billing.list_price_switch` with `pending` true. `billing.pending_subscription_change` stays null, because it reports billing cycle changes only. Every account session receives [User Update](/gateway/events/#user-update). A pending cancellation is never cleared by this route.
### Rate limit
@@ -334,7 +334,9 @@ A missing, malformed, forged, or expired token and a token for another harvest a
| Field | Type | Description |
| --- | --- | --- |
| token | string | Signed download token issued with the download URL |
| token<sup>1</sup> | string | Signed download token issued with the download URL |
<sup>1</sup> Required, although the request schema does not declare it. A request without it returns 404
### Request headers
@@ -186,7 +186,7 @@ The input form of the [user settings object](/http-api/users/#user-settings-obje
The staff-only fields are the exception. Every update resets `suppress_unprivileged_self_mentions`, `suppress_unprivileged_self_mentions_bypass_user_ids`, and `staff_dm_access_user_ids` to false, empty, and empty for an ordinary account, including an update that names none of them.
Further members are accepted and ignored. `guild_positions` is an array of at most 200 guild snowflakes, and nothing reads it. `default_share_voice_activity` belongs to [Modify voice activity sharing](#modify-voice-activity-sharing).
Further members are accepted and ignored. Nothing reads `guild_positions`, and only [Modify voice activity sharing](#modify-voice-activity-sharing) writes `default_share_voice_activity`.
### Structure
@@ -230,6 +230,8 @@ Further members are accepted and ignored. `guild_positions` is an array of at mo
| profile_privacy? | integer | [Profile privacy level](/http-api/users/#profile-privacy-levels) |
| privacy_setup_version?<sup>14</sup> | integer | The privacy setup version the user just reviewed (1-1000) |
| synced_preferences?<sup>13</sup> | ?string | The base64-encoded `fluxer.user.preferences.v1.SyncedPreferences` snapshot, or null to clear it |
| guild_positions? | array[snowflake] | Accepted and ignored (max 200) |
| default_share_voice_activity? | boolean | Accepted and ignored |
<sup>1</sup> A string matches ISO 8601 and an integer is Unix milliseconds between 0 and 8640000000000000. Anything else is rejected with `INVALID_ISO_TIMESTAMP`
@@ -149,8 +149,8 @@ Every operation returning a [webhook object](#webhook-object) of an incoming web
| Field | Type | Description |
| --- | --- | --- |
| content?<sup>1</sup> | ?string | Message content |
| embeds?<sup>2</sup> | array[[rich embed input](/http-api/messages/#rich-embed-input-objects) object] | Rich embeds |
| attachments?<sup>3</sup> | array[[webhook attachment input](#webhook-attachment-input-object) object] | Attachment metadata |
| embeds?<sup>2</sup> | array[[rich embed](/http-api/messages/#rich-embed-object) object] | Rich embeds |
| attachments?<sup>3</sup> | array[[pre-uploaded attachment](/http-api/messages/#pre-uploaded-attachment-object) object] | Attachment metadata |
| message_reference?<sup>4</sup> | ?[message reference input](/http-api/messages/#message-reference-input-object) object | Reply or forward reference, or null |
| allowed_mentions?<sup>5</sup> | ?[allowed mentions](/http-api/messages/#allowed-mentions-object) object | Mention parsing policy, or null |
| flags? | integer | [Message flags](/http-api/messages/#message-flags), where omission is treated as zero and every bit outside the sendable mask is cleared |
@@ -165,7 +165,7 @@ Every operation returning a [webhook object](#webhook-object) of an incoming web
<sup>2</sup> Bounded by the resolved [max_embeds_per_message](/http-api/instance/#limit-keys) value for the guild, defaulting to 10. Exceeding it returns 400 `INVALID_FORM_BODY` with `TOO_MANY_EMBEDS`
<sup>3</sup> A JSON body attaches no file. A multipart body attaches files as described in [Execute webhook](#execute-webhook)
<sup>3</sup> Each entry names a file uploaded earlier by its `upload_filename`. A multipart body also attaches direct files, as [Execute webhook](#execute-webhook) describes
<sup>4</sup> A forward reference requires both `channel_id` and `message_id`, and it must not accompany content, embeds, or attachments. See [message references](#message-references)
@@ -181,34 +181,6 @@ When `embeds` is absent, Fluxer rewrites the singular key `embed` to a one-eleme
A multipart body accepts both [pre-uploaded attachments](/http-api/messages/#pre-uploaded-attachment-object) and [direct multipart attachment metadata](/http-api/messages/#direct-multipart-attachment-metadata-object).
## Webhook attachment input object
A JSON [webhook message body](#webhook-message-body) accepts this attachment metadata shape. Every member is optional, and a member outside this table is discarded.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| id? | snowflake \| integer | The ID of the attachment |
| filename? | string | Attachment filename (1-1024 characters) |
| description? | string | Attachment description (1-4096 characters) |
| content_type? | string | Media type (1-256 characters) |
| size? | integer | Byte count, as a non-negative safe integer |
| url? | string | Absolute `http` or `https` URL of at most 2,048 characters |
| proxy_url? | string | Absolute `http` or `https` URL of at most 2,048 characters |
| height? | integer | Pixel height |
| width? | integer | Pixel width |
| ephemeral? | boolean | Whether the attachment is ephemeral |
| duration? | number | Audio duration in seconds |
| waveform? | string | Base64 waveform data (1-256 characters) |
| flags? | integer | [Attachment flags](/http-api/messages/#attachment-flags) |
:::caution[A JSON body cannot attach a file]
The execution attaches only an entry with `upload_filename`, and this object defines no such member. A webhook that needs to attach a file uses a [multipart body](#execute-webhook).
:::
Every entry supplied in a JSON body is dropped, and it counts towards neither the attachment limit nor the non-empty message check.
## Message references
A webhook execution accepts the [message reference input](/http-api/messages/#message-reference-input-object) object. The constraints below are specific to webhooks.
@@ -228,7 +200,7 @@ A forward reference must not accompany content, embeds, or attachments, and one
| Field | Type | Description |
| --- | --- | --- |
| content?<sup>1</sup> | ?string | Replacement message content, or null |
| embeds?<sup>2</sup> | array[[rich embed input](/http-api/messages/#rich-embed-input-objects) object] | Replacement collection of embeds |
| embeds?<sup>2</sup> | array[[rich embed](/http-api/messages/#rich-embed-object) object] | Replacement collection of embeds |
| flags? | integer | Replacement [message flags](/http-api/messages/#message-flags) |
| allowed_mentions? | ?[allowed mentions](/http-api/messages/#allowed-mentions-object) object | Replacement mention parsing policy, or null |
@@ -271,7 +243,7 @@ These objects define the body [Execute Slack webhook](#execute-slack-webhook) ac
| title_link?<sup>3</sup> | string | Title URL, mapped to the embed URL |
| fields?<sup>4</sup> | array[[Slack attachment field](#slack-attachment-field-object) object] | Fields mapped to embed fields |
| footer? | string | Footer text, mapped to the embed footer |
| ts?<sup>5</sup> | number | Unix seconds, mapped to the embed timestamp |
| ts?<sup>5</sup> | integer | Unix seconds, mapped to the embed timestamp |
| author_name?<sup>6</sup> | string | Author name, mapped to the embed author name |
| author_link?<sup>3</sup> | string | Author URL, mapped to the embed author URL |
| author_icon?<sup>3</sup> | string | Author icon URL, mapped to the embed author icon |
@@ -1046,7 +1046,7 @@ With feeds off, Fluxer checks none of this data. The first worker start with fee
#### `FLUXER_REPORT_RETENTION_DRY_RUN`
Default dry run. Read by `worker`. The daily [report retention](/admin-api/reports/#report-retention) job deletes reports 365 days after they were filed, with their evidence copies and search entries, unless a legal hold is in place. Until this is `false` or `0`, the job only logs what it would delete. Any other value, or no value, keeps the dry run. Check the counts in the `Processed report retention` log line before you turn deletion on, because deleted evidence cannot be restored. Compose does not forward it, so add it to `worker` through an override file.
Default unset, which keeps the dry run. Read by `worker`. The daily [report retention](/admin-api/reports/#report-retention) job deletes reports 365 days after they were filed, with their evidence copies and search entries, unless a legal hold is in place. Until this is `false` or `0`, the job only logs what it would delete. Any other value, or no value, keeps the dry run. Check the counts in the `Processed report retention` log line before you turn deletion on, because deleted evidence cannot be restored. Compose does not forward it, so add it to `worker` through an override file.
#### `FLUXER_BREACHED_PASSWORD_CHECK_ENABLED`