Compare commits

..
95 changed files with 689 additions and 658 deletions
@@ -27,6 +27,7 @@ import {parse} from '@app/features/messaging/components/markdown/renderers';
import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes';
import MessageEdit from '@app/features/messaging/state/MessageEdit';
import {hasStyleableMessageText} from '@app/features/messaging/utils/FailedMessageDisplayUtils';
import {buildMessageContentCopyText} from '@app/features/messaging/utils/MessageCopyTextUtils';
import {
buildExistingAttachmentEditReferences,
canSubmitEmptyMessageEdit,
@@ -140,6 +141,16 @@ export const UserMessage = observer(() => {
}),
[message.id, message.channelId, message.mentionChannels],
);
const contentCopyText = useMemo(
() =>
buildMessageContentCopyText(astNodes, {
channelId: message.channelId,
messageId: message.id,
mentionChannels: message.mentionChannels,
i18n,
}),
[astNodes, message.id, message.channelId, message.mentionChannels, i18n.locale],
);
const shouldHideContent =
UserSettings.getRenderEmbeds() &&
message.embeds.length > 0 &&
@@ -281,7 +292,7 @@ export const UserMessage = observer(() => {
className={clsx(markupStyles.markup)}
data-search-highlight-scope="message"
data-flx="channel.user-message.render-message-content.div"
{...messageContentCopyBlockProps(message.content)}
{...messageContentCopyBlockProps(contentCopyText)}
>
<SafeMarkdown
content={message.content}
@@ -315,6 +326,7 @@ export const UserMessage = observer(() => {
shouldShowEditingInput,
shouldHideContent,
markdownOptions,
contentCopyText,
message,
message.content,
message.id,
@@ -415,7 +427,7 @@ export const UserMessage = observer(() => {
className={clsx(markupStyles.markup)}
data-search-highlight-scope="message"
data-flx="channel.user-message.div"
{...messageContentCopyBlockProps(message.content)}
{...messageContentCopyBlockProps(contentCopyText)}
>
<SafeMarkdown
content={message.content}
@@ -244,7 +244,7 @@ function renderTableRow(
export function TableRenderer({node, id, renderChildren, options}: RendererProps<TableNode>): React.ReactElement {
const copyText = renderTableNodeToMarkdown(node, {
channelId: options.channelId,
preserveMarkdown: true,
preserveMarkdown: false,
includeEmojiNames: true,
i18n: options.i18n,
});
@@ -3,7 +3,12 @@
import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes';
import type {Message} from '@app/features/messaging/models/MessagingMessage';
import {getParserFlagsForContext} from '@app/features/messaging/utils/markdown/MarkdownParserFlags';
import {parseAndRenderToPlaintext} from '@app/features/messaging/utils/markdown/Plaintext';
import {
type PlaintextRenderOptions,
parseAndRenderToPlaintext,
renderAstToPlaintext,
} from '@app/features/messaging/utils/markdown/Plaintext';
import type {Node} from '@app/features/messaging/utils/markdown/parser/Nodes';
import * as DateUtils from '@app/features/user/utils/DateFormatting';
import {MessageEmbedTypes} from '@fluxer/constants/src/ChannelConstants';
import type {MessageEmbed} from '@fluxer/schema/src/domains/message/EmbedSchemas';
@@ -102,6 +107,17 @@ function buildOmittedEmbedUrls(content?: string | null, renderedContent?: string
return urls;
}
function createPlaintextCopyOptions(context: MarkdownCopyContext): PlaintextRenderOptions {
return {
channelId: context.channelId,
preserveMarkdown: false,
includeEmojiNames: true,
includeLinkUrls: true,
mentionChannels: context.mentionChannels,
i18n: context.i18n,
};
}
function renderMarkdownCopyText(
content: string | undefined | null,
parserFlags: number,
@@ -110,14 +126,11 @@ function renderMarkdownCopyText(
if (!content) {
return '';
}
return parseAndRenderToPlaintext(content, parserFlags, {
channelId: context.channelId,
preserveMarkdown: false,
includeEmojiNames: true,
includeLinkUrls: true,
mentionChannels: context.mentionChannels,
i18n: context.i18n,
});
return parseAndRenderToPlaintext(content, parserFlags, createPlaintextCopyOptions(context));
}
export function buildMessageContentCopyText(nodes: Array<Node>, context: MarkdownCopyContext): string {
return normaliseCopyBlock(renderAstToPlaintext(nodes, createPlaintextCopyOptions(context)));
}
function buildAttachmentCopyText(attachment: MessageAttachment): string {
@@ -44,7 +44,7 @@ describe('Message selection copy utils', () => {
expect(buildMessageSelectionCopyTextForRange({rootElement: root, selectionRange: range})).toBe(tableCopyText);
});
it('does not duplicate a block message header when a bot badge is selected with the body', () => {
const messageContent = '## App Canary Deployed\n\nVersion: `2026.519.3`\nImage: `2026.519.3`';
const messageContent = 'App Canary Deployed\n\nVersion: 2026.519.3\nImage: 2026.519.3';
document.body.innerHTML = [
'<div data-message-selection-root="true">',
'<div data-message-id="message-1" data-is-group-start="true">',
@@ -6,7 +6,7 @@ description: Admin API key objects and the operations that manage them.
import RouteHeader from '@/components/RouteHeader.astro';
An Admin API key is a long-lived credential for the [Admin API](/admin-api/). It authenticates as the account that created it, has its own [ACL](/admin-api/#acl-registry) set, and is honoured only on paths below `/v1/admin`.
An Admin API key is a long-lived credential for the [Admin API](/admin-api/). It authenticates as the account that created it, has its own [ACL](/admin-api/#acl-registry) set, and authenticates only requests to paths below `/v1/admin`.
Every operation on this page requires `admin_api_key:manage`. None records an Admin audit entry or reads the `X-Audit-Log-Reason` header. An operation that addresses one key returns 404 `ADMIN_API_KEY_NOT_FOUND` when any of these holds:
@@ -20,7 +20,7 @@ The wildcard ACL reaches no key created by another account, and such a key repor
## Admin API key object
A key is owned by the account that created it, and it stores its own ACL set. Narrowing the owner and narrowing the key are separate actions.
A key is owned by the account that created it, and it stores its own ACL set. Removing an ACL from the owning account and removing an ACL from the key are separate changes.
Fluxer checks both sets when a request presents a key. A request passes when all of these hold:
@@ -210,7 +210,7 @@ An update never rotates the credential, and no field on this route changes the e
<sup>1</sup> A value that is empty after trimming is rejected
<sup>2</sup> An empty array leaves the key with no ACLs, the narrowest state short of revocation. An empty request body is accepted and changes nothing
<sup>2</sup> An empty array leaves the key with no ACLs, so the key satisfies no operation. An empty request body is accepted and changes nothing
### Response
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
An application is an OAuth2 client that can own one bot account. These routes inspect applications and [transfer their ownership](#transfer-application-ownership).
These records are the same ones the public [Applications](/http-api/applications/) resource serves. Creation, deletion, renaming, bot creation, redirect URI editing, and credential rotation stay there.
These records are the same ones the public [Applications](/http-api/applications/) resource serves. Creation, deletion, renaming, bot creation, redirect URI editing, and credential rotation are available only through that resource.
:::note[Admin operations report credential metadata only]
The Admin application object reports whether a client secret or bot token hash is stored, when it was created, and the non-secret bot token preview.
@@ -42,9 +42,9 @@ The Admin view of one application. An application and its bot account share one
| client_secret_created_at<sup>4</sup> | ?ISO8601 timestamp | When the client secret was created |
| version<sup>5</sup> | integer | The optimistic locking version of the stored record (0-2147483647) |
<sup>1</sup> Null when the owner account can no longer be read
<sup>1</sup> Null when no account with `owner_user_id` exists
<sup>2</sup> Null when the application has no bot, and also null when the bot account can no longer be read
<sup>2</sup> Null when the application has no bot, and also null when no account with `bot_user_id` exists
<sup>3</sup> The stored set arrives in no guaranteed order
@@ -85,11 +85,11 @@ Fluxer serves one synthetic application for its own Admin OAuth2 client. The app
- `id` is the fixed constant `1234567890123456789`.
- `name` is `Fluxer Admin`, and `owner_user_id` is the system account `0`.
- `oauth2_redirect_uris` has one entry, the configured Admin endpoint followed by `/oauth2_callback`.
- `has_client_secret` is true on every response that has it, and `client_secret_created_at` is null.
- `has_client_secret` is true in every response that returns this application, and `client_secret_created_at` is null.
- `version` is always 1.
:::caution[The built-in application is read-only]
[Get application](#get-application) resolves it only while a client secret is configured for the deployment, and answers a null `application` otherwise. [List applications](#list-applications) never returns it. [Transfer application ownership](#transfer-application-ownership) answers 403 `FORBIDDEN`.
[Get application](#get-application) returns it only while an Admin client secret is configured for the deployment, and returns a null `application` otherwise. [List applications](#list-applications) never returns it. [Transfer application ownership](#transfer-application-ownership) answers 403 `FORBIDDEN`.
:::
## List applications
@@ -172,7 +172,7 @@ Fluxer answers 200 with `application` set to null when the ID names no applicati
Moves the application to another account and returns the stored [Admin application](#admin-application-object) object. Requires `application:transfer_ownership`.
Ownership is the only field this operation writes. The new owner need not be related to the application, and Fluxer does not notify the previous owner.
Ownership is the only field this operation writes. The new owner can be any existing account, and Fluxer does not notify the previous owner.
### Path parameters
@@ -186,7 +186,7 @@ Ownership is the only field this operation writes. The new owner need not be rel
| --- | --- | --- |
| new_owner_id<sup>1</sup> | snowflake | The account to transfer the application to |
<sup>1</sup> Fluxer resolves the application before the account, so an unknown application fails with 404 `UNKNOWN_APPLICATION` and an unknown replacement owner fails with 404 `UNKNOWN_USER`
<sup>1</sup> An unknown application fails with 404 `UNKNOWN_APPLICATION`, and an unknown replacement owner fails with 404 `UNKNOWN_USER`. Fluxer looks up the application first, so a request where both are unknown fails with `UNKNOWN_APPLICATION`
### Response body
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
An archive is a snapshot of one user account or one guild, built in the background and downloaded as a file. [Create user archive](/admin-api/users/#create-user-archive) and [Create guild archive](/admin-api/guilds/#create-guild-archive) request one. The operations here read archive state and issue time-limited download URLs.
Assets that no longer exist are omitted. Other read or write failures fail the attempt.
While Fluxer builds an archive, it leaves out any stored file that is missing from storage. Any other read or write failure fails that build attempt and sets `failed_at`.
Every operation needs an [ACL](/admin-api/#acl-evaluation) covering the subject type it touches.
@@ -22,7 +22,7 @@ An archive becomes unavailable through these routes at `expires_at`, 365 days af
## Archive object
An archive is in one of these lifecycle states. It is building while `completed_at` and `failed_at` are both null, complete once `completed_at` is set, and failed once `failed_at` is set. A retried attempt clears `failed_at` and `error_message` when it starts, so a failed archive reads as building again while the retry runs.
An archive is in one of these lifecycle states. It is building while `completed_at` and `failed_at` are both null, complete once `completed_at` is set, and failed once `failed_at` is set. When Fluxer starts a new attempt at a failed archive, it clears `failed_at` and `error_message`, so the archive reads as building again while that attempt runs.
### Structure
@@ -121,7 +121,7 @@ Returns [archive](#archive-object) objects matching the supplied filters, newest
<sup>2</sup> `subject_type` also has to name `user` or `guild`
<sup>3</sup> Ignored when `subject_id` is supplied. When it does apply it runs regardless of `subject_type`
<sup>3</sup> Ignored when `subject_id` is supplied. When `requested_by` applies, Fluxer ignores `subject_type` and returns that account's archives of both subject types
<sup>4</sup> The listing is not paginated and returns no cursor. Only a narrower filter reaches older records
@@ -6,7 +6,7 @@ description: The safety blocklists, their value forms, and the operations that m
import RouteHeader from '@/components/RouteHeader.astro';
Fluxer has nine blocklists for account access and user content. Each defines its accepted values, matching rules and metadata. Values are normalised consistently when added and checked.
Fluxer has nine blocklists for account access and user content. Each defines its accepted values, matching rules and metadata. Fluxer normalises a value when it adds the value and again when it checks the value.
Each list has its own [Admin ACLs](/admin-api/#acl-registry). A read needs the selected list's `check` permission, an addition or an update needs its `add` permission, and a removal needs its `remove` permission. An account holding `ban:ip:add` writes to the `ip` list and to no other. Every write records the audit reason on the [Admin audit entries](/admin-api/#admin-audit-entry-object) it produces. The reads record nothing.
@@ -32,13 +32,13 @@ Fluxer synchronises disposable email domains from external feeds every six hours
| avatar-hash<sup>8</sup> | Avatar hashes blocked from being set |
| profile-substring<sup>4</sup> <sup>9</sup> | Substrings blocked from one named profile field |
<sup>1</sup> Fluxer refuses an address with 400 `IP_BAN_DECLINED` when it is on the instance exemption list, or when the high blast-radius carrier network check matches, and records both refusals in the Admin audit log
<sup>1</sup> Fluxer refuses an address with 400 `IP_BAN_DECLINED` when it is on the instance exemption list, or when IP lookup data shows that a single address is on a mobile carrier network, and records both refusals in the Admin audit log
<sup>2</sup> Stored lowercased, so a mixed-case value does not create a second row
<sup>3</sup> Fluxer never shows the list to the account holder, who sees only the verified-phone gate. A domain must match `^[a-zA-Z0-9][a-zA-Z0-9\-.]*\.[a-zA-Z]{2,}$`
<sup>4</sup> Canonicalised by NFKC normalisation, removal of control, format and variation-selector characters, lowercasing, and trimming. Match time also normalises inserted whitespace, punctuation, and compatibility characters
<sup>4</sup> Canonicalised by NFKC normalisation, removal of control, format and variation-selector characters, lowercasing, and trimming. When Fluxer matches a value, it also normalises inserted whitespace, punctuation, and compatibility characters
<sup>5</sup> Canonicalised before storage. A value Fluxer cannot canonicalise returns 400 `INVALID_FORM_BODY` naming `url`
@@ -214,7 +214,7 @@ A value can be blocked with no row of its own, as a single address covered by a
## Blocklist entry creation object
The body of [Add blocklist entry](#add-blocklist-entry) is a union resolved by the `list_type` path segment. The field that has the value differs per blocklist, and two of the nine take an array, so one request can add up to 1000 avatar hashes or profile substrings.
The body of [Add blocklist entry](#add-blocklist-entry) has one shape per blocklist, and the `list_type` path segment selects the shape. The field that has the value differs per blocklist, and two of the nine take an array, so one request can add up to 1000 avatar hashes or profile substrings.
### Value field
@@ -365,13 +365,13 @@ Every write is an upsert on the canonical value. An omitted optional field is wr
A body field the selected blocklist does not accept is stripped and never produces that 400. A `url` Fluxer cannot canonicalise returns 400 `INVALID_FORM_BODY` naming `url` in the `errors` array.
Fluxer refuses to add an `ip` with 400 `IP_BAN_DECLINED` when the address is on the instance exemption list, and when a single address is classified as a high blast-radius mobile or carrier network. It skips the blast-radius guard for a CIDR range, and a lookup failure counts as no risk, so the address is written.
Fluxer refuses to add an `ip` with 400 `IP_BAN_DECLINED` when the address is on the instance exemption list, and when a single address is classified as a high blast-radius mobile or carrier network. Fluxer runs that carrier network check only for a single address, so the check never refuses a CIDR range. When the IP lookup for the check fails, Fluxer treats the address as low risk and writes it.
The response has no body, so it does not report the canonical form that was stored. Read it back with [List blocklist entries](#list-blocklist-entries).
### Side effects
The written rows take effect for subsequent blocklist decisions. For every list except `email` and `email-domain-suspicious`, other nodes see the rows after a short propagation delay. No Gateway Dispatch is emitted.
Fluxer checks later requests against the written rows. For every list except `email` and `email-domain-suspicious`, other nodes see the rows after a short propagation delay. No Gateway Dispatch is emitted.
Fluxer records one [Admin audit entry](/admin-api/#admin-audit-entry-object) per written value, with that value in its metadata. It records an entry for a refused `ip` too, under the action `ban_ip_skipped_exempt` or `ban_ip_skipped_cgnat`, before it returns the 400.
@@ -469,7 +469,7 @@ The operation is idempotent and reports no counts. A value with no stored row st
### Side effects
Removed entries stop affecting subsequent blocklist decisions after changes propagate across the instance. A value can remain blocked by another matching entry. No Gateway Dispatch is emitted.
After the removal reaches every node, Fluxer no longer checks later requests against the removed entries. A value can remain blocked by another matching entry. No Gateway Dispatch is emitted.
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) per canonical value processed.
@@ -518,7 +518,7 @@ Reports whether one value is currently blocked by the selected blocklist and ret
<sup>4</sup> Two addresses are the same origin when they share a decision key, which is the exact address for IPv4 and the `/64` prefix for IPv6
<sup>5</sup> A domain the account policy exempts from contact-domain reputation reads as not blocked even while a row exists
<sup>5</sup> A domain that the instance account policy marks as exempt from reputation checks reads as not blocked even while a row exists
<sup>6</sup> This check does not read the `url-domain` list, so a URL that a stored domain blocks reads as not blocked. Check the hostname separately
@@ -598,7 +598,7 @@ Changing the `scope` of a `profile-substring` row writes a second row under the
### Side effects
The updated fields take effect for subsequent matches after changes propagate across the instance. No Gateway Dispatch is emitted.
After the change reaches every node, later matches use the updated fields. No Gateway Dispatch is emitted.
The write records one [Admin audit entry](/admin-api/#admin-audit-entry-object) under the same action an add records, with the canonical value but no previous values of the changed fields.
@@ -692,7 +692,7 @@ Both fields are optional, so an empty body and `{}` are both valid requests.
The account keeps the blocked avatar until it next sets one. Clearing it is a separate request naming `avatar` in the `fields` array of [Clear user profile fields](/admin-api/users/#clear-user-profile-fields).
Every account using that image shares the same truncated prefix, so blocking the hash blocks the image for every account.
The avatar hash is the first 8 characters of the MD5 digest of the image, so every account using that image has the same avatar hash, so blocking the hash blocks the image for every account.
### Side effects
@@ -75,7 +75,7 @@ Body validation runs ahead of authentication and ACL evaluation, so a malformed
### JSON body
The `task` discriminator selects one of these structures. Every ID array has an upper bound and no lower bound, so an empty array queues a job that processes nothing and settles as `succeeded`.
The `task` discriminator selects one of these structures. Every ID array has an upper bound and no lower bound, so an empty array queues a job that processes nothing and finishes with the status `succeeded`.
#### Update user flags structure
@@ -86,7 +86,7 @@ The `task` discriminator selects one of these structures. Every ID array has an
| add_flags?<sup>1</sup> | array[string] | [Account flag](/admin-api/users/#account-flags) values to add (max 64, default empty) |
| remove_flags?<sup>1</sup> | array[string] | [Account flag](/admin-api/users/#account-flags) values to remove (max 64, default empty) |
<sup>1</sup> Each entry is one 64-bit flag value written as an unsigned decimal string, such as `1024`. The boundary rejects a symbolic name. Additions are applied before removals, so a value named in both arrays ends up cleared
<sup>1</sup> Each entry is one 64-bit flag value written as an unsigned decimal string, such as `1024`. Body validation rejects a symbolic name. Additions are applied before removals, so a value named in both arrays ends up cleared
#### Update suspicious activity flags structure
@@ -110,7 +110,7 @@ The `task` discriminator selects one of these structures. Every ID array has an
| add_features?<sup>1</sup> | array[string] | [Guild features](/http-api/guilds/#guild-features) to add (max 100, default empty) |
| remove_features?<sup>1</sup> | array[string] | [Guild features](/http-api/guilds/#guild-features) to remove (max 100, default empty) |
<sup>1</sup> The boundary accepts any string, so a name outside the registry is written to the guild's feature set. Additions are applied before removals
<sup>1</sup> Body validation accepts any string, so a name outside the registry is written to the guild's feature set. Additions are applied before removals
#### Add guild members structure
@@ -148,7 +148,7 @@ The [job](/admin-api/jobs/) records the acting Admin and audit reason. Queueing
Entities are processed in the submitted order. [Cancel job](/admin-api/jobs/#cancel-job) stops the run between entities and sets its status to `cancelled`. Completed changes remain in place. A failed or unknown entity counts as failed without stopping the remaining work.
Progress updates arrive before work starts, after every 25 entities, and at completion. `schedule_user_deletion` updates after every 10 accounts instead. The final message includes successful and failed counts.
Every task updates its progress before work starts and at completion. Every task except `schedule_user_deletion` also updates its progress after every 25 entities, and `schedule_user_deletion` updates its progress after every 10 accounts. The final `progress_message` has the successful and failed counts.
Every task writes one summary Admin audit entry when it finishes, with the action `bulk_update_user_flags`, `bulk_update_suspicious_activity_flags`, `bulk_update_guild_features`, `bulk_add_guild_members`, `bulk_schedule_deletion`, `bulk_ban_file_shas`, or `bulk_delete_user_messages`. The summary has the audit reason, the entity count, the operation-specific parameters, the job identifier, and the processed, successful, and failed counts. Its `target_type` is `bulk_job` and its `target_id` is the job identifier, except for `add_guild_members`, which targets the guild. A failed job writes no summary entry. A cancelled job writes one, marked `cancelled`, covering the entities it processed before it stopped.
@@ -158,7 +158,7 @@ Every task writes one summary Admin audit entry when it finishes, with the actio
`update_guild_features` writes one `update_features` entry for each guild, dispatches [Guild Update](/gateway/events/#guild-update), and reindexes the guild for search. Fluxer reconciles a guild that already has a discovery application record against the new feature set, so gaining `DISCOVERABLE` approves the record and losing it marks the record removed. A guild with no discovery record is left alone.
`add_guild_members` bypasses the ban check and the risk gate. The task suppresses the join system message, records the join source as an Admin force add, and dispatches [Guild Member Add](/gateway/events/#guild-member-add) to the guild and [Guild Create](/gateway/events/#guild-create) to the added account's sessions. The task still enforces the per-account guild cap and the guild member cap, so an account at either ceiling is counted as failed. An account that is already a member is left unchanged and counted as successful, with no second membership and no Dispatch. Adding a bot account also records a `BOT_ADD` guild audit log entry attributed to the acting Admin.
`add_guild_members` bypasses the ban check and the deferred phone verification check that a normal join runs for an account without a verified phone. The task suppresses the join system message, records the join source as an Admin force add, and dispatches [Guild Member Add](/gateway/events/#guild-member-add) to the guild and [Guild Create](/gateway/events/#guild-create) to the added account's sessions. The task still enforces the per-account guild cap and the guild member cap, so an account at either ceiling is counted as failed. An account that is already a member is left unchanged and counted as successful, with no second membership and no Dispatch. Adding a bot account also records a `BOT_ADD` guild audit log entry attributed to the acting Admin.
`delete_user_messages` deletes every message each account wrote, across every channel. It writes one `delete_all_user_messages` entry for each account, with the audit reason, the account, and the deleted message count, and reports progress after every account.
@@ -40,7 +40,7 @@ A guild's submitted application, together with the guild details Fluxer resolves
| custom_tags<sup>6</sup> <sup>8</sup> | array[string] | Normalised [custom tags](/http-api/discovery/#custom-tags) of the listing |
| applied_at | ISO8601 timestamp | Time at which the guild applied |
<sup>1</sup> Resolved from the guild at request time. A guild that can no longer be resolved yields the literal name `(unknown guild)`, a null icon, the owner ID `0`, a member count of 0, a null NSFW level, an empty feature array, and null for all owner name fields
<sup>1</sup> Resolved from the guild at request time. A guild Fluxer fails to load yields the literal name `(unknown guild)`, a null icon, the owner ID `0`, a member count of 0, a null NSFW level, an empty feature array, and null for all owner name fields
<sup>2</sup> Read from the owner account, so it is null when the guild resolved but the owner account did not
@@ -92,7 +92,7 @@ An approved listing. It has every field of the [Admin pending application object
## Discovery application object
This is the [discovery application object](/http-api/discovery/#discovery-application-object) of the public Discovery resource. Every write on this page answers with it, so a client re-reads the listing after a write to render the enriched guild fields. No response on this page exposes the Admin that approved, rejected, or removed the application.
This is the [discovery application object](/http-api/discovery/#discovery-application-object) of the public Discovery resource. Every write on this page answers with it, so a client that needs the guild name, icon, owner, member count, NSFW level, or features re-reads the listing after a write. No response on this page exposes the Admin that approved, rejected, or removed the application.
### Structure
@@ -317,7 +317,7 @@ Files every named guild under one [discovery category](/http-api/discovery/#disc
Fluxer attempts the guilds one at a time and in order, and a failure does not stop the ones after it. Read `failed_guild_ids` for the guilds that did not move. The operation never returns 404.
:::
Repeating the request is safe. A pending application moves on the same terms as an approved one, so this operation can refile a guild that is still awaiting review.
Repeating the request is safe. The operation moves a pending application the same way it moves an approved one, so this operation can refile a guild that is still awaiting review.
### Side effects
@@ -371,7 +371,7 @@ Every member is optional. An omitted member preserves the stored value, and an e
### Side effects
The supplied members update the application while preserving its status, submission time, review time, review reason, and removal fields. Editing an approved listing does not return it to review. The new copy is visible in the next public search. Editing a pending application changes nothing a public reader can see.
The supplied members update the application while preserving its status, submission time, review time, review reason, and removal fields. Editing an approved listing does not return it to review. The new description, category, language, and tags appear in the next public search. Editing a pending application changes nothing a public reader can see.
No guild field changes, so no [Guild Update](/gateway/events/#guild-update) fires.
@@ -245,7 +245,7 @@ The counts cover voice states in guild channels and in calls. A node that does n
<RouteHeader method="POST" path="/v1/admin/gateway/reloads" />
Rebuilds server-side state for the supplied guilds from the database and returns how many live guild processes were selected. Requires `gateway:reload_all`.
Fetches the guild data for the supplied guilds from the database again and sends it to each live guild process and returns how many live guild processes were selected. Requires `gateway:reload_all`.
### JSON body
@@ -277,7 +277,7 @@ Every reloaded guild process fires one [Guild Update](/gateway/events/#guild-upd
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | Every selected guild process was dispatched |
| 200 | response body | Fluxer started a reload for every selected guild process |
### Side effects
@@ -11,12 +11,12 @@ Admin gift code generation issues [premium](/http-api/premium/) codes in bulk, e
This resource has that one operation. No Admin operation lists, revokes, or redeems a code, or reports who redeemed one.
:::caution[A self-hosted instance answers 403]
The error code is `FEATURE_NOT_AVAILABLE_SELF_HOSTED`. Body validation runs first, so a request that also fails validation is refused by that failure.
The error code is `FEATURE_NOT_AVAILABLE_SELF_HOSTED`. Body validation runs first, so a self-hosted instance answers a body that fails validation with 400 `INVALID_FORM_BODY`.
:::
## Gift duration units
The unit and the quantity together define the span a redeemed code grants. Fluxer adds the span to the redeemer's entitlement anchor at redemption time. Lifetime gifts are not supported.
The unit and the quantity together define the span a redeemed code grants. At redemption, Fluxer adds the span to the entitlement anchor, which is the latest of the current time, the end of the redeemer's premium, and the end of any earlier gift extension. Lifetime gifts are not supported.
| Value | Description |
| --- | --- |
@@ -47,7 +47,7 @@ The compact guild representation returned by [List guilds](#list-guilds) and by
<sup>3</sup> No operation populates these, so they are absent from every current response
<sup>4</sup> Present only on [List user guilds](/admin-api/users/#list-user-guilds) when that operation is asked for counts
<sup>4</sup> Present only on [List user guilds](/admin-api/users/#list-user-guilds) when the request sets `with_counts` to true
### Example
@@ -175,7 +175,7 @@ A custom emoji or a sticker of a guild, with a resolvable media URL. The listing
| creator_id | snowflake | The account that uploaded the expression |
| media_url<sup>1</sup> | string | The [Media Proxy](/media-proxy/routes/) URL the expression is served from (1-2048 characters) |
<sup>1</sup> Always the WebP representation, at size 160 for an emoji and size 320 for a sticker, and it has the `animated=true` selector only when `animated` is true
<sup>1</sup> Always the WebP representation, at size 160 for an emoji and size 320 for a sticker, and the URL has the `animated=true` query parameter only when `animated` is true
### Example
@@ -218,7 +218,7 @@ One entry of the `errors` array returned by [Purge guild assets](#purge-guild-as
| id | string | The asset that could not be purged, as supplied |
| error<sup>1</sup> | string | The reason the asset could not be purged (1-4000 characters) |
<sup>1</sup> The operation produces `Invalid numeric ID`, `Asset belongs to another guild`, and `Failed to purge asset`. Any other value is the message of the underlying failure
<sup>1</sup> The operation produces `Invalid numeric ID`, `Asset belongs to another guild`, and `Failed to purge asset`. Any other value is the message of the error Fluxer hit while purging that ID
## Asset types
@@ -226,7 +226,7 @@ One entry of the `errors` array returned by [Purge guild assets](#purge-guild-as
| --- | --- |
| emoji | The ID resolved to a custom emoji owned by the requested guild |
| sticker | The ID resolved to a sticker owned by the requested guild |
| unknown | The ID matched no emoji and no sticker record, and only associated media was queued for removal |
| unknown | The ID matched no emoji or sticker, and Fluxer queued removal of any emoji and sticker media stored under that ID |
An ID owned by a different guild appears in `errors` with no asset type.
@@ -246,7 +246,7 @@ The index matches `q` against the guild name, the discovery tags, the custom inv
| limit? | integer | Maximum guilds to return (1-200, default 50) |
| offset? | integer | Guilds to skip before returning results (0-10000, default 0) |
<sup>1</sup> A `q` of all decimal digits also resolves that value as an exact guild ID, but only while `offset` is 0. A guild the index did not return is prepended to `guilds` and adds one to `total`
<sup>1</sup> A `q` of all decimal digits also resolves that value as an exact guild ID, but only while `offset` is 0. When the index did not return the guild with that ID, Fluxer prepends it to `guilds` and adds one to `total`
### Response body
@@ -356,7 +356,7 @@ A body with no field at all selects nothing, so an empty patch applies no change
A code already claimed by any invite is rejected with 400 `INVALID_FORM_BODY` and the validation code `THIS_VANITY_URL_IS_ALREADY_TAKEN` against `vanity_url_code`.
The channel references, idle timeout, and message history cutoff of a guild are not Admin-writable. They change through the public [Modify guild](/http-api/guilds/#modify-guild) operation.
`afk_channel_id`, `system_channel_id`, `afk_timeout`, and `message_history_cutoff` of a guild are not Admin-writable. They change through the public [Modify guild](/http-api/guilds/#modify-guild) operation.
### Response body
@@ -490,7 +490,7 @@ A member query the main Gateway reports as failed returns 502 `BAD_GATEWAY`, an
Adds a user to a guild without an invite. Requires `guild:force_add_member`. The operation takes no request body.
Only the Admin ACL is evaluated.
Fluxer checks only the Admin ACL. The acting account needs no membership and no permission in the guild.
### Path parameters
@@ -515,11 +515,11 @@ Only the Admin ACL is evaluated.
### Side effects
Fluxer creates the membership with the Admin force-add join source and restores a communication timeout still in force from a previous membership. The guild ban list is not checked, so a banned user can be admitted. The suspicious activity phone gate does not run. The per-user guild limit and the guild member limit are still enforced.
Fluxer creates the membership with the Admin force-add join source and restores a communication timeout still in force from a previous membership. The guild ban list is not checked, so a banned user can be admitted. The [deferred phone gate](/admin-api/instance/#deferred-phone-gate-object) does not run. The per-user guild limit and the guild member limit are still enforced.
[Guild Member Add](/gateway/events/#guild-member-add) fires to the guild, and the user's sessions are joined to the guild on the main Gateway. The member enters guild member search when the guild has an indexed member set. The ordinary join system message is created, and with it a [Message Create](/gateway/events/#message-create) Dispatch, unless the guild sets `SUPPRESS_JOIN_NOTIFICATIONS` or has no usable system channel. A bot target also records a `BOT_ADD` entry in the guild's own [audit log](/http-api/guild-audit-logs/#audit-actions).
[Guild Member Add](/gateway/events/#guild-member-add) fires to the guild, and the user's sessions are joined to the guild on the main Gateway. When the guild's members are already indexed for search, Fluxer adds the new member to guild member search. The ordinary join system message is created, and with it a [Message Create](/gateway/events/#message-create) Dispatch, unless the guild sets `SUPPRESS_JOIN_NOTIFICATIONS` or has no `system_channel_id`, or its `system_channel_id` names a channel that no longer exists. A bot target also records a `BOT_ADD` entry in the guild's own [audit log](/http-api/guild-audit-logs/#audit-actions).
A user who is already a member keeps their existing membership. No membership is created, no counter moves, and no Dispatch is emitted. The Admin audit entry is still written.
A user who is already a member keeps their existing membership. No membership is created, `member_count` does not change, and no Dispatch is emitted. The Admin audit entry is still written.
One Admin audit entry is recorded with the action `force_add_to_guild`, the target type `user`, the added user as the target, and the guild ID in the metadata.
@@ -557,7 +557,7 @@ The acting account must see the guild, hold [KICK_MEMBERS](/http-api/permissions
### Side effects
The user loses membership and disappears from guild member search. A later rejoin restores previous membership settings, including any communication timeout still in force.
The user loses membership and disappears from guild member search. A later rejoin restores a communication timeout that is still in force. The rejoined member starts with no nickname and no roles.
[Guild Member Remove](/gateway/events/#guild-member-remove) fires to the guild. A `MEMBER_KICK` entry is written to the guild's own [audit log](/http-api/guild-audit-logs/#audit-actions). The entry names the acting Admin account and has the audit reason. The write fires [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) to sessions holding [VIEW_AUDIT_LOG](/http-api/permissions/).
@@ -735,7 +735,7 @@ Every ID in `processed` loses its record and its stored media permanently. The o
A record owned by the guild in the path is deleted, and its stored media is queued for removal. The guild then receives one [Guild Emojis Update](/gateway/events/#guild-emojis-update) or [Guild Stickers Update](/gateway/events/#guild-stickers-update) Dispatch with its complete remaining expression set. A request that purges several records emits one such Dispatch per record.
A record owned by a different guild is left untouched and reported in `errors`. An ID with no record queues emoji and sticker media removal for that ID and is reported with the `unknown` asset type, so the operation also clears orphaned media.
A record owned by a different guild is left untouched and reported in `errors`. An ID with no record queues emoji and sticker media removal for that ID and is reported with the `unknown` asset type, so the operation also removes stored media that has no emoji or sticker record.
Every entry in `processed` records its own Admin audit entry, with the numeric ID as the target. A purged emoji records `purge_guild_emoji_asset` with the target type `guild_emoji`, a purged sticker records `purge_guild_sticker_asset` with `guild_sticker`, and an `unknown` ID records `purge_asset` with `asset`. Entries in `errors` record nothing.
@@ -749,7 +749,7 @@ Every entry in `processed` records its own Admin audit entry, with the numeric I
Returns one page of the guild's own in-app audit log, without requiring guild membership or [VIEW_AUDIT_LOG](/http-api/permissions/). Requires `guild:audit_log:view`.
The page has the same shape and semantics as the public [List guild audit logs](/http-api/guild-audit-logs/#list-guild-audit-logs) operation, including its message deletion consolidation.
The response has the same fields, and its query parameters behave the same way, as in the public [List guild audit logs](/http-api/guild-audit-logs/#list-guild-audit-logs) operation, including its message deletion consolidation.
### Path parameters
@@ -864,7 +864,7 @@ A shutdown the main Gateway reports as failed returns 502 `BAD_GATEWAY`, an unan
### Side effects
The main Gateway stops the guild process. Every session subscribed to the guild receives a [Guild Delete](/gateway/events/#guild-delete) Dispatch with `unavailable` true and reconnects after one second, which starts the guild again from stored data. Stored guild data is untouched, and [Reload guild](#reload-guild) also starts a stopped guild.
The main Gateway stops the guild process. Every session subscribed to the guild receives a [Guild Delete](/gateway/events/#guild-delete) Dispatch with `unavailable` true. One second later the main Gateway reconnects each of those sessions to the guild, which starts the guild again from stored data. Stored guild data is untouched, and [Reload guild](#reload-guild) also starts a stopped guild.
One Admin audit entry is recorded with the action `shutdown_guild`, the target type `guild`, and the guild ID in both the target and the metadata.
@@ -18,7 +18,7 @@ Fluxer evaluates the credential in a fixed order.
2. A credential presented with the `Bot` scheme is refused with 401 `UNAUTHORIZED`.
3. Any access token issued to an OAuth2 application other than the built-in Admin application is refused with 403 `ACCESS_DENIED`.
4. An account holding neither `admin:authenticate` nor the wildcard `*` is refused with 403 `MISSING_PERMISSIONS`.
5. An account that clears both gates but satisfies none of the [ACLs](#acl-evaluation) the operation names is refused with 403 `MISSING_ACL`.
5. An account that passes every check above but satisfies none of the [ACLs](#acl-evaluation) the operation names is refused with 403 `MISSING_ACL`.
:::caution[A denial names no requirement]
The `MISSING_ACL` body is a fixed sentence with no ACL name and no requirement list, so a denial never says which requirement failed.
@@ -26,9 +26,9 @@ The `MISSING_ACL` body is a fixed sentence with no ACL name and no requirement l
No Admin operation declares an OAuth2 scope, an MFA requirement, or a sudo requirement, so none consumes the `X-Fluxer-Sudo-Mode-JWT` header. An accepted credential together with the required ACLs is the complete authorisation boundary.
An Admin API key authenticates as the account that created it, and Fluxer reads it only on a path below `/v1/admin`. A request presenting a key on any other path continues with no credential resolved. Fluxer authorises a key-authenticated request twice. The key's own ACL set must satisfy the operation, and unless the owning account holds `*` that account must itself hold one of the ACLs the operation names. Removing an ACL from either side narrows every existing key at once, with no rotation.
An Admin API key authenticates as the account that created it, and Fluxer reads it only on a path below `/v1/admin`. A request presenting a key on any other path is treated as a request with no credential. Fluxer authorises a key-authenticated request twice. The key's own ACL set must satisfy the operation, and unless the owning account holds `*` that account must itself hold one of the ACLs the operation names. When an ACL is removed from a key, or from the account that owns the key, the key no longer passes a check for that ACL on its next request. The key does not need to be rotated.
Initial setup is the single exception. While the instance reports its setup as unconfigured, a session credential reaches the [instance configuration](/admin-api/instance/) operations without holding any ACL. The session that switches setup to configured is granted `*` immediately. Once setup is configured those operations apply the boundary above.
Initial setup is the single exception. While `app_public.setup.configured` is false, a session credential reaches the [instance configuration](/admin-api/instance/) operations without holding any ACL. The session that switches setup to configured is granted `*` immediately. After `app_public.setup.configured` becomes true, those operations require an accepted credential and the ACLs they name.
Admin authorisation runs before request validation on every operation except [Queue bulk job](/admin-api/bulk-jobs/#queue-bulk-job), whose body is validated first because its ACL is selected from that body. An account lacking the required ACL is therefore refused with 403 even when its query string or body is also malformed.
@@ -60,12 +60,12 @@ A body with none of those fields resolves to no ACL at all and applies no change
- `add_guild_members` maps to `bulk:add:guild_members`.
- `schedule_user_deletion` maps to `bulk:delete:users`.
Fluxer bounds every grant separately. [Set user ACLs](/admin-api/users/#set-user-acls) and [Create Admin API key](/admin-api/api-keys/#create-admin-api-key) both refuse to write an ACL the acting Admin does not itself hold, with 403 `MISSING_ACL`. A wildcard holder is exempt. Set user ACLs also refuses the acting Admin's own account with 403 `ACCESS_DENIED`, and it resolves the target before the bound is evaluated, so an unknown ID fails first with 404 `UNKNOWN_USER`.
Fluxer checks each ACL an Admin grants against the ACLs that Admin holds. [Set user ACLs](/admin-api/users/#set-user-acls) and [Create Admin API key](/admin-api/api-keys/#create-admin-api-key) both refuse to write an ACL the acting Admin does not itself hold, with 403 `MISSING_ACL`. A wildcard holder is exempt. Set user ACLs also refuses the acting Admin's own account with 403 `ACCESS_DENIED`, and it looks up the target account before it checks the granted ACLs, so an unknown ID fails first with 404 `UNKNOWN_USER`.
[Set user ACLs](/admin-api/users/#set-user-acls), [Create Admin API key](/admin-api/api-keys/#create-admin-api-key), and [Update Admin API key](/admin-api/api-keys/#update-admin-api-key) each accept at most 111 ACLs and validate every entry against the registry, so a value outside it returns 400 `INVALID_FORM_BODY`.
:::caution[`*` satisfies every present and future ACL]
It lifts the per-key owner check and lifts the escalation bound on every grant its holder makes.
A key whose owning account holds `*` skips the owner check. An Admin holding `*` can grant any ACL.
:::
The [ACL registry](#acl-registry) below lists every value the instance recognises.
@@ -80,7 +80,7 @@ A request refused by authentication, ACL evaluation, rate limiting, or body vali
Fluxer can record more than one entry for one request. [Update guild](/admin-api/guilds/#update-guild) records one entry for each mapped field group it applies. A queued bulk job records one summary entry when it finishes, with the creating Admin and that request's audit reason. The `update_user_flags`, `update_guild_features`, and `schedule_user_deletion` tasks also record one ordinary entry for each account or guild the worker changes, with `audit_log_reason` null.
An entry stores an acting Admin, a target type, a target ID, an action name, an optional audit reason, and a string metadata map. Fluxer resolves every other member at read time.
An entry stores an acting Admin, a target type, a target ID, an action name, an optional audit reason, and a string metadata map. Fluxer fills every other field of the entry when the entry is read.
### Structure
@@ -239,7 +239,7 @@ An operation records one of the following actions. The field is a free-form stri
| ban_file_sha | A file hash was added to the attachment blocklist |
| ban_ip | An address was added to the IP blocklist |
| ban_ip_skipped_exempt | An IP blocklist entry was declined by the instance exemption list |
| ban_ip_skipped_cgnat | An IP blocklist entry was declined by the carrier-grade NAT blast-radius guard |
| ban_ip_skipped_cgnat | An IP blocklist entry was declined because IP intelligence reports the address as a mobile carrier network |
| ban_member | An account was banned from a guild |
| ban_phrase | A phrase was added to the message phrase blocklist |
| ban_profile_substring | A substring was added to the profile substring blocklist |
@@ -276,7 +276,7 @@ An operation records one of the following actions. The field is a free-form stri
| mark_suspicious_ip_skipped_invalid | The address could not be parsed, so no marker was written |
| mark_suspicious_ip_skipped_ipinfo_unavailable | IP intelligence was unavailable, so no marker was written |
| mark_suspicious_ip_skipped_trusted_commercial_privacy_provider | The address belongs to a trusted commercial privacy provider, so no marker was written |
| mark_suspicious_ip_skipped_high_blast_radius | The address failed the blast-radius guard, so no marker was written |
| mark_suspicious_ip_skipped_high_blast_radius | IP intelligence reports the address as a mobile, anycast, satellite, or education network, so no marker was written |
| purge_asset | A stored media asset was purged |
| purge_guild_emoji_asset | A guild emoji asset was purged |
| purge_guild_sticker_asset | A guild sticker asset was purged |
@@ -332,7 +332,7 @@ Send `X-Audit-Log-Reason` on operations that support an audit reason. Values are
Normalisation strips control and format characters and trims surrounding whitespace. An absent header, a blank value, and a value whose normalised length exceeds 512 characters all resolve to null. Fluxer never fails a request during this normalisation, so an over-long reason is dropped silently.
Fluxer writes the resolved value to `audit_log_reason` on every entry the request records. An operation that records no entry accepts the header and does nothing with it. An Admin resource page marks an operation with the audit reason capability only where an entry stores the value.
Fluxer writes the resolved value to `audit_log_reason` on every entry the request records. An operation that records no entry accepts the header and does nothing with it. An Admin resource page shows the Audit reason label in an operation's route header only where an entry stores the value.
## Errors and rate limits
@@ -352,7 +352,7 @@ An Admin failure uses the same envelope as the [HTTP API error response](/http-a
<sup>2</sup> Present on an `INVALID_FORM_BODY` response, and on any other failure that has field detail
Structured detail sits at the top level of the object. A client MUST treat a member it does not recognise as absent, and MUST NOT assume that two failures with the same `code` have the same extra members.
A failure with extra detail puts those extra members next to `code` and `message`, at the top level of the object. A client MUST treat a member it does not recognise as absent, and MUST NOT assume that two failures with the same `code` have the same extra members.
### Standard response statuses
@@ -376,7 +376,7 @@ Every Admin operation declares a route bucket, and each Admin resource page name
The global user bucket permits 50 requests per second, rising to 1,200 for an account that has the `HIGH_GLOBAL_RATE_LIMIT` flag. An account with the `RATE_LIMIT_BYPASS` flag is evaluated against no bucket at all. Instance configuration can change route and global limits as described in [rate limits](/topics/rate-limits/).
Fluxer classifies a successful Admin request from an ordinary account as a user request, and the response has no informational rate limit headers.
A successful Admin request from an account that is not a bot has no informational rate limit headers.
## List ACLs
@@ -491,7 +491,7 @@ The registry is returned in this order by [List ACLs](#list-acls). A value outsi
| system:heap_snapshot | Captures a heap snapshot of a running process |
| user:cancel:bulk_message_deletion | Cancels the bulk message deletion an account scheduled for itself |
| user:delete | Schedules or cancels account deletion |
| user:disable:suspicious | Applies the suspicious account disable operation |
| user:disable:suspicious | Disables a user for suspicious activity |
| user:list:dm_channels | Reads a user's direct message and group direct message channels |
| user:list:guilds | Reads a user's guild memberships |
| user:list:relationships | Reads a user's relationships |
@@ -572,7 +572,7 @@ Without an index, `q` is ignored, and a filtered page holds only the matches amo
| logs | array[[Admin audit entry](#admin-audit-entry-object) object] | The entries in this page |
| total<sup>1</sup> | integer | The number of entries the query matched |
<sup>1</sup> The index reports the full match count. The table fallback reports only the matches inside the rows it scanned
<sup>1</sup> The index reports the full match count. Without the search index, the operation reports only the matches among the first `limit` plus `offset` entries
### Response
@@ -92,7 +92,7 @@ Admission and dispatch tuning for the Gateway cluster.
Every field is present on read. An absent document or missing field uses the defaults above.
Admin requests use `rpc_request_timeout_ms`. The legacy stored name is covered in the [operator configuration reference](/operator/configuration/#stored-instance-policy).
Admin reads and writes name this field `rpc_request_timeout_ms`. The legacy stored name is covered in the [operator configuration reference](/operator/configuration/#stored-instance-policy).
## Voice noise suppression configuration object
@@ -104,20 +104,20 @@ The instance rollout of client-side noise suppression. [Experiments](/http-api/e
| --- | --- | --- |
| enabled | boolean | Whether the rollout runs at all (default false) |
| config_version | integer | Revision counter, raised by Fluxer and never accepted from a request |
| default_backend | string | Backend given to a drawn account (default `standard`) |
| default_backend | string | Backend given to an account the rollout selects (default `standard`) |
| enabled_backends | array[string] | Backends a client MAY run, up to 7 entries (default every backend) |
| allow_user_override | boolean | Whether an account's own choice replaces the assigned backend (default true) |
| rollout_basis_points | integer | Share of accounts drawn, in basis points (0-10000, default 0) |
| rollout_basis_points | integer | Share of accounts the rollout selects, in basis points (0-10000, default 0) |
| rollout_salt | string | Salt of the sampling hash (1-64 characters, default `voice-ns-v1`) |
| included_user_ids | array[snowflake] | Accounts always drawn, up to 1000 entries (default empty) |
| excluded_user_ids | array[snowflake] | Accounts never drawn, up to 1000 entries (default empty) |
| included_user_ids | array[snowflake] | Accounts the rollout always selects, up to 1000 entries (default empty) |
| excluded_user_ids | array[snowflake] | Accounts the rollout never selects, up to 1000 entries (default empty) |
| guild_overrides | array[[guild override](/http-api/experiments/#noise-suppression-guild-override-object) object] | Per-guild replacements, up to 200 entries (default empty) |
| stereo_enabled | boolean | Whether a drawn client publishes a stereo microphone track (default false) |
| stereo_enabled | boolean | Whether a client the rollout selects publishes a stereo microphone track (default false) |
| suppression_strength | integer | Suppression strength (0-100, default 80) |
Every field is present on read. An absent document or missing field uses the defaults above.
`excluded_user_ids` is applied before `included_user_ids`, so an account in both is never drawn. A `default_backend` or `guild_overrides` entry naming a backend outside `enabled_backends` is dropped from what a client is served, and the stored value is kept as written.
`excluded_user_ids` is applied before `included_user_ids`, so the rollout never selects an account in both. A `default_backend` or `guild_overrides` entry naming a backend outside `enabled_backends` is dropped from what a client is served, and the stored value is kept as written.
How often a client revalidates this rollout is not set here. It is set once for every experiment in the [experiment delivery configuration](#experiment-delivery-configuration-object) below.
@@ -130,11 +130,11 @@ How often a client polls [Get experiment assignments](/http-api/experiments/#get
| Field | Type | Description |
| --- | --- | --- |
| poll_interval_seconds | integer | Seconds between client revalidations (60-86400, default 300) |
| poll_jitter_percent | integer | Spread applied to each revalidation (0-50, default 15) |
| poll_jitter_percent | integer | Maximum random change to each revalidation interval, as a percentage in either direction (0-50, default 15) |
Every field is present on read. An absent document or missing field uses the defaults above.
Both fields are served to every account, whether or not any experiment targets that account, and neither one is versioned by `config_version`. A client that has never reached the experiments route holds the same values as built-in defaults, 300 seconds and 15 percent, so neither field reaches a client that cannot read the route.
Both fields are served to every account, whether or not any experiment targets that account, and neither one is versioned by `config_version`. A client that has never received a response from the experiments route uses built-in defaults of 300 seconds and 15 percent, which equal the defaults above. A client that cannot read the route never receives either field.
## Registration configuration object
@@ -169,7 +169,7 @@ A valid registration URL is accepted in every mode, including `closed`, and its
## Registration URL object
A registration URL is an invitation an Admin can issue while public registration is closed or gated. It has its own expiry, use budget, and approval requirement.
A registration URL is an invitation an Admin can issue while the registration mode is `closed` or `approval`. It has its own expiry, use budget, and approval requirement.
Fluxer accepts a URL while it has no revocation time, has not passed its expiry, and has a use count below `max_uses`. A URL failing any of those tests is still reported here.
@@ -269,7 +269,7 @@ Community, direct message, premium and gating policy for the whole deployment.
<sup>2</sup> Each key is the operator override when one is set, and otherwise the matching `services_available` value
<sup>3</sup> `gif` and `youtube` report whether an API key resolves from the stored configuration or the deployment configuration. `bluesky` reports the integration's resolved enablement
<sup>3</sup> `gif` and `youtube` report whether an API key resolves from the stored configuration or the deployment configuration. `bluesky` is the value of `integrations.bluesky.effective_enabled`
## Premium modes
@@ -280,7 +280,7 @@ Community, direct message, premium and gating policy for the whole deployment.
## Deferred phone gate object
A rule that requires phone verification in a configured window of hours after registration.
A rule for accounts whose phone verification requirement was deferred. When such an account joins a guild within `window_hours` of registration, and the guild has the `DISCOVERABLE` feature or more than `member_threshold` members, Fluxer refuses the join until the account verifies a phone.
### Structure
@@ -373,7 +373,7 @@ Attachment retention overrides the operator has set, and the values in force.
<sup>1</sup> The override keys are `enabled`, `min_size_mb`, `max_size_mb`, `max_eligible_size_mb`, `min_lifetime_days`, `max_lifetime_days`, `curve`, `renew_threshold_days`, and `renew_window_days`. Each is null when the deployment default applies, and `effective` reports the value in force. `enabled` is a boolean, `curve` is a number from 0 to 1, the `_mb` keys are positive numbers, and the `_days` keys are positive safe integers
Conflicting maximum size or lifetime overrides are cleared and resolved from deployment defaults. The effective size range must have a finite maximum above its minimum, and retention must produce a valid expiry date. Check the returned `effective` values after an update.
Fluxer clears `max_size_mb` when it is not above `min_size_mb`, clears `max_eligible_size_mb` when it is below `max_size_mb`, and clears `max_lifetime_days` when it is below `min_lifetime_days`. A cleared key uses the deployment default. The effective size range must have a finite maximum above its minimum, and retention must produce a valid expiry date. Check the returned `effective` values after an update.
## Branding asset kinds
@@ -472,7 +472,7 @@ One rule in that ordered set, with the filters that scope it and the limits it s
| limits | map[string, integer] | Non-negative value for each [limit key](/http-api/instance/#limit-keys) the rule sets |
| modifiedFields?<sup>1</sup> | array[string] | Limit keys whose value differs from the deployment default |
<sup>1</sup> Compared with the deployment default rule of the same identifier, or with the deployment's `default` rule for a custom identifier. A set key absent from that default counts as modified. A rule with no differing key omits the field. Explicit limit values remain unchanged
<sup>1</sup> Compared with the deployment default rule of the same identifier, or with the deployment's `default` rule for a custom identifier. A set key absent from that default counts as modified. A rule with no differing key omits the field. Computing this field changes no value in `limits`
## Limit key metadata object
@@ -543,7 +543,7 @@ The body has one optional object for each section. Fluxer leaves an absent secti
<sup>3</sup> A secret such as `klipy_api_key`, `api_key`, `hcaptcha_secret_key`, `turnstile_secret_key`, or the SMTP `password` is written when supplied and left alone when absent. `integrations.bluesky.keys` is the only way to write the Bluesky signing keys counted as `bluesky.key_count`. It takes up to 8 entries of `kid` (1-255 characters) and nullable `private_key` (up to 10000 characters), and replaces the stored key set outright
:::note[Single sign-on URL validation is conditional]
Fluxer skips URL validation while the merged configuration leaves single sign-on disabled. A configuration both enabled and enforced fails validation against `sso` with `SSO_MISCONFIGURED` unless it resolves an authorisation endpoint, a token endpoint, a client identifier, and a claims source. `issuer` stands in for the endpoints and the claims source.
Fluxer skips URL validation while the merged configuration leaves single sign-on disabled. A configuration both enabled and enforced fails validation against `sso` with `SSO_MISCONFIGURED` unless it resolves an authorisation endpoint, a token endpoint, a client identifier, and a claims source, which is `jwks_url` or `userinfo_url`. A set `issuer` meets the endpoint and claims source requirements, because Fluxer can discover them from the issuer.
:::
#### Instance policy update structure
@@ -558,11 +558,11 @@ Fluxer skips URL validation while the merged configuration leaves single sign-on
| services? | object | Nullable `gif_enabled`, `youtube_enabled`, and `bluesky_enabled` overrides |
| deferred_phone_gate?<sup>3</sup> | object | `enabled`, `window_hours`, and `member_threshold` |
<sup>1</sup> Setting `single_community_enabled` to true adopts the already designated guild when one still exists. When none is designated or the designated guild was deleted, it creates a community using `single_community_name` or the configured product name. A malformed designation or other datastore lookup error fails the operation instead of creating a replacement. On a deployment whose setup is already complete, enabling it while no guild is designated fails with 400 `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED`, as does enabling it when the acting Admin account cannot be resolved. Setting it to false only clears the flag and leaves the guild in place
<sup>1</sup> Setting `single_community_enabled` to true adopts the already designated guild when one still exists. When none is designated or the designated guild was deleted, it creates a community using `single_community_name` or the configured product name. When the stored guild ID is not a valid ID, or the guild lookup fails for a reason other than an unknown guild, the operation fails and Fluxer creates no community. On a deployment whose setup is already complete, enabling it while no guild is designated fails with 400 `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED`, as does enabling it when the acting Admin account cannot be resolved. Setting it to false only clears the flag and leaves the guild in place
<sup>2</sup> The setting can be changed only while `direct_messages_locked` is false, and a change attempted after the lock is set fails with 400 `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED` unless the same request sets `direct_messages_locked` to false. Re-enabling direct messages sets the lock again
<sup>3</sup> `window_hours` is a positive number up to 8760 and `member_threshold` is a positive integer up to 1000000. Each key is applied on its own
<sup>3</sup> `window_hours` is a positive number up to 8760 and `member_threshold` is a positive integer up to 1000000. An omitted key keeps its stored value
`direct_messages_locked` accepts only false, and a body that sets it to true fails with 400 `INVALID_FORM_BODY`.
@@ -579,7 +579,7 @@ The order is `gateway_rollout`, `voice_noise_suppression`, `experiment_delivery`
### Side effects
Gateway rollout changes apply across the cluster. Premium mode changes affect the limits in force without replacing the saved limit configuration. On self-hosted deployments, `everyone` hides premium-filtered rules. Switching back to `mirror` restores them unless an Admin has replaced the limit configuration in the meantime. Enabling single community mode creates the community when none is designated, with the acting Admin as owner.
Fluxer publishes a `gateway_rollout` change to the Gateway cluster. Premium mode changes affect the limits in force without replacing the saved limit configuration. On self-hosted deployments, `everyone` hides premium-filtered rules. Switching back to `mirror` restores them unless an Admin has replaced the limit configuration in the meantime. Enabling single community mode creates the community when none is designated, with the acting Admin as owner.
Initial setup completes on the first update that sets `app_public.setup.configured` to true from a session credential whose account holds neither `admin:authenticate` nor the wildcard. That update grants the account the wildcard Admin ACL and marks the deployment as bootstrapped.
@@ -661,10 +661,10 @@ No configuration is written and the supplied credentials are discarded when the
<RouteHeader method="POST" path="/v1/admin/instance/registration-urls" />
Issues a registration URL an Admin can hand out while public registration is closed or gated, and returns a [registration URL creation](#registration-url-creation-object) object. Requires `instance:config:update`.
Issues a registration URL an Admin can hand out while the registration mode is `closed` or `approval`, and returns a [registration URL creation](#registration-url-creation-object) object. Requires `instance:config:update`.
:::caution[The code is the identifier]
The returned `code` is the same value as `registration_url.id`, and every [Get instance configuration](#get-instance-configuration) response has that identifier. Treat `instance:config:view` as a registration capability on a gated deployment.
The returned `code` is the same value as `registration_url.id`, and every [Get instance configuration](#get-instance-configuration) response has that identifier. Treat `instance:config:view` as a registration capability on a deployment in `closed` or `approval` mode.
:::
### JSON body
@@ -758,7 +758,7 @@ Both decisions remove the pending registration, but the endpoint does not requir
Approval removes both the `registration_pending_approval` trait and the `registration_rejected` trait, and joins the account to the single community when that mode is enabled and a guild is designated. A join that fails is logged and does not fail the request. Rejection removes the `registration_pending_approval` trait and adds the `registration_rejected` trait, which blocks login and every later session creation. A session issued before the decision stays valid. The pending registration is removed either way.
Invalid pending-registration data prevents the decision. A later failure can still leave account changes applied, so check the account and pending list before retrying.
When the stored pending registration list fails validation, the request fails before Fluxer changes the account. A failure after Fluxer writes the account traits leaves those traits written and can leave the entry in the pending list. Check the account traits and the pending list before retrying.
One Admin audit entry with the action `approve_registration` or `reject_registration` targets the account, records the audit reason, and has no metadata.
@@ -793,7 +793,7 @@ The response reflects the configuration in force on the node that serves the req
Replaces the stored limit configuration with the supplied document and returns the resulting [limit configuration response](#limit-configuration-response-object) object. Requires `instance:limit_config:update`.
:::caution[An absent custom rule is removed]
A rule whose identifier matches a deployment default rule survives. Fluxer re-merges the write with the current defaults immediately.
A default rule that the body omits is restored from the deployment defaults. Fluxer re-merges the write with the current defaults immediately.
:::
:::note[The stored document can differ from the body]
@@ -11,7 +11,7 @@ These routes list recorded background jobs and request cancellation. Create jobs
Reads require `jobs:view` and cancellation requires `jobs:cancel`. Every operation on this page shares the `admin:jobs:view` bucket.
:::note[Job updates vary by task]
Not all background work appears here, and recorded status, progress and attempts may not reflect every execution. Use the [archive routes](/admin-api/archives/) to track archive progress and failures.
Mention processing, link previews, and every scheduled task except `syncDisposableEmailDomains`, `syncUrlBlocklists` and `syncFileShaBlocklists` run with no job record and never appear here. A job also runs with no record when Fluxer fails to write that record. When a later write of status, progress or attempts fails, Fluxer logs the failure and the job continues, so the stored values can be behind the real run. Use the [archive routes](/admin-api/archives/) to track archive progress and failures.
:::
## Admin job object
@@ -37,14 +37,14 @@ One object describes one recorded job. These routes can only change `cancel_requ
| jet_stream_lane | ?string | The assigned [processing lane](#processing-lanes), or null |
| jet_stream_seq | ?string | The recorded queue sequence as a decimal string, or null |
| attempts | integer | The recorded retry count, not a total of all executions |
| max_attempts | integer | The recorded attempt limit. Actual retry behaviour may differ |
| max_attempts | integer | Attempt limit recorded at queue time. Retries stop at the lane's delivery limit, which can differ from this value |
| run_at | ?ISO8601 timestamp | The earliest permitted run time, or null for an immediate job |
| cancel_requested | boolean | Whether cancellation has been requested |
| context_link | ?string | An Admin console path related to the job, or null |
| payload | ?string | The JSON-encoded job input, or null |
| result | ?string | The JSON-encoded result, or null |
Progress fields may remain null. The payload can contain identifiers and message content, including in list responses.
Progress fields stay null until the task reports progress. A task can report progress with no total or no message, which sets `progress_total` or `progress_message` to null. The payload can contain identifiers and message content, including in list responses.
### Example
@@ -103,7 +103,7 @@ Pass all three fields from `next_cursor` back to [List jobs](#list-jobs) using t
| queued | The job was recorded and no later status has been recorded |
| running | The job was recorded as started |
| succeeded | The job was recorded as completed successfully |
| cancelled | The task honoured a cancellation request |
| cancelled | The task read the cancellation request and stopped |
| deadletter | The job was recorded as failed, including a failure to queue it |
`queued` and `running` accept cancellation requests. The other statuses are terminal. A cancellation request does not guarantee that the task will stop.
@@ -170,7 +170,7 @@ Supply all three cursor parameters together or omit all three. An incomplete or
Returns recorded active jobs as [Admin job](#admin-job-object) objects. Requires `jobs:view`.
This operation has no filters or pagination. Recorded state can lag execution.
This operation has no filters or pagination. The stored status and progress can be behind the work the task has already done.
### Response body
@@ -247,7 +247,7 @@ Returns false for a missing or terminal job. Repeating a request for a queued or
### Side effects
Sets `cancel_requested` to true for a queued or running job. Only tasks that support cancellation will stop, and completed work is not undone. A status of `cancelled` confirms that the task honoured the request.
Sets `cancel_requested` to true for a queued or running job. `sendSystemDm`, `syncDisposableEmailDomains`, `bulkBanFileShas`, `bulkDeleteMessagesForUsers`, and the tasks queued by [Bulk jobs](/admin-api/bulk-jobs/) check for the request while they run and stop at the next check. Other tasks run to the end. Completed work is not undone. A status of `cancelled` means the task stopped because of the request.
### Rate limit
@@ -285,7 +285,7 @@ One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded with t
Searches the message index of one channel, or resolves one message in that channel by message ID or by attachment identity. Requires `message:lookup`.
The modes return a search body or a lookup body, and the query decides which one arrives.
A request with `message_id` or `attachment_id` returns the lookup response body. Any other request returns the search response body.
### Query parameters
@@ -433,7 +433,7 @@ Fluxer resolves the attachment from the live message when that message still exi
The attachment is `submitting` while the request runs, then `submitted` with its report ID on success. A failed submission attempts to retract the opened report, marks the attachment `failed` with the raw reason, and returns `NCMEC_SUBMISSION_FAILED`. A failed retraction is logged without changing that recorded state.
When the resolved attachment has an author who has not already been enforced against, Fluxer sets the account's deleted and disabled flags, clears any temporary ban, and records the deletion reason for child sexual content. It then schedules deletion 60 days ahead, deletes every authentication session, propagates the resulting user update, and triggers a user archive for that account.
When the resolved attachment has an author whose account an earlier NCMEC submission has not already disabled, Fluxer sets the account's deleted and disabled flags, clears any temporary ban, and records the deletion reason for child sexual content. It then schedules deletion 60 days ahead, deletes every authentication session, sends [User Update](/gateway/events/#user-update) to that account, and triggers a user archive for that account. When the account's public user fields change, Fluxer also sends [Guild Member Update](/gateway/events/#guild-member-update) to each guild the account is in.
Message content is deleted only after that archive completes. The archive is re-checked every 15 seconds by default, for at most 240 attempts. Once the archive is complete the reported message is deleted, its attachments are purged, and [Message Delete](/gateway/events/#message-delete) is sent.
@@ -285,7 +285,7 @@ An instance with no search backend returns 403 `FEATURE_TEMPORARILY_DISABLED` on
<sup>1</sup> Matched against `category`, `additional_info`, `reported_guild_name`, and `reported_channel_name` only. Omitting it matches every report satisfying the remaining filters
<sup>2</sup> The route accepts it, and it alone selects the search branch. It narrows nothing
<sup>2</sup> The route accepts it, and supplying it with no other filter still selects the search branch. The search ignores its value and returns matching reports from every channel
<sup>3</sup> `created_at` orders by the time encoded in the report ID, and `reported_at` by the submission time stored on the report
@@ -84,7 +84,7 @@ Progress for one queued rebuild. The object shape is selected by `status`.
<sup>1</sup> `total` is the same value as `indexed` while the rebuild runs and becomes the real total on completion. The `discovery` rebuild reports the approved listing count from its first batch onwards
<sup>2</sup> The unit is the document the handler writes. A `channel_messages` rebuild counts the channels it queued
<sup>2</sup> Each document the rebuild writes counts as one. A `channel_messages` rebuild counts the channels it queued
<sup>3</sup> Rewritten on every progress report, so its value moves forward while the rebuild runs
@@ -120,8 +120,8 @@ Every field is optional. Fluxer reads an absent or empty body as an empty object
| Field | Type | Description |
| --- | --- | --- |
| guild_id?<sup>1</sup> | snowflake | The ID of the guild whose copy of the index is rebuilt |
| user_id?<sup>2</sup> | snowflake | The ID of the user whose copy of the index is rebuilt |
| guild_id?<sup>1</sup> | snowflake | The ID of the guild whose documents in the index are rebuilt |
| user_id?<sup>2</sup> | snowflake | The ID of the user whose documents in the index are rebuilt |
<sup>1</sup> Required by `channel_messages` and `guild_members`. Every other index name ignores it
@@ -168,7 +168,7 @@ Returns the [search index refresh progress](#search-index-refresh-progress-objec
| --- | --- | --- |
| job_id<sup>1</sup> | string | The identifier returned by [Refresh search index](#refresh-search-index) |
<sup>1</sup> The value is bounded at 1 to 128 characters after normalisation and need not be a snowflake
<sup>1</sup> The value is bounded at 1 to 128 characters after Fluxer removes every form feed (U+000C) and right-to-left override (U+202E) character and trims whitespace from both ends. It need not be a snowflake
### Response
@@ -52,7 +52,7 @@ Filter [List jobs](/admin-api/jobs/#list-jobs) by the `sendSystemDm` task type t
For each recipient the job opens the direct message channel between the system account and that recipient, then sends the message. [Channel Create](/gateway/events/#channel-create) reaches a recipient only when the channel is newly created or was closed on that recipient's side. A recipient who already has the channel open observes only [Message Create](/gateway/events/#message-create).
Recipients need not have interacted with the system account before. Broadcasts bypass normal direct message spam restrictions.
Recipients need not have interacted with the system account before. Fluxer skips the direct message permission checks for the system account. A block, no friendship, no mutual guild, and the recipient's direct message privacy settings do not stop delivery.
Cancellation stops remaining deliveries and leaves sent messages in place. The [job](/admin-api/jobs/#admin-job-object) then reports `cancelled`.
@@ -13,14 +13,14 @@ Each mutable field group has its own route, its own [ACL](/admin-api/#acl-regist
A read records no audit entry and reads no audit reason. [List user sessions](#list-user-sessions) and [List user WebAuthn credentials](#list-user-webauthn-credentials) are the exceptions. `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]
The read still succeeds, so a null field is no evidence that the account has none.
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 the matching ACL, Fluxer redacts field groups and still returns every key, so the object shape is identical for every caller.
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
@@ -294,7 +294,7 @@ One entry for each authentication session of an account. Terminated sessions rem
## Admin resolved user object
The account summary the Admin direct message channel and relationship objects embed. It has `avatar` on top of the [Admin user summary](/admin-api/#admin-user-summary-object) that audit entries embed.
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
@@ -395,14 +395,14 @@ One entry for each direct message or group direct message channel the account ha
## Admin user change log object
One recorded change to an identity or contact field. The account holder's own changes appear here, and so do [Change user username](#change-user-username) and [Change user email](#change-user-email).
One recorded change to the account's email address, phone verification state, or 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_id<sup>1</sup> | string | The ID of the change entry as an unsigned 64-bit decimal string |
| field | string | The name of the identity or contact field that changed |
| field | string | The field that changed, one of `email`, `has_verified_phone`, or `fluxer_tag` |
| old_value<sup>2</sup> | ?string | The value before the change, or null when the field was unset |
| new_value<sup>2</sup> | ?string | The value after the change, or null when the field was cleared |
| reason<sup>3</sup> | ?string | The recorded reason for the change, or null when unrecorded |
@@ -571,7 +571,7 @@ An account with a pending or completed deletion is still returned, with its life
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 every self-hosted instance, and on any other instance only when the `feature_custom_discriminator` limit admits it.
A target account may hold a custom discriminator on every self-hosted instance, and on any other instance only when the `feature_custom_discriminator` limit resolves to a value above zero for that account.
### Path parameters
@@ -694,11 +694,11 @@ The operation accepts no request body and never clears verification, so the one
| 200<sup>1</sup> | response body | Email address was marked verified |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
<sup>1</sup> An account that has no stored email address is still accepted, and the verified marker is written against the absent address
<sup>1</sup> 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. Every email-clearable [suspicious activity flag](#suspicious-activity-flags) is cleared from the account in the same write.
`email_verified` becomes true and `email_bounced` becomes false. The same write clears each of these [suspicious activity flag](#suspicious-activity-flags) bits: `REQUIRE_VERIFIED_EMAIL`, `REQUIRE_REVERIFIED_EMAIL`, `REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE`, `REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE`, `REQUIRE_VERIFIED_EMAIL_OR_REVERIFIED_PHONE`, and `REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE`.
[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.
@@ -713,7 +713,7 @@ The operation accepts no request body and never clears verification, so the one
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]
An already verified account with no email reverification [suspicious activity flag](#suspicious-activity-flags) returns 204 without storing a token or sending anything.
When the account's email is already verified and none of the [suspicious activity flag](#suspicious-activity-flags) bits `REQUIRE_REVERIFIED_EMAIL`, `REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE`, `REQUIRE_VERIFIED_EMAIL_OR_REVERIFIED_PHONE`, or `REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE` is set, Fluxer returns 204 without storing a token or sending anything.
:::
:::note[Delivery is dropped silently]
@@ -775,7 +775,7 @@ Delivery is silently dropped when the instance email transport is disabled and w
<sup>1</sup> 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 per-address control and does not refuse a bot account.
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
@@ -869,7 +869,7 @@ Clearing is the only profile change on this resource. No route sets a biography,
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 a partial user field changed, which covers `avatar` and `global_name`.
[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.
@@ -915,7 +915,7 @@ Marks the account as a bot or as an ordinary account, and returns the resulting
### 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 a partial user field changed.
[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.
@@ -1108,7 +1108,7 @@ Additions are applied before removals, so a flag named in both arrays ends up cl
### Side effects
[User Update](/gateway/events/#user-update) is emitted to the account's own sessions. A change to publicly visible user fields also sends [Guild Member Update](/gateway/events/#guild-member-update) to the account's guilds.
[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.
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.
@@ -1168,7 +1168,7 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje
Sets whether the account is treated as having completed phone verification, and returns the resulting account. Requires `user:update:phone`.
The user-facing phone verification marker is otherwise irreversible, and this is the one operation that clears it.
This operation is the one way to set `has_verified_phone` back to false once it is true.
### Path parameters
@@ -1242,7 +1242,7 @@ The operation sets verification requirements without disabling the account. [Dis
| 200 | response body | Flags were replaced |
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
The stored deferral bit `1 << 16` is preserved only when it was already set and the submitted value has the same non-zero deferrable phone bits as the stored value. Any other submitted value clears the deferral, so the requirement takes effect immediately.
The stored deferral bit `1 << 16` is preserved only when it was already set and the submitted value sets the same `REQUIRE_VERIFIED_PHONE` and `REQUIRE_REVERIFIED_PHONE` bits as the stored value, with at least one of them set. Any other submitted value clears the deferral bit, so any phone requirement in the submitted value stops waiting for a guild join.
[Bulk jobs](/admin-api/bulk-jobs/) applies the same change to up to 1,000 accounts as a queued `update_suspicious_activity_flags` task.
@@ -1355,7 +1355,7 @@ Every authentication session of the account is deleted. [Unban user](#unban-user
`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 state and `temp_banned_until` in the same request, so a temporary ban ends without an Admin operation.
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.
@@ -1454,19 +1454,19 @@ The `X-Audit-Log-Reason` value is also stored on the account as the private dele
| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist |
| 409 | [error response](/admin-api/#error-response) | `CONFLICT`, because erasure has started or the deletion state changed during the request |
The permission also allows scheduling deletion of the acting Admin or an account with broader permissions.
`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 are deleted. 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. A failure in that path is logged, and the deletion still applies.
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.
Reasons other than `USER_REQUESTED` also trigger email and IP blocking and resolution of pending reports. These enforcement steps are best-effort and can fail without cancelling the deletion schedule.
For every reason other than `USER_REQUESTED`, Fluxer also blocks the account's email address. It marks the account's last active IP address, its authorised IP addresses, and the IP addresses of its active and terminated sessions as suspicious, and it 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` and `reason_code`. The identifier bans record their own blocklist entries, and a non-zero report pass records a second entry with action `auto_resolve_reports_on_deletion` and a metadata key `resolved_count`.
[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` and `reason_code`. 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
@@ -1705,7 +1705,7 @@ Removes every relationship of the account in one [relationship category](#relati
| --- | --- | --- |
| removed_count<sup>1</sup> | integer | The number of relationships that were removed |
<sup>1</sup> Counted from the target account's perspective, so a mirrored friendship contributes one
<sup>1</sup> Counted from the target account's perspective, so a friendship, which both accounts store, contributes one
### Response
@@ -1722,7 +1722,7 @@ Removals run one at a time, so a failure partway through leaves the earlier remo
Friendships and friend requests are removed for both accounts. Blocks are removed only for the target account.
Both parties of a mirrored removal 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.
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`.
@@ -1764,7 +1764,7 @@ The operation addresses one category, so an account that is both a former friend
A friendship or friend request is removed for both accounts. A block is removed only for the owning account.
Both parties of a mirrored removal receive [Relationship Remove](/gateway/events/#relationship-remove) naming the other account. A `blocked` removal dispatches only to 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`.
@@ -1848,7 +1848,7 @@ There is no operation that revokes one session. The account's Admin API keys, bo
| terminated_count | integer | The number of sessions that were terminated |
:::caution[A terminated session cannot be restored]
The tombstone stays visible through [List user sessions](#list-user-sessions), but the session itself is gone.
[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
@@ -1922,7 +1922,7 @@ The passkey or security key stops authenticating immediately and cannot be resto
### Side effects
The credential record is deleted. When it was the account's final WebAuthn credential, 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 resyncs the authenticator mirror of every bot the account owns.
The credential record is deleted. When it was the account's final WebAuthn credential, 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.
[WebAuthn Credentials Update](/gateway/events/#webauthn-credentials-update) is emitted to the target account with its remaining credentials.
@@ -1957,7 +1957,7 @@ Clearing the authenticator type set removes `WEBAUTHN` from the advertised authe
### Side effects
The account's TOTP secret, authenticator type set, and every multi-factor backup code are deleted. Fluxer resyncs the authenticator mirror of every bot the account owns. Sessions and credentials are not revoked.
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.
@@ -11,7 +11,7 @@ A voice region is a named group of media machines, and a voice server is one mac
No operation here addresses a live session, a participant, or a track. [Get voice state counts](/admin-api/gateway/#get-voice-state-counts) reports occupancy, and [List RTC regions](/http-api/channels/#list-rtc-regions) is the caller-facing view of the same regions.
:::note[Changes apply to new placements]
Changes take effect across the instance after a short delay. No write moves or disconnects an existing session.
Every write publishes a change notification. Each API node reloads the voice configuration when it receives that notification, a short time after the write. Placements after that reload use the new configuration. No write moves or disconnects an existing session.
:::
:::caution[A failed response can follow a successful change]
@@ -64,7 +64,7 @@ The operator supplies `id` on creation. Channels reference it through `rtc_regio
| updated_at | ?ISO8601 timestamp | Time the region record last changed, or null when the stored row has none |
| servers?<sup>4</sup> | array[[Admin voice server](#admin-voice-server-object) object] | The servers registered in the region |
<sup>1</sup> The flag is not exclusive, and setting it on a second region does not clear it on the first. Each node then takes the first flagged region its reload lists as the default, or the first region its reload lists when no region is flagged
<sup>1</sup> The flag is not exclusive, and setting it on a second region does not clear it on the first. Each API node then uses the first flagged region in the region list it loaded as the default. When no region is flagged, it uses the first region in that list
<sup>2</sup> Setting any of these three makes the region unusable for a private call
@@ -93,7 +93,7 @@ The operator supplies `id` on creation. Channels reference it through `rtc_regio
## Admin voice server object
A server record names one LiveKit deployment, the API key pair the instance authenticates to it with, and its own copy of the eligibility fields. A server is reachable for placement only when its region is also reachable.
A server record names one LiveKit deployment, the API key pair the instance authenticates to it with, and its own set of eligibility fields. A server is available for placement only when its region also admits the placement.
The `region_id` and `server_id` pair addresses one server, and a server belongs to exactly one region. The same server identifier can exist in two regions. Moving a server between regions means deleting it and recreating it. Both identifiers are operator-chosen strings of 1 to 64 characters, and no operation renames either one.
@@ -123,7 +123,7 @@ A server can also have a soft connection limit, described under [soft connection
<sup>2</sup> The two coordinates are set and cleared together, and a server with only one of them cannot be stored
<sup>3</sup> Fluxer skips an inactive server when it resolves a new placement. Server-side moderation of a session already on it keeps working
<sup>3</sup> Fluxer skips an inactive server when it resolves a new placement. Fluxer can still mute, deafen, change the permissions of, or disconnect a participant already on it
<sup>4</sup> Duplicate entries collapse and the returned order is not the submitted order
@@ -256,7 +256,7 @@ Stores a region and returns it. Requires `voice:region:create`.
| allowed_guild_ids? | array[snowflake] | The guilds admitted without checking the other guild gates (max 1000, default empty) |
| allowed_user_ids? | array[snowflake] | The accounts allowed to use the region at all (max 1000, default empty) |
<sup>1</sup> The identifier is not checked for collision. Reusing the identifier of an existing region overwrites that record in full, resets its creation time to now, and replaces every one of its collections
<sup>1</sup> The identifier is not checked for collision. Reusing the identifier of an existing region overwrites that record in full, resets its creation time to now, and replaces its `required_guild_features`, `allowed_guild_ids`, and `allowed_user_ids` arrays
<sup>2</sup> Each item is 1 to 64 characters. A value that is not a real guild feature is stored as supplied and then matches no guild
@@ -370,7 +370,7 @@ There is no confirmation step or recovery operation. Save the server configurati
### Side effects
Operations that need a deleted server fail once the change takes effect. Existing sessions are not disconnected by the deletion itself.
After each API node reloads its topology, Fluxer can no longer mute, deafen, change the permissions of, or remove a participant on a deleted server. Existing sessions are not disconnected by the deletion itself.
One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `delete_voice_region` and the target type `voice_region` records the region identifier and its name in metadata. No `delete_voice_server` entry is written for the servers deleted with the region.
@@ -596,7 +596,7 @@ Deletes a voice server. Requires `voice:server:delete`.
### Side effects
Each node reloads its topology after the removal, and server-side moderation of a session still on that server then fails. Sessions already placed on the server are not disconnected by the deletion itself. The stored credentials are removed with the record and are not recoverable.
Each node reloads its topology after the removal, and Fluxer can then no longer mute, deafen, change the permissions of, or remove a participant still on that server. Sessions already placed on the server are not disconnected by the deletion itself. The stored credentials are removed with the record and are not recoverable.
One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `delete_voice_server` and the target type `voice_server` records the region identifier, the server identifier, and the endpoint the server held in metadata.
@@ -53,7 +53,7 @@ A [sudo mode](#sudo-mode) proof supplements the token through the separate `X-Fl
## Bot tokens
The Gateway accepts a bot token in [Identify](/gateway/commands/#identify). The HTTP operations [`GET /v1/gateway/bot`](/http-api/gateway/#get-gateway-information) and [`GET /v1/applications/@me`](/http-api/applications/#get-bot-application) accept case-insensitive scheme prefixes. `GET /v1/applications/@me` specifically requires `Bot` and returns 401 `INVALID_TOKEN` for anything else. The [Gateway authentication](#gateway-authentication) section covers the other route.
The Gateway accepts a bot token in [Identify](/gateway/commands/#identify). The HTTP operations [`GET /v1/gateway/bot`](/http-api/gateway/#get-gateway-information) and [`GET /v1/applications/@me`](/http-api/applications/#get-bot-application) accept case-insensitive scheme prefixes. `GET /v1/applications/@me` specifically requires `Bot` and returns 401 `INVALID_TOKEN` for anything else. The [Gateway authentication](#gateway-authentication) section covers `GET /v1/gateway/bot`.
A bot cannot use an operation restricted to ordinary user accounts, and such an operation returns 403 `ACCESS_DENIED`. An operation in [Authentication](/http-api/authentication/) that resolves an account from its request body or token, such as login, password recovery, email verification, email revert, and IP authorisation, returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED` when that account is a bot.
@@ -63,7 +63,7 @@ Every OAuth2 access token belongs to an account. Supported grants are authorisat
Only operations that explicitly support OAuth2 accept access tokens. A user operation without that support returns 403 `ACCESS_DENIED` for a valid access token.
Scopes apply only to OAuth2 access tokens, not to user session credentials accepted by the same operation. A bearer-only operation rejects a session token, bot token, or Admin API key with 401 `UNAUTHORIZED`.
An operation that accepts both a user session token and an OAuth2 access token checks the scope only on the access token. A bearer-only operation rejects a session token, bot token, or Admin API key with 401 `UNAUTHORIZED`.
A missing scope returns 403 `MISSING_OAUTH_SCOPE`. Each operation requires its named scope exactly. The [OAuth2 HTTP API](/http-api/oauth2/) defines the supported [scopes](/http-api/oauth2/#oauth2-scopes), grants, refresh, revocation, and introspection.
@@ -98,7 +98,7 @@ A credential can affect even an unauthenticated operation. It selects account-ba
A protected operation returns 401 `UNAUTHORIZED` for a missing, malformed, unknown, expired, or revoked credential. Bot tokens on Admin operations and non-bearer credentials on bearer-only operations also return 401.
A valid identity denied by the operation returns 403 `ACCESS_DENIED`, subject to the credential-specific exceptions above.
A valid identity denied by the operation returns 403 `ACCESS_DENIED`. An [Authentication](/http-api/authentication/) operation that resolves a bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`, as [Bot tokens](#bot-tokens) describes.
Scope and Admin permission failures use the specific codes above. A 401 has no `WWW-Authenticate` header, so clients must inspect `code`.
@@ -123,7 +123,7 @@ Enforcement applies at those operations only, and does not gate password change
## Account state gates
The ordinary login requirement rejects an account that has effective suspicious activity flags with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. A requirement disappears from the response as soon as it is met.
An ordinary authenticated operation rejects an account that has an unmet suspicious activity requirement with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. Each set flag in the response is one requirement the account has not met. A flag no longer appears in the response once the account meets that requirement.
### Account suspicious activity body
@@ -131,7 +131,7 @@ The ordinary login requirement rejects an account that has effective suspicious
| --- | --- | --- |
| data | object | An object whose `suspicious_activity_flags` member is the integer [suspicious activity flag](/admin-api/users/#suspicious-activity-flags) bitfield still outstanding |
A route that explicitly admits restricted accounts still accepts the credential. These stay reachable while a requirement is outstanding:
A route that explicitly admits an account with an unmet suspicious activity requirement still accepts its credential. These stay reachable while a requirement is outstanding:
- [Get current user](/http-api/users/current-user/#get-current-user) and [Modify current user](/http-api/users/current-user/#modify-current-user).
- [Get current user settings](/http-api/users/settings/#get-current-user-settings).
@@ -156,7 +156,7 @@ Sudo mode is a short-lived proof that the account holder recently re-verified a
A sudo proof lasts five minutes. Present it in the `X-Fluxer-Sudo-Mode-JWT` request header. An invalid, expired, or account-mismatched token produces the same response as a missing one.
Fluxer issues a token only for an account holding a multi-factor authenticator, so a password-only account re-verifies for each operation that requires sudo mode. [Create WebAuthn registration options](/http-api/users/mfa/#create-webauthn-registration-options) and [Disable current account](/http-api/users/current-user/#disable-current-account) issue no token and return no header even for a multi-factor account. A bot account satisfies sudo mode immediately. So does an account that has neither a password nor a multi-factor authenticator.
Fluxer issues a token only for an account holding a multi-factor authenticator, so a password-only account re-verifies for each operation that requires sudo mode. [Create WebAuthn registration options](/http-api/users/mfa/#create-webauthn-registration-options) and [Disable current account](/http-api/users/current-user/#disable-current-account) issue no sudo token and return no `X-Fluxer-Sudo-Mode-JWT` response header, even for a multi-factor account. A bot account satisfies sudo mode immediately. So does an account that has neither a password nor a multi-factor authenticator.
:::note[A sudo proof covers every account session]
Revoking the session that obtained a proof leaves that proof valid until it expires.
@@ -13,7 +13,7 @@ Except for [Heartbeat](#heartbeat), [Identify](#identify), and [Resume](#resume)
| Opcode | Command | Result |
| --- | --- | --- |
| 1 | Heartbeat | Opcode `11` Heartbeat ACK |
| 2 | Identify | [Ready](/gateway/events/#ready), a close frame, or silence when the payload is held or discarded |
| 2 | Identify | [Ready](/gateway/events/#ready), a close frame, or no response when Fluxer discards a rate-limited Identify or holds it to retry the session start |
| 3 | Presence Update | No direct response |
| 4 | Voice State Update | [Voice State Update](/gateway/events/#voice-state-update) and [Voice Server Update](/gateway/events/#voice-server-update) when state changes |
| 6 | Resume | Replayed Dispatches followed by [Resumed](/gateway/events/#resumed), Invalid Session, or a close frame |
@@ -77,7 +77,7 @@ Opcode `2` authenticates and creates a new session.
<sup>2</sup> Names are upper-cased and deduplicated. See [Event filtering](/gateway/event-filtering/) for the exact suppression rule
<sup>3</sup> The guild delivers active traffic without a [Lazy Request](#lazy-request) and sends no initial [Guild Sync](/gateway/events/#guild-sync). A value that is not a canonical decimal Snowflake string is ignored without failing Identify
<sup>3</sup> The session is active in that guild without a [Lazy Request](#lazy-request), as [Event filtering](/gateway/event-filtering/#active-and-passive-guilds) describes, and receives no initial [Guild Sync](/gateway/events/#guild-sync). A value that is not a canonical decimal Snowflake string is ignored without failing Identify
<sup>4</sup> `shard_count` is an integer from 1 through 16,384, and `shard_id` is an integer that is at least 0 and below `shard_count`
@@ -203,7 +203,7 @@ Opcode `6` restores a retained session.
All fields are required. A missing field, a non-string `token` or `session_id`, or a `seq` that is not an integer closes with `4002` and reason `Invalid resume payload`.
An unknown or expired session produces Opcode `9` with `d: false` and leaves the socket unauthenticated. A `seq` below the [replay floor](/gateway/limits-and-rate-limits/#replay-and-backpressure), the highest sequence already dropped from the buffer, produces the same frame. A token that does not own the session closes with `4004` and reason `Invalid token`. A `seq` above the session's current sequence, or below the sequence it has already acknowledged, closes with `4007` and reason `Invalid sequence`. A negative `seq` closes with `4000` and reason `Session unavailable`, and so does a session that cannot be reached. None of those closes destroys a separately retained session.
An unknown or expired session produces Opcode `9` with `d: false` and leaves the socket unauthenticated. A `seq` below the [replay floor](/gateway/limits-and-rate-limits/#replay-and-backpressure), the highest sequence already dropped from the buffer, produces the same frame. A token that does not own the session closes with `4004` and reason `Invalid token`. A `seq` above the session's current sequence, or below the sequence it has already acknowledged, closes with `4007` and reason `Invalid sequence`. A negative `seq` closes with `4000` and reason `Session unavailable`, and so does a session that cannot be reached. None of those closes ends the retained session that `session_id` names.
A successful Resume replays every retained Dispatch strictly above `seq` in order and finishes with [Resumed](/gateway/events/#resumed). It also replaces the session's socket, and the displaced socket receives Opcode `7` followed by a close.
@@ -294,7 +294,7 @@ Every field is optional. A non-null `guild_id` or `channel_id` is a canonical de
`latitude` and `longitude` accept a number or a string here, and Fluxer coerces both to a string.
The command has no `session_id` field. The current Gateway session is the membership identity.
The command has no `session_id` field. Fluxer identifies the voice membership by the Gateway session that sends the command.
Joining or replacing a grant produces [Voice Server Update](/gateway/events/#voice-server-update) with the token and endpoint for the media connection, and [Voice State Update](/gateway/events/#voice-state-update) for every session that can see the channel.
@@ -401,7 +401,7 @@ Opcode `14` sets the per-guild subscriptions that decide member list, typing, an
| member_list_channels?<sup>2</sup> | map[snowflake, array[array[integer]]] | The member list windows to subscribe to, keyed by channel ID |
| members? | array[snowflake] | The explicit member IDs to subscribe to, at most 1,000 |
<sup>1</sup> Both are Booleans when present. Any other value drops the rest of the command silently, without a close and without a result
<sup>1</sup> Both are Booleans when present. Any other value stops Fluxer from applying the remaining options for that guild and every guild it has not yet processed, without a close and without a result
<sup>2</sup> Subscriptions may be combined over a 100 ms window. Ranges sent during that window are merged for the same channel, and an empty range list clears its pending ranges
@@ -421,7 +421,7 @@ Subscribing a channel to at least one range drops the session's other member lis
## Request Guild Counts
Opcode `15` requests current count records.
Opcode `15` requests the current member and online counts for guilds.
| Field | Type | Description |
| --- | --- | --- |
@@ -444,7 +444,7 @@ Results arrive in one [Guild Counts Update](/gateway/events/#guild-counts-update
## Request Channel Member Counts
Opcode `16` requests count records for channels in one guild.
Opcode `16` requests the member and online counts for channels in one guild.
| Field | Type | Description |
| --- | --- | --- |
@@ -16,10 +16,10 @@ Fluxer evaluates a guild-scoped Dispatch against these gates in order.
| --- | --- | --- |
| 1 | Guild availability | Whether the guild dispatches anything except [Guild Update](/gateway/events/#guild-update) |
| 2 | Permission and visibility | Which sessions may see the event at all |
| 3 | Guild subscription state | Whether a passive session in a large guild falls inside the fixed subset that still receives it |
| 3 | Guild subscription state | Whether a passive session in a guild with more than 250 members still receives it, as [Active and passive guilds](#active-and-passive-guilds) lists |
| 4 | Session-level filters | Whether the shard filter, and then the `ignored_events` list, drops it inside the session after the guild has already chosen the recipients |
A guild with the `UNAVAILABLE_FOR_EVERYONE` or `UNAVAILABLE_FOR_EVERYONE_BUT_STAFF` feature fails gate 1. `UNAVAILABLE_FOR_EVERYONE_BUT_STAFF` decides whether the guild hands a session its full state or an `unavailable` stub when the session connects. Gate 1 has no staff exemption, so a staff session also receives nothing but Guild Update while the feature is set.
A guild with the `UNAVAILABLE_FOR_EVERYONE` or `UNAVAILABLE_FOR_EVERYONE_BUT_STAFF` feature fails gate 1. `UNAVAILABLE_FOR_EVERYONE_BUT_STAFF` decides what a session receives when it connects. A staff session receives the guild's full state, and every other session receives an `unavailable` stub. Gate 1 has no staff exemption, so a staff session also receives nothing but Guild Update while the feature is set.
An account-scoped Dispatch skips gates 1 through 3 and is subject only to gate 4. Direct message traffic, relationship changes, and account record changes arrive that way.
@@ -41,7 +41,7 @@ Every one of those sets excludes a session that has not yet received the guild's
Channel visibility is `VIEW_CHANNEL` on the channel, plus extensions. A category is visible when at least one of its children is visible. A user with a live voice connection in a channel keeps virtual access to it whenever the channel would otherwise stop being visible. That covers a role or overwrite change removing `VIEW_CHANNEL`, and a move into a channel the user cannot view. Virtual access is keyed by user, so it applies to every session of that user. It is dropped when the user's voice connection to the channel ends.
Message access is `READ_MESSAGE_HISTORY` on the channel. Without that permission a session still receives events for messages newer than the guild's message history cutoff. A guild that sets no cutoff offers no such fallback, so a session without `READ_MESSAGE_HISTORY` receives none of the message-access filtered events there.
Message access is `READ_MESSAGE_HISTORY` on the channel. Without that permission a session still receives events for messages newer than the guild's [message history cutoff](/http-api/guilds/#guild-object). A guild that sets no cutoff offers no such fallback, so a session without `READ_MESSAGE_HISTORY` receives none of the message-access filtered events there.
[Channel Update Bulk](/gateway/events/#channel-update-bulk) contains only channels the recipient can view. If none are visible, no event is sent.
@@ -88,7 +88,7 @@ Every 30 seconds a passive session receives [Passive Updates](/gateway/events/#p
[Typing Start](/gateway/events/#typing-start) never follows the rule above. When the session set `typing` for the guild through [Lazy Request](/gateway/commands/#lazy-request), that value alone decides delivery. Without an override the event follows the active state, so a passive session in a large guild does not receive it.
The override applies to every session, including a bot session. A bot suppresses Typing Start in one guild through that override alone.
The override applies to every session, including a bot session. The only way for a bot to stop Typing Start in one guild and keep it in other guilds is to set `typing` to false for that guild.
### Member lists
@@ -129,7 +129,7 @@ A session that identified with a `shard` pair whose `shard_id` is not 0 drops ev
Account-level traffic, direct message traffic, relationship changes, and calls therefore never reach a session on a shard other than 0.
Sessions on shard 0, and sessions that identified without a `shard` pair, filter nothing at this gate. Fluxer still applies guild ownership at Identify, as [Sharding](/gateway/overview/#sharding) describes, so a shard 0 session is only ever connected to the guilds its shard owns.
Sessions on shard 0, and sessions that identified without a `shard` pair, filter nothing at this gate. Fluxer still filters guilds by shard at Identify, as [Sharding](/gateway/overview/#sharding) describes, so a shard 0 session is only ever connected to the guilds its shard owns.
## What a bot should send
+33 -33
View File
@@ -24,11 +24,11 @@ A Dispatch reports a change or command result through the [Gateway](/gateway/ove
}
```
[Ready](#ready) establishes sequence 1. Live Dispatches advance it by one. Replayed Dispatches keep their original sequence and can have gaps. [Resumed](#resumed) has the session's current sequence without advancing it and establishes the new live baseline. The sequence is local to one Gateway session and orders nothing across shards or HTTP operations.
[Ready](#ready) establishes sequence 1. Live Dispatches advance it by one. Replayed Dispatches keep their original sequence and can have gaps. [Resumed](#resumed) has the session's current sequence without advancing it. The next live Dispatch has that sequence plus one. The sequence is local to one Gateway session and orders nothing across shards or HTTP operations.
## Dispatch delivery
[Event filtering](/gateway/event-filtering/) defines delivery by guild availability, channel visibility, permissions, and session settings. Account-scoped Dispatches use only the session-level filters.
[Event filtering](/gateway/event-filtering/) defines delivery by guild availability, channel visibility, permissions, and session settings. Account-scoped Dispatches go through only the session-level filters, which are the shard filter and the `ignored_events` list.
Most guild-scoped Dispatches have a `guild_id` string. [Guild Create](#guild-create) and [Guild Sync](#guild-sync) identify the guild as `id`, and so does every [Guild Delete](#guild-delete) other than the one the guild itself dispatches when the guild is deleted. [Guild Counts Update](#guild-counts-update) and [Channel Member Counts Update](#channel-member-counts-update) have no top-level `guild_id`, and each entry in their `counts` array has its own.
@@ -62,15 +62,15 @@ A Dispatch is buffered for [Resume](/gateway/commands/#resume) replay unless it
| [Favorite Meme Update](#favorite-meme-update) | One of the current user's memes changes | Current user |
| [Favorite Meme Delete](#favorite-meme-delete) | The current user deletes a meme | Current user |
| [Guild Create](#guild-create) | A guild becomes available to the session | Guild connection |
| [Guild Sync](#guild-sync) | A subscribed session receives a replacement guild snapshot | Guild connection |
| [Guild Sync](#guild-sync) | A subscribed session receives the guild's full current state again | Guild connection |
| [Guild Update](#guild-update) | A guild's configuration changes | Guild connection |
| [Guild Delete](#guild-delete) | A guild leaves the session's visibility or becomes unavailable | Guild connection |
| [Guild Role Create](#guild-role-create) | A role is created in a guild | Guild connection |
| [Guild Role Update](#guild-role-update) | Exactly one role record changes | Guild connection |
| [Guild Role Update Bulk](#guild-role-update-bulk) | One operation changes several role records together | Guild connection |
| [Guild Role Delete](#guild-role-delete) | A role is deleted from a guild | Guild connection |
| [Guild Emojis Update](#guild-emojis-update) | A guild's emoji collection is replaced | Guild connection |
| [Guild Stickers Update](#guild-stickers-update) | A guild's sticker collection is replaced | Guild connection |
| [Guild Emojis Update](#guild-emojis-update) | A guild's emojis change | Guild connection |
| [Guild Stickers Update](#guild-stickers-update) | A guild's stickers change | Guild connection |
| [Channel Create](#channel-create) | A channel becomes visible to the session | Channel visibility |
| [Channel Update](#channel-update) | A visible channel changes | Channel visibility |
| [Channel Update Bulk](#channel-update-bulk) | One operation changes several channels together | Channel visibility |
@@ -90,7 +90,7 @@ A Dispatch is buffered for [Resume](/gateway/commands/#resume) replay unless it
| [Guild Ban Remove](#guild-ban-remove) | A guild ban is removed | Guild connection |
| [Presence Update](#presence-update) | One visible presence changes | Presence subscription |
| [Presence Update Bulk](#presence-update-bulk) | A recovering guild delivers its visible presences together | Guild connection |
| [Passive Updates](#passive-updates) | Passive channel watermarks and voice states advance for one session | Passive session |
| [Passive Updates](#passive-updates) | Channel `last_message_id` values or voice states changed for one passive session | Passive session |
| [Message Create](#message-create) | A visible message is created | Channel visibility |
| [Message Update](#message-update) | A visible message changes and is republished in full | Message access |
| [Message Delete](#message-delete) | One visible message is deleted | Message access |
@@ -170,8 +170,8 @@ The same structure appears in Ready, [Guild Create](#guild-create), and [Guild S
| properties | [guild](/http-api/guilds/#guild-object) object | Guild record without its roles, channels, emojis, stickers, or members |
| roles | array[[guild role](/http-api/permissions/#guild-role-object) object] | Every role in the guild |
| channels | array[[channel](/http-api/channels/#channel-object) object] | Channels the session can view |
| emojis | array[[guild emoji](/http-api/guild-emojis/#guild-emoji-object) object] | Guild emojis |
| stickers | array[[guild sticker](/http-api/guild-stickers/#guild-sticker-object) object] | Guild stickers |
| emojis | array[[guild emoji](/http-api/guild-emojis/#guild-emoji-object) object] | Every emoji in the guild |
| stickers | array[[guild sticker](/http-api/guild-stickers/#guild-sticker-object) object] | Every sticker in the guild |
| members<sup>1</sup> | array[[guild member](/http-api/guild-members/#guild-member-object) object] | The members the session needs immediately |
| member_count | integer | Total member count |
| online_count<sup>2</sup> | integer | Online member count |
@@ -189,7 +189,7 @@ The same structure appears in Ready, [Guild Create](#guild-create), and [Guild S
An unavailable guild is reduced to `id` and `unavailable: true`, plus `unavailable_hidden: true` when the guild is hidden. It has none of the other fields.
Inside [Ready](#ready), and inside the [Guild Create](#guild-create) burst a bot receives immediately after Ready, each member of this object has its `user` replaced by `{"id": "..."}`. On a user session the removed accounts appear in the Ready payload's `users` array. A bot's `users` array is empty, so a bot pulls those accounts with [Request Guild Members](/gateway/commands/#request-guild-members). A [Guild Create](#guild-create) sent later in the session, and every [Guild Sync](#guild-sync), have the members with `user` intact.
Inside [Ready](#ready), and inside the [Guild Create](#guild-create) burst a bot receives immediately after Ready, each entry in `members` has its `user` replaced by `{"id": "..."}`. On a user session the removed accounts appear in the Ready payload's `users` array. A bot's `users` array is empty, so a bot pulls those accounts with [Request Guild Members](/gateway/commands/#request-guild-members). A [Guild Create](#guild-create) sent later in the session, and every [Guild Sync](#guild-sync), have the members with `user` intact.
#### Session presence object
@@ -200,7 +200,7 @@ Inside [Ready](#ready), and inside the [Guild Create](#guild-create) burst a bot
| afk | boolean | Whether the session is away |
| mobile | boolean | Whether the session is mobile |
The first entry always has `session_id: "all"` and the account's flattened status.
The first entry always has `session_id: "all"` and the account's combined status, which is the first of `dnd`, `online`, `idle`, and `invisible` that any of its sessions has, or `offline` when none has one.
#### WebAuthn credential object
@@ -229,11 +229,11 @@ Sent after a successful [Resume](/gateway/commands/#resume) has replayed every r
| --- | --- | --- |
| _timings_gw? | object | Gateway-side timing breakdown, present only for a staff account |
The payload is otherwise empty. Resumed has the session's current sequence in `s` without advancing it, and that sequence becomes the new live baseline.
The payload is otherwise empty. Resumed has the session's current sequence in `s` without advancing it, and the next live Dispatch has that sequence plus one.
### <span id="sessions-replace"></span>SESSIONS_REPLACE
The account's set of live sessions changed. The payload, a bare JSON array of [session presence objects](#session-presence-object), replaces the client's copy in full. [Ready](#ready) sends the initial set as `sessions`.
The account's set of live sessions changed. The payload is a bare JSON array of [session presence objects](#session-presence-object) and is always the complete set. A client that stores the sessions replaces them with this array. [Ready](#ready) sends the initial set as `sessions`.
### <span id="auth-session-change"></span>AUTH_SESSION_CHANGE
@@ -288,7 +288,7 @@ The current user wrote or cleared a private note.
### <span id="user-pinned-dms-update"></span>USER_PINNED_DMS_UPDATE
The current user's pinned private channel set changed. The payload is a bare JSON array of channel ID strings in pinned order and replaces the client's copy in full. [Ready](#ready) sends the initial set as `pinned_dms`.
The current user's pinned private channel set changed. The payload is a bare JSON array of channel ID strings in pinned order and is always the complete set. A client that stores the pinned channels replaces them with this array. [Ready](#ready) sends the initial set as `pinned_dms`.
### <span id="user-connections-update"></span>USER_CONNECTIONS_UPDATE
@@ -298,11 +298,11 @@ The current user's external connection set changed.
| --- | --- | --- |
| connections | array[connection object] | Every connection the account holds |
The array replaces the client's copy in full.
`connections` is always the complete set. A client that stores the connections replaces them with this array.
### <span id="webauthn-credentials-update"></span>WEBAUTHN_CREDENTIALS_UPDATE
The current user's WebAuthn credential set changed. The payload is a bare JSON array of [WebAuthn credential objects](#webauthn-credential-object) and replaces the client's copy in full. [Ready](#ready) sends the initial set as `webauthn_credentials`.
The current user's WebAuthn credential set changed. The payload is a bare JSON array of [WebAuthn credential objects](#webauthn-credential-object) and is always the complete set. A client that stores the credentials replaces them with this array. [Ready](#ready) sends the initial set as `webauthn_credentials`.
### <span id="relationship-add"></span>RELATIONSHIP_ADD
@@ -378,15 +378,15 @@ The current user deleted a meme.
A guild became available to the session. The payload is a [guild ready object](#guild-ready-object).
Every collection in the event replaces the client's copy for that guild.
`roles`, `channels`, `emojis`, `stickers`, and `voice_states` are always complete. A client that stores any of them for the guild replaces its stored list with the new array. `members` is a partial list. A client adds or updates those members and keeps every other member it already stores.
A user session receives Guild Create when a guild becomes available after Ready, for example after joining one or after an unavailable guild recovers. A bot session receives one for every guild in the burst that follows Ready.
### <span id="guild-sync"></span>GUILD_SYNC
A session that asked for a sync through [Lazy Request](/gateway/commands/#lazy-request) receives a replacement snapshot of the guild. The payload is a [guild ready object](#guild-ready-object) and has the same replacement semantics as [Guild Create](#guild-create).
A session that asked for a sync through [Lazy Request](/gateway/commands/#lazy-request) receives the guild's full current state again. The payload is a [guild ready object](#guild-ready-object), and a client handles it the same way as [Guild Create](#guild-create).
Fluxer sends a sync when the subscription switches the guild between active and passive, and when `sync: true` names a guild the session has not already synced. A second `sync: true` for an already-synced guild sends nothing.
Fluxer sends a sync when a Lazy Request switches the guild between [active and passive](/gateway/event-filtering/#active-and-passive-guilds), and when `sync: true` names a guild the session has not already synced. A second `sync: true` for an already-synced guild sends nothing.
### <span id="guild-update"></span>GUILD_UPDATE
@@ -407,7 +407,7 @@ A guild left the session's visibility, or became unavailable.
<sup>1</sup> Present only when the guild itself is deleted. The payload has `id` alone when the account leaves a guild or is removed from one, and `id` with `unavailable` in the unavailable form
Without `unavailable`, the account is no longer a member and the client discards the guild. With `unavailable: true`, the guild is retained in a placeholder state and a later [Guild Create](#guild-create) restores it.
Without `unavailable`, the account is no longer a member, and a client deletes everything it stores for that guild. With `unavailable: true`, the guild is temporarily unreachable. A client keeps the guild as an unavailable entry until a later [Guild Create](#guild-create) sends its full state again.
### <span id="guild-role-create"></span>GUILD_ROLE_CREATE
@@ -447,25 +447,25 @@ A role was deleted from a guild.
### <span id="guild-emojis-update"></span>GUILD_EMOJIS_UPDATE
A guild's emoji collection changed.
A guild's emojis changed.
| Field | Type | Description |
| --- | --- | --- |
| guild_id | snowflake | Guild the emojis belong to |
| emojis | array[[guild emoji](/http-api/guild-emojis/#guild-emoji-object) object] | The complete emoji collection |
| emojis | array[[guild emoji](/http-api/guild-emojis/#guild-emoji-object) object] | Every emoji in the guild |
The array replaces the client's copy for that guild. Fluxer does not send per-emoji create, update, or delete events.
A client that stores the guild's emojis replaces them with this array. Fluxer does not send per-emoji create, update, or delete events.
### <span id="guild-stickers-update"></span>GUILD_STICKERS_UPDATE
A guild's sticker collection changed.
A guild's stickers changed.
| Field | Type | Description |
| --- | --- | --- |
| guild_id | snowflake | Guild the stickers belong to |
| stickers | array[[guild sticker](/http-api/guild-stickers/#guild-sticker-object) object] | The complete sticker collection |
| stickers | array[[guild sticker](/http-api/guild-stickers/#guild-sticker-object) object] | Every sticker in the guild |
The array replaces the client's copy for that guild. Fluxer does not send per-sticker create, update, or delete events.
A client that stores the guild's stickers replaces them with this array. Fluxer does not send per-sticker create, update, or delete events.
### <span id="channel-create"></span>CHANNEL_CREATE
@@ -549,7 +549,7 @@ A user became a member of a guild the session is connected to. The payload is th
A member's guild state or public user representation changed. The payload is the complete [guild member object](/http-api/guild-members/#guild-member-object) with `guild_id` added.
In a large guild, a passive session receives this event only when the subject is its own user.
In a guild with more than 250 members, a [passive](/gateway/event-filtering/#active-and-passive-guilds) session receives this event only when the subject is its own user.
### <span id="guild-member-remove"></span>GUILD_MEMBER_REMOVE
@@ -562,7 +562,7 @@ A user stopped being a member of a guild the session is connected to.
<sup>1</sup> The object has `id` alone. No other account field is sent, so a client MUST resolve the account from state it already holds
In a large guild, a passive session receives this event only when the subject is its own user.
In a guild with more than 250 members, a [passive](/gateway/event-filtering/#active-and-passive-guilds) session receives this event only when the subject is its own user.
### <span id="guild-members-chunk"></span>GUILD_MEMBERS_CHUNK
@@ -758,7 +758,7 @@ One visible message was deleted.
| guild_id? | snowflake | Guild the channel belongs to |
| member?<sup>2</sup> | [guild member](/http-api/guild-members/#guild-member-object) object | The author's guild member object, present in a guild channel |
<sup>1</sup> Both fields are omitted when the deletion came from moderation tools, and `author_id` is also omitted for a message with no author
<sup>1</sup> Both fields are omitted when an instance administrator deleted the message through the Admin API, when Fluxer deleted it after a CSAM report, or when Fluxer deleted it because content moderation blocked a link preview in it, and `author_id` is also omitted for a message with no author
<sup>2</sup> The `user` field is removed from it, and the whole field is absent when `author_id` is absent or the author is no longer a member
@@ -824,7 +824,7 @@ With the `DEBOUNCE_MESSAGE_REACTIONS` [session flag](/gateway/commands/#session-
<sup>1</sup> Every addition in `reactions` belongs to this message. A window covering several messages produces a separate Dispatch for each
When the window closes holding exactly one addition, the session sends [Message Reaction Add](#message-reaction-add) instead. A session without the flag receives one Message Reaction Add per addition.
When the window closes holding exactly one addition, Fluxer sends [Message Reaction Add](#message-reaction-add) to the session. A session without the flag receives one Message Reaction Add per addition.
#### Reaction addition object
@@ -884,7 +884,7 @@ A visible user began typing in a channel.
| guild_id? | snowflake | Guild the channel belongs to |
| member? | [guild member](/http-api/guild-members/#guild-member-object) object | The typing user's guild member object, present in a guild channel |
The `typing` override set through [Lazy Request](/gateway/commands/#lazy-request) decides delivery in a guild. With no override, a session receives the event when it is active in the guild or when the guild has 250 members or fewer, so a passive session in a small guild still receives it. A guild that sets the `TYPING_EVENTS` bit in its [disabled operations](/http-api/guilds/#disabled-guild-operations) produces the event for nobody.
The `typing` override set through [Lazy Request](/gateway/commands/#lazy-request) decides delivery in a guild. With no override, a session receives the event when it is [active](/gateway/event-filtering/#active-and-passive-guilds) in the guild or when the guild has 250 members or fewer, so a passive session in a small guild still receives it. A guild that sets the `TYPING_EVENTS` bit in its [disabled operations](/http-api/guilds/#disabled-guild-operations) produces the event for nobody.
### <span id="channel-pins-update"></span>CHANNEL_PINS_UPDATE
@@ -911,7 +911,7 @@ The current user acknowledged a channel's pins. Every session of the account rec
A participant's voice state changed. The payload is a [voice state object](#voice-state-object).
Recipients are the sessions that can view the voice channel, passive sessions included. A passive session in a large guild also receives the changed voice states through [Passive Updates](#passive-updates).
Recipients are the sessions that can view the voice channel, passive sessions included. A passive session in a guild with more than 250 members also receives the changed voice states through [Passive Updates](#passive-updates).
A `channel_id` of null means the participant left.
@@ -937,7 +937,7 @@ A `channel_id` of null means the participant left.
| e2ee_capable | boolean | Whether the participant's client supports end-to-end encrypted voice |
| version | integer | Monotonic version of this participant's voice state |
<sup>1</sup> Publisher-asserted. In a guild voice channel Fluxer sets it to false when the participant lacks `STREAM`
<sup>1</sup> The participant's client reports this value. In a guild voice channel Fluxer sets it to false when the participant lacks `STREAM`
The broadcast form has no `region_id`, `server_id`, `latitude`, or `longitude`.
@@ -1018,7 +1018,7 @@ A call ended, or became unavailable.
<sup>1</sup> Absent when the call ended
With `unavailable: true`, the client MUST retain a placeholder for the call. If it becomes available again, a fresh [Call Create](#call-create) includes `recipients` and `created_at`. Recovery is not guaranteed.
With `unavailable: true`, the client MUST keep the call as an unavailable entry. If it becomes available again, a fresh [Call Create](#call-create) includes `recipients` and `created_at`. Recovery is not guaranteed.
## Count response events
@@ -10,7 +10,7 @@ Every main Gateway connection has limits on what it sends, how long its session
[Framing](/gateway/overview/#framing) owns the protocol version, the payload bound, and the compression contract. One inbound WebSocket message is limited to 4,096 bytes on the wire and to a further 4,096 bytes after decompression, and either bound closes with `4002` and reason `Payload too large`.
A compressed message that decompresses past 10 MiB closes with `4002` and reason `Decompression failed`, before the 4,096-byte bound is reached.
A compressed message that decompresses past 10 MiB closes with `4002` and reason `Decompression failed`. That message never closes with `Payload too large`.
## Session lifecycle
@@ -25,7 +25,7 @@ One user credential holds at most 100 live sessions. A further Identify closes w
Shard counts run from 1 through 16,384. One bot shard covers at most 2,500 guilds. A malformed shard pair closes with `4010` for every credential. A bot assignment above the guild ceiling closes with `4011` and reason `Sharding required`. A user session is never refused for its guild count.
:::note[Session creation can be delayed]
During maintenance, a rollout or temporary capacity limits, an Identify can remain pending without a response. Continue heartbeating while waiting for Ready.
An Identify can remain pending with no response while the node drains, while the node is at capacity, while session starts are paused, or while the account is outside the session rollout percentage. Continue heartbeating while waiting for Ready.
:::
## Session start limit
@@ -88,7 +88,7 @@ Lazy Request accepts at most 10 member list ranges per channel, each with `end`
Request Guild Counts accepts at most 100 guild IDs after deduplication. Request Channel Member Counts accepts at most 25 channel IDs after deduplication. Both nonces run from 1 through 64 bytes, and a nonce outside that bound is omitted from the result.
Identify accepts at most 256 `ignored_events` entries. A longer array closes with `4002` and reason `Invalid identify payload`. Every other command payload bound coerces or drops. [Client commands](/gateway/commands/) states the exact coercion or drop rule for each field.
Identify accepts at most 256 `ignored_events` entries. A longer array closes with `4002` and reason `Invalid identify payload`. Fluxer coerces or drops a value outside any other command payload bound. [Client commands](/gateway/commands/) states the exact coercion or drop rule for each field.
## Voice admission
@@ -58,7 +58,7 @@ An opcode is the number that names a [Gateway payload](/gateway/overview/#gatewa
<sup>4</sup> Resume is accepted whether or not a session is already attached to the connection
<sup>5</sup> Opcode 7 precedes the close when the Gateway node is draining, when the session is fenced for a cluster handoff, and when a Resume from a new socket displaces this one
<sup>5</sup> Opcode 7 precedes the close when the Gateway node is draining, when the node transfers the session to another Gateway node, and when a Resume from a new socket displaces this one
<sup>6</sup> After the frame, a socket whose session ended is unauthenticated. After a failed Resume, a socket that already held a session still holds it
@@ -80,7 +80,7 @@ A client SHOULD log an unknown opcode and ignore the frame, and MUST NOT close o
| Code | Name | Meaning |
| --- | --- | --- |
| 4000 | Unknown error | The Gateway drained the connection, or a session operation could not be completed |
| 4000 | Unknown error | Drain, an unclassified session creation error, or a Resume whose retained session could not be reached |
| 4001 | Unknown opcode | The opcode is undefined, is a server opcode, or the payload has no `d` |
| 4002 | Decode error | The payload size, compression stream, encoding, or command fields are invalid |
| 4003 | Not authenticated | An authenticated command arrived before Identify or Resume attached a session |
@@ -95,7 +95,7 @@ A client SHOULD log an unknown opcode and ignore the frame, and MUST NOT close o
<sup>1</sup> `shard_count` is an integer from 1 to 16384, and `shard_id` is a non-negative integer below `shard_count`
<sup>2</sup> The count is taken after the shard filter, so a bot clears it by identifying with a `shard_count` large enough to divide its guilds
<sup>2</sup> The count is taken after the shard filter, so a bot clears it by identifying with a `shard_count` large enough that no shard owns more than 2,500 guilds
Code 4006 is unassigned, and no code above 4012 is defined. [Event filtering](/gateway/event-filtering/) describes how a client bounds the events its session receives.
@@ -120,7 +120,7 @@ Code 4006 is unassigned, and no code above 4012 is defined. [Event filtering](/g
<sup>2</sup> A Resume that fails token verification leaves the named session in place for the rest of its retention window, so a later Resume with the owning token still recovers it. An Identify that fails token verification leaves nothing to recover
`Resumable` describes only whether an already established session can still be recovered with [Resume](/gateway/commands/#resume). The 60,000 ms retention window and the bounded replay buffer described in [Limits and rate limits](/gateway/limits-and-rate-limits/#replay-and-backpressure) apply unchanged.
`Resumable` describes only whether an already established session can still be recovered with [Resume](/gateway/commands/#resume). The 60,000 ms retention window and the bounded replay buffer described in [Limits and rate limits](/gateway/limits-and-rate-limits/#replay-and-backpressure) still limit whether a Resume succeeds and what it replays.
:::caution[Reconnecting unchanged reproduces `4004`, `4010`, and `4012`]
A client changes the token, the shard pair, or the version before it reconnects.
@@ -176,7 +176,7 @@ The Gateway sends an exact reason string with every application close.
<sup>3</sup> The Gateway holds the Identify and retries it silently after a classified transient failure, so the connection stays open. That covers a paused rollout, a draining node, an ineligible account, an Identify rate limit, a saturated start budget, and a failed session RPC
<sup>4</sup> The Gateway node is draining, the session is being fenced for a cluster handoff, or a Resume from a new socket displaced this one
<sup>4</sup> The Gateway node is draining, the node is transferring the session to another Gateway node, or a Resume from a new socket displaced this one
Reason strings are stable wire values. A client branches on the code and MAY record the reason for diagnosis.
@@ -61,7 +61,7 @@ Append the connection parameters to the discovered URL.
<sup>3</sup> `compress=zstd-stream` selects compression only when `stream` is also `1` or `true`. Without the flag the connection is uncompressed
Unknown parameters are ignored. An unrecognised `compress` or `stream` value selects no compression and the connection stays open. A client MUST read the negotiated representation from the frame type it receives.
Unknown parameters are ignored. An unrecognised `compress` or `stream` value selects no compression and the connection stays open. A client MUST read whether the connection is compressed from the frame type it receives. On a `zstd-stream` connection every server frame is a binary frame.
The reference client connects with `?v=1&encoding=json&compress=zstd-stream&stream=1`.
@@ -86,7 +86,7 @@ A decoded payload that is not a JSON object closes with `4002` and reason `Decod
A Dispatch is a server-to-client event payload. Every live Dispatch advances the session sequence by one. A session starts at sequence 0, so the [Ready](/gateway/events/#ready) sequence is 1.
A replayed Dispatch keeps its original sequence, and a replayed run can have gaps, because several event families are delivered live and never retained. [Resumed](/gateway/events/#resumed) has the current sequence and does not advance it, which sets the new live baseline. The sequence is local to one Gateway session and has no meaning across sessions or shards.
A replayed Dispatch keeps its original sequence, and a replayed run can have gaps, because Guild Sync, Guild Member List Update, and Guild Members Chunk are delivered live and never retained. [Resumed](/gateway/events/#resumed) has the current sequence and does not advance it. The next live Dispatch after Resumed has that sequence plus one. The sequence is local to one Gateway session and has no meaning across sessions or shards.
## Framing
@@ -94,7 +94,7 @@ One inbound WebSocket message is limited to 4,096 bytes on the wire, and a compr
JSON payloads are UTF-8 objects. An uncompressed client payload is sent in a text frame, and a payload the client compressed with the negotiated zstd stream is sent in a binary frame.
The Gateway does not inspect the inbound frame type. It reads the bytes from the negotiated compression alone. A client MUST send every payload in the negotiated representation. On a connection with no negotiated compression the payload is uncompressed, and on a `zstd-stream` connection every client payload goes through the same compression stream in order.
The Gateway does not inspect the inbound frame type. It decodes every inbound message with the compression the connection negotiated, whatever the frame type. A client MUST send every payload in the negotiated representation. On a connection with no negotiated compression the payload is uncompressed, and on a `zstd-stream` connection every client payload goes through the same compression stream in order.
### JSON integer representation
@@ -133,7 +133,7 @@ A connection moves through these states: Opening, Unauthenticated, Starting, Rep
| Identify. Valid Identify payload and Identify capacity available | Begin session creation | Starting |
| Identify. Gateway draining, node at capacity, session starts paused, or the account outside the session rollout | Hold the payload and retry it in the background | Unauthenticated |
| Identify. The source IP Identify budget is exhausted | Discard the payload without a reply | Unauthenticated |
| Resume. Valid Resume payload | Resolve the retained session | Starting |
| Resume. Valid Resume payload | Look up the retained session named by `session_id` | Starting |
| Authenticated command. Any command other than Heartbeat, Identify, or Resume | Close with `4003` and reason `Not authenticated` | Closed |
### Starting
@@ -141,8 +141,8 @@ A connection moves through these states: Opening, Unauthenticated, Starting, Rep
| Event and condition | Action | Next state |
| --- | --- | --- |
| Session creation succeeds. Identify was accepted | Send Ready | Ready |
| Session creation fails permanently. Invalid token, invalid shard, sharding required, or too many sessions | Close with the mapped code and reason | Closed |
| Session creation fails without a mapped code | Close with `4000` and reason `Failed to start session` | Closed |
| Session creation fails permanently. Invalid token, invalid shard, sharding required, or too many sessions | Close with the code and reason that [Hello and session creation](#hello-and-session-creation) lists for that failure | Closed |
| Session creation fails with an error the Gateway does not classify | Close with `4000` and reason `Failed to start session` | Closed |
| Session creation fails temporarily. Draining, at capacity, RPC failure, timeout, or the account outside the session rollout | Hold the Identify and retry it in the background | Unauthenticated |
| Resume succeeds. The retained session accepted the sequence | Replay retained Dispatches | Replaying |
| Session cannot be resumed. Resume named an unknown or expired session, or a `seq` below the replay floor | Send Invalid Session with `d: false` | Unauthenticated |
@@ -178,7 +178,7 @@ A connection moves through these states: Opening, Unauthenticated, Starting, Rep
| --- | --- | --- |
| Heartbeat. No session is attached, or the payload is `null`, or the attached session accepts the sequence | Send Heartbeat ACK | Same state |
| Heartbeat deadline. The connection is awaiting an acknowledgement and more than 45,000 ms have passed since the last one | Close with `4009` and reason `Heartbeat timeout` | Closed |
| Invalid frame or payload. Size, decompression, or decoding validation fails | Close with the applicable close code | Closed |
| Invalid frame or payload. Size, decompression, or decoding validation fails | Close with `4002` and the matching reason from [Gateway payload](#gateway-payload) or [Framing](#framing) | Closed |
| Transport ends. A session exists | Retain the session for 60,000 ms | Closed |
An opcode outside the registry, and a server opcode sent by a client, close with `4001` once a session is attached and with `4003` while the connection is unauthenticated.
@@ -221,9 +221,9 @@ Opcode 1 is accepted before and after authentication. Before a session exists it
}
```
The server answers with Opcode 11 Heartbeat ACK, which has no `d`. Once a session is attached, a `d` value that is neither `null` nor an integer closes with `4007` and reason `Invalid sequence`. A session that does not answer within 5,000 ms closes with the same code and reason.
The server answers with Opcode 11 Heartbeat ACK, which has no `d`. Once a session is attached, a `d` value that is neither `null` nor an integer closes with `4007` and reason `Invalid sequence`. When the Gateway cannot confirm the sequence with the session within 5,000 ms, the connection closes with the same code and reason.
The server can request an immediate heartbeat with Opcode 1 and `d: null`. Answer it with your own Opcode 1. Continue sending heartbeats at the advertised interval. A connection that misses the heartbeat deadline closes with `4009` and reason `Heartbeat timeout`.
The server requests an immediate heartbeat with Opcode 1 and `d: null` once 90 per cent of the interval has passed since the last acknowledgement. Answer it with your own Opcode 1. Continue sending heartbeats at the advertised interval. A connection that misses the heartbeat deadline closes with `4009` and reason `Heartbeat timeout`.
A heartbeat with a sequence permanently trims every retained Dispatch at or below that sequence from the replay buffer and records it as the acknowledged sequence. A client MUST send the sequence it has processed, because a later Resume from a lower sequence closes with `4007`.
@@ -257,7 +257,7 @@ Unlike Identify, Resume is accepted in every open state. A socket that already h
When the resumed session was attached to a different socket, that socket receives Opcode 7 Reconnect and then closes with `4000`.
:::caution[Retention covers reconnection recovery only]
[Limits and rate limits](/gateway/limits-and-rate-limits/#replay-and-backpressure) states the exact bounds, and several high-volume Dispatch events are never retained.
[Limits and rate limits](/gateway/limits-and-rate-limits/#replay-and-backpressure) states the exact bounds, and Guild Sync, Guild Member List Update, and Guild Members Chunk are never retained.
:::
## Reconnect
@@ -270,7 +270,7 @@ Opcode 7 Reconnect asks the client to open a new WebSocket. The Gateway sends it
}
```
The current socket then closes with `4000` and reason `Session drain requested; reconnect to continue`. The session can be resumed while it remains inside its retention bounds.
The current socket then closes with `4000` and reason `Session drain requested; reconnect to continue`. A Resume sent within 60,000 ms of the close can recover the session, subject to the sequence bounds in [Resuming a session](#resuming-a-session).
## Invalid session
@@ -298,7 +298,7 @@ For a user session, the filtered set is also the [Ready](/gateway/events/#ready)
Fluxer checks only a bot session against the guild ceiling. A bot whose shard owns more than 2,500 guilds closes with `4011` and reason `Sharding required`. A bot that supplies no pair is checked against its whole guild list. A user session is bounded by the 100-session-per-user limit alone, whatever its guild count.
:::note[Shard 0 also receives account-level traffic]
The per-Dispatch shard filter in [Dispatch delivery](/gateway/events/#dispatch-delivery) runs only when `shard_id` is not 0. No other shard receives a copy, so handle those events on shard 0.
The per-Dispatch shard filter in [Dispatch delivery](/gateway/events/#dispatch-delivery) runs only when `shard_id` is not 0. A session with a non-zero `shard_id` drops every Dispatch that names no guild, apart from Rate Limited, Guild Counts Update, and Channel Member Counts Update. Account-level Dispatches name no guild, so handle them on shard 0.
:::
Fluxer has no large bot tier, no shard-count alignment requirement, and no Identify concurrency buckets. `GET /v1/gateway/bot` returns a fixed recommendation.
@@ -307,4 +307,4 @@ Fluxer has no large bot tier, no shard-count alignment requirement, and no Ident
Dispatch ordering applies within one Gateway session. It creates no total order across shards, HTTP responses, or Media Proxy operations.
[Guild Create](/gateway/events/#guild-create) and [Guild Sync](/gateway/events/#guild-sync) are replacement boundaries for the guild they name. Everything else is a delta against the state those boundaries established.
[Guild Create](/gateway/events/#guild-create) and [Guild Sync](/gateway/events/#guild-sync) send the complete roles, channels, emojis, stickers, and voice states for the guild they name, and a client replaces its stored lists with them, as Guild Create describes. Every other Dispatch for that guild changes part of that stored state.
@@ -10,9 +10,9 @@ An application is an OAuth2 client owned by one user account, and every bot acco
## Access rules
Every route except [Get public application](#get-public-application) requires a credential. A user-only route rejects a bot token and an OAuth2 bearer with 403 `ACCESS_DENIED`. Fluxer accepts an account with an outstanding required action everywhere here.
Every route except [Get public application](#get-public-application) requires a credential. A user-only route rejects a bot token and an OAuth2 bearer with 403 `ACCESS_DENIED`. Every route on this page accepts an account that has an outstanding [required action](/http-api/users/#required-actions).
The sudo-gated routes read `X-Fluxer-Sudo-Mode-JWT`. A valid proof for the authenticated account satisfies [sudo mode](/http-api/users/mfa/#sudo-mode) on its own, and Fluxer echoes it back in the response header. Fluxer issues no token to an account with no authenticator, so that account proves sudo mode with `password` in the body.
The sudo-gated routes read `X-Fluxer-Sudo-Mode-JWT`. A valid sudo token in that header, issued to the authenticated account, satisfies [sudo mode](/http-api/users/mfa/#sudo-mode) with no body proof. Fluxer returns the same token in the `X-Fluxer-Sudo-Mode-JWT` response header. Fluxer issues no token to an account with no authenticator, so that account proves sudo mode with `password` in the body.
:::caution[Credentials are shown once]
A client secret is returned only by [Create application](#create-application) and [Reset client secret](#reset-client-secret), and a [bot token](/authentication/#token-formats) only by [Create application](#create-application) and [Reset bot token](#reset-bot-token).
@@ -20,7 +20,7 @@ A client secret is returned only by [Create application](#create-application) an
## Rate limits
A bucket name ending in `::client_id` still counts one allowance for the authenticated user across every application that user owns. [List owned applications](#list-owned-applications), [Get current application](#get-current-application), [Get application](#get-application), [Get public application](#get-public-application), and [List OAuth2 authorisations](/http-api/oauth2/#list-oauth2-authorisations) all draw on one `oauth_dev:clients:list` allowance. [Get bot application](#get-bot-application) draws on the same bucket, keyed by the bot account the token names. That allowance is separate from the owner's.
A bucket whose name ends in `::client_id` gives the authenticated user one allowance. Requests for every application that user owns count against that one allowance. [List owned applications](#list-owned-applications), [Get current application](#get-current-application), [Get application](#get-application), [Get public application](#get-public-application), and [List OAuth2 authorisations](/http-api/oauth2/#list-oauth2-authorisations) all draw on one `oauth_dev:clients:list` allowance. [Get bot application](#get-bot-application) draws on the same bucket, keyed by the bot account the token names. That allowance is separate from the owner's.
## Application object
@@ -42,7 +42,7 @@ The application record as its owner sees it.
<sup>2</sup> Present only in the response to [Create application](#create-application) and [Reset client secret](#reset-client-secret), which are the only operations that issue one
<sup>3</sup> Present only when the operation resolved the bot account, which excludes [Reset client secret](#reset-client-secret), an application that owns no bot account, and an application whose bot account record can no longer be read
<sup>3</sup> Present in every response except [Reset client secret](#reset-client-secret), and absent for an application that owns no bot account or whose bot account record can no longer be read
### Example
@@ -109,7 +109,7 @@ The application as any caller sees it, including a caller that presents no crede
| bot<sup>4</sup> | ?[application bot](#application-bot-object) object | The bot account the application owns |
| current_user?<sup>5</sup> | ?[partial user](/http-api/users/#partial-user-object) object | The requesting account |
<sup>1</sup> Null when the application has no bot account or that account has no avatar, and the stored hash is reported unchanged
<sup>1</sup> Null when the application has no bot account or that account has no avatar. Otherwise it is the bot account's stored avatar hash, and an animated hash keeps its `a_` prefix
<sup>2</sup> Contains `bot` when the application owns a bot account, and is otherwise empty
@@ -121,7 +121,7 @@ The application as any caller sees it, including a caller that presents no crede
## Current bot application object
The application as the bot token that application issued sees it.
The application that issued the requesting bot token, as returned to that token.
### Structure
@@ -140,7 +140,7 @@ The application as the bot token that application issued sees it.
<sup>1</sup> Read from the bot account, so it changes with [Update bot profile](#update-bot-profile), and it is null when the application has no bot account
<sup>2</sup> No application signing key is available
<sup>2</sup> Always 64 `0` characters, because no application signing key is available
<sup>3</sup> The request fails with 401 `INVALID_TOKEN` when the owning account no longer exists
@@ -150,7 +150,7 @@ The application as the bot token that application issued sees it.
## Bot profile object
The bot account's profile fields after an update. [Update bot profile](#update-bot-profile) is the only operation that returns this shape, and it has no bot token.
The bot account's profile fields after an update. [Update bot profile](#update-bot-profile) is the only operation that returns this shape. The object has no `token` field.
### Structure
@@ -285,7 +285,7 @@ This representation exposes the full registered redirect URI list and the bot pr
Creates an application together with its bot account. Returns the [application](#application-object) object with the initial client secret and bot token.
An unclaimed account cannot create an application. An account is unclaimed while it holds no password credential, is not a bot, and does not have the single sign-on trait.
An unclaimed account cannot create an application. An account is unclaimed while it holds no password credential, is not a bot, and has not been linked to a single sign-on identity.
### Request headers
@@ -294,7 +294,7 @@ An unclaimed account cannot create an application. An account is unclaimed while
| X-Captcha-Token?<sup>1</sup> | string | The CAPTCHA proof for the request |
| X-Captcha-Type?<sup>2</sup> | string | The CAPTCHA provider to verify against, either `hcaptcha` or `turnstile` |
<sup>1</sup> A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`. Fluxer skips verification when CAPTCHA is disabled for the instance, when the account has the CAPTCHA exemption flag, or when the caller's contact has the CAPTCHA exemption capability, as described by [CAPTCHA handling](/topics/captcha/)
<sup>1</sup> A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`. Fluxer skips verification when CAPTCHA is disabled for the instance, when the account has the CAPTCHA exemption flag, or when the instance's account policy grants the caller's email address the CAPTCHA exemption capability, as described by [CAPTCHA handling](/topics/captcha/)
<sup>2</sup> Fluxer verifies against the instance's configured provider when the header is absent
@@ -405,7 +405,7 @@ Removing a redirect URI takes effect immediately for new authorisation requests.
### Side effects
The submitted fields replace the matching application configuration. Neither credential is rotated, and no Gateway Dispatch is emitted.
Each submitted field replaces the stored value of that field. Neither credential is rotated, and no Gateway Dispatch is emitted.
### Rate limit
@@ -444,7 +444,7 @@ Updates the bot account an application owns and returns the resulting [bot profi
<sup>5</sup> Fluxer reads only the [bot flags](#bot-flags) from the supplied bitfield and sets or clears each to match. It ignores every other bit
Fluxer checks the decoded bytes of `avatar` and `banner` against the instance's avatar byte ceiling, which applies to both fields and defaults to 10 MiB. Each image also passes the format allowlist and the animation rules of the asset policy for the field it sets. Pixel dimensions are never checked.
Fluxer checks the decoded bytes of `avatar` and `banner` against the instance's avatar byte ceiling, which applies to both fields and defaults to 10 MiB. An image in a format the field does not accept, an animated image on a field that accepts no animation, and any animated AVIF return the field code `INVALID_IMAGE_FORMAT`. Pixel dimensions are never checked.
### Response
@@ -69,7 +69,7 @@ The public single sign-on state. The same object is embedded by the [instance di
| 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 |
<sup>1</sup> The value is true only when the operator has enabled SSO and the resolved provider configuration is complete, so a partially configured provider reports false
<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
<sup>2</sup> The value is true only when `enabled` is also true, and every local authentication operation then returns 403 `SSO_REQUIRED`
@@ -85,7 +85,7 @@ The parameters for sending the user to the identity provider, bound to one new S
| state<sup>1</sup> | string | The one-use CSRF state |
| redirect_uri | string | The callback URI bound to this state |
<sup>1</sup> The state is consumed by the first [complete SSO](#complete-sso) attempt that resolves it
<sup>1</sup> The first [complete SSO](#complete-sso) request that presents the unexpired state consumes it, whether or not that request then succeeds
A client MUST return the state unchanged and MUST NOT interpret its contents.
@@ -264,8 +264,8 @@ The code that identifies one pending desktop handoff.
<sup>1</sup> The code is 12 characters drawn from the alphabet `ABCDEFGHJKMNPQRSTUVWXYZ23456789`, rendered as two groups of six separated by a hyphen
:::caution[The code alone cancels a handoff]
Anyone who learns the code reads the pending device metadata and cancels the handoff. The token needs `poll_secret` as well. A client MUST show the code only to the person performing the handoff.
:::caution[The code alone reads the device metadata]
Anyone who learns the code reads the pending device metadata. Reading the token and cancelling the handoff both require `poll_secret`. A client MUST show the code only to the person performing the handoff.
:::
## Handoff information object
@@ -407,9 +407,9 @@ Approval mode registration instead returns 403 `REGISTRATION_PENDING_APPROVAL` a
<RouteHeader method="POST" path="/v1/auth/register" unauthenticated />
Creates an ordinary account. Returns an [authentication token response](#authentication-token-response) when the instance admits the account immediately and a [registration pending approval response](#registration-pending-approval-response-object) when it does not. Emits a [Guild Member Add](/gateway/events/#guild-member-add) Gateway event when an invite or instance community admission takes effect.
Creates an ordinary account. Returns an [authentication token response](#authentication-token-response) when the instance admits the account immediately and a [registration pending approval response](#registration-pending-approval-response-object) when it does not. Emits a [Guild Member Add](/gateway/events/#guild-member-add) Gateway event for each guild the new account joins through an invite or through the instance's single community guild.
Registration verifies [CAPTCHA](/topics/captcha/) when CAPTCHA is enabled. It permits 3 attempts per hour for each client IP address and 15 per hour for each client subnet, which is the IPv4 /24 or IPv6 /48 network. A supplied email address permits 3 attempts per 15 minutes of its own. Those allowances are separate from the route bucket, and only a deployment that relaxes registration rate limits disables them.
Registration verifies [CAPTCHA](/topics/captcha/) when CAPTCHA is enabled. It permits 3 attempts per hour for each client IP address and 15 per hour for each client subnet, which is the IPv4 /24 or IPv6 /48 network. A supplied email address permits 3 attempts per 15 minutes of its own. Those allowances are separate from the route bucket, and only a deployment with `dev.relax_registration_rate_limits` set to true disables them.
### Request headers
@@ -478,7 +478,7 @@ There is no retry key. A repeated request with the same values creates a second
The operation applies the instance's registration, email-domain, breached-password, and regional policies before creating the account, and its risk policy can set suspicious activity flags on the created 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.
Registration can accept a supplied or instance-configured invite, and the account can join the instance community. Each join emits [Guild Member Add](/gateway/events/#guild-member-add) to the affected guild's sessions. Registration policy can suppress invite admission.
Registration accepts the `invite_code` from the body, or the instance's configured auto-join invite when the body has none. On an instance with single-community mode enabled, the account also joins the community guild. Each join emits [Guild Member Add](/gateway/events/#guild-member-add) to the affected guild's sessions. Registration policy can suppress invite admission.
Approval mode registration creates no guild membership and no authentication session, and the account cannot sign in until an administrator approves it. Every other successful registration creates one session and returns its token.
@@ -533,7 +533,7 @@ Account policy can return 403 `REGISTRATION_PENDING_APPROVAL`, 403 `REGISTRATION
On an account the user had disabled, a correct password clears the disabled state. It also cancels a self-scheduled deletion if erasure has not started. Both happen before any second factor is requested, without a separate confirmation step. Once erasure starts, login returns `ACCOUNT_SUSPENDED_PERMANENTLY`.
:::
The login clears an expired temporary suspension the same way, but returns the 403 of a live temporary or permanent administrator suspension without clearing it.
A correct password also clears an expired temporary suspension before any second factor is requested. A live temporary or permanent administrator suspension stays in place, and the login returns its 403.
### Response
@@ -860,7 +860,7 @@ Password recovery verifies CAPTCHA when CAPTCHA is enabled. Fluxer consumes both
<sup>1</sup> An address whose domain has no usable DNS records returns the field code `INVALID_EMAIL_ADDRESS`, while an address that passes DNS validation but belongs to no account returns the ordinary success response
:::note[Account existence is not disclosed]
An address that resolves to no account produces the same 204 response as an address that resolves to an ordinary account. Only DNS validation failure, the route bucket, the extra allowances, and an address belonging to a bot account can produce a different outcome.
An address that resolves to no account produces the same 204 response as an address that resolves to an ordinary account. Only a DNS validation failure, the route bucket, the client IP address and email address allowances, and an address belonging to a bot account can produce a different outcome.
:::
### Response
@@ -943,7 +943,7 @@ An unknown or already consumed token, and a token bound to a deleted account, re
Passwords are checked against a breached-password corpus, as they are during [registration](#register-an-account) and [email reversion](#revert-an-email-change).
:::caution[Resetting a password revokes every existing session]
A successful reset terminates every authentication session on the account, including the one any other device is holding. It then issues one fresh session. An account with a second factor instead receives an MFA ticket, and the session follows the factor.
A successful reset terminates every authentication session on the account, including the one any other device is holding. It then issues one fresh session. An account with a second factor receives an MFA ticket, and completing MFA with that ticket creates the session.
:::
### Response
@@ -971,7 +971,7 @@ An account with no second factor then receives one new session and its token. An
<RouteHeader method="POST" path="/v1/auth/email-revert" unauthenticated />
Consumes the token delivered to the previous email address, restores that address, replaces the password, and rebuilds the account's credential state. Returns an [authentication token response](#authentication-token-response). Emits a [User Update](/gateway/events/#user-update) Gateway event.
Consumes the token delivered to the previous email address, restores that address, replaces the password, terminates every authentication session, and clears every second factor. The requesting IP address becomes the only authorised IP address. Returns an [authentication token response](#authentication-token-response). Emits a [User Update](/gateway/events/#user-update) Gateway event.
The token is valid for 24 hours after the address change that issued it.
@@ -1005,7 +1005,7 @@ Fluxer checks the replacement password against the same breached-password corpus
### Side effects
The previous email address becomes verified and the replacement password takes effect. All other sessions, second factors and authorised IP addresses are removed. Only the requesting IP address remains authorised.
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.
The account receives [User Update](/gateway/events/#user-update). Existing Gateway sessions end as described under [shared behaviour](#shared-behaviour), and the response returns one new authentication session.
@@ -1115,7 +1115,7 @@ An unknown, expired, already consumed, or account-mismatched token returns the f
| Status | Body | Condition |
| --- | --- | --- |
| 204 | empty | The IP address was authorised and the login result was published |
| 204 | empty | The IP address was authorised and the new session token is readable through poll IP authorisation |
| 400 | [error response](/http-api/#error-response) | The body or authorisation token is invalid |
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation or the token resolves to a bot account |
| 404 | [error response](/http-api/#error-response) | The token resolves to an account that no longer exists |
@@ -1147,7 +1147,7 @@ Sends the authorisation message for an outstanding IP authorisation ticket again
<sup>1</sup> The resend reuses the authorisation token already bound to the ticket, so a message delivered by an earlier send remains valid
An unknown or expired ticket returns the field code `INVALID_OR_EXPIRED_AUTHORIZATION_TICKET`. A resend before the delay elapses returns 429 `IP_AUTHORIZATION_RESEND_COOLDOWN` with a `Retry-After` header and a top-level `resend_available_in` in seconds. A second resend returns 400 `IP_AUTHORIZATION_RESEND_LIMIT_EXCEEDED`.
An unknown or expired ticket returns the field code `INVALID_OR_EXPIRED_AUTHORIZATION_TICKET`. A resend less than 30 seconds after the ticket was issued returns 429 `IP_AUTHORIZATION_RESEND_COOLDOWN` with a `Retry-After` header and a top-level `resend_available_in` in seconds. A second resend returns 400 `IP_AUTHORIZATION_RESEND_LIMIT_EXCEEDED`.
:::caution[A lost response still spends the resend]
There is no retry key. A client that loses the response cannot tell whether the message was delivered, and a retry returns 400 `IP_AUTHORIZATION_RESEND_LIMIT_EXCEEDED` once the first request marked the resend used.
@@ -1414,7 +1414,7 @@ Reports the state of a handoff and delivers the new session token once, to a cal
| --- | --- | --- |
| code | string | The handoff code, using the same normalisation contract as [get desktop handoff information](#get-desktop-handoff-information) |
### Request body
### JSON body
| Field | Type | Description |
| --- | --- | --- |
@@ -1457,18 +1457,24 @@ Discards a handoff and everything stored against its code. Authentication is not
| --- | --- | --- |
| code | string | The handoff code, using the same normalisation contract as [get desktop handoff information](#get-desktop-handoff-information) |
An unknown or already expired code is an idempotent success.
### JSON body
:::caution[Anyone holding the code can cancel]
The route checks no credential and the code is its only input, so a party that learns the code can cancel a handoff the initiating device is still waiting on. The initiating device observes this as the `expired` status.
| Field | Type | Description |
| --- | --- | --- |
| poll_secret | string | The secret returned by [initiate desktop handoff](#initiate-desktop-handoff) |
An unknown or already expired code has no stored secret, so it returns 400 `INVALID_HANDOFF_CODE`.
:::caution[Cancelling requires the poll secret]
The route requires the `poll_secret` that [initiate desktop handoff](#initiate-desktop-handoff) returned, sent in the JSON body. A wrong secret returns 400 `INVALID_HANDOFF_CODE`, so a party that knows only the code cannot cancel the handoff. After a cancellation the initiating device reads the `expired` status.
:::
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | empty | The handoff was discarded, or no handoff existed for that code |
| 400 | [error response](/http-api/#error-response) | The code is malformed, returning `INVALID_HANDOFF_CODE` |
| 204 | empty | The handoff was discarded |
| 400 | [error response](/http-api/#error-response) | The code is malformed, or the secret does not match, returning `INVALID_HANDOFF_CODE` |
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
| 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 |
@@ -6,12 +6,12 @@ description: Checkout, card preapproval, gift purchase, age verification, refund
import RouteHeader from '@/components/RouteHeader.astro';
These routes take payment for premium, verify an account holder's age, and refund the most recent purchase. Each finishes on the payment provider's own pages, so a route here creates the provider session and returns its URL for the browser to open. [Receive Stripe webhook](#receive-stripe-webhook) takes the signed events the provider sends back. The [Premium resource](/http-api/premium/) owns entitlement state, mirrored billing data and subscription self-service.
These routes take payment for premium, verify an account holder's age, and refund the most recent purchase. Checkout, card preapproval and age verification each finish on the payment provider's own pages, so those routes create the provider session and return its URL for the browser to open. [Receive Stripe webhook](#receive-stripe-webhook) takes the signed events the provider sends back. The [Premium resource](/http-api/premium/) owns entitlement state, mirrored billing data and subscription self-service.
Every route here is hosted-only, as [deployment availability](/http-api/deployment-availability/) describes. All of them are user-only except [receive Stripe webhook](#receive-stripe-webhook) and [continue localised card preapproval](#continue-localised-card-preapproval).
:::note[Every response is a Fluxer object]
The responses are Fluxer redirect, eligibility, refund and acknowledgement objects. The only payment provider values in them are the identifiers on a [refund](#refund-object).
The responses are Fluxer redirect, eligibility, refund and acknowledgement objects. They copy only a few payment provider values, such as the identifiers on a [refund](#refund-object) and the `invoice_id` of a [refund eligibility](#refund-eligibility-object) object.
:::
## Redirect URL object
@@ -36,7 +36,7 @@ One absolute URL that completes a billing operation in a browser. [Create subscr
## Localised card preapproval result object
The state of one card preapproval flow. A localised recurring price is offered only to a card issued in the matching country. Fluxer creates the paid session only after a separate setup mode session has proven the card country. `status` says which of the variants the object is, and each variant defines its own members.
The state of one card preapproval flow. A localised recurring price is offered only to a card issued in the matching country. Fluxer creates the paid session only after a separate setup mode session, which takes no payment, has shown that the card was issued in the matching country. `status` says which of the variants the object is, and each variant defines its own members.
### Structure
@@ -67,7 +67,7 @@ The state of one card preapproval flow. A localised recurring price is offered o
| Value | Description |
| --- | --- |
| pending | The setup session has not completed, or a concurrent continuation is already resolving the same token |
| pending | Setup has not completed, or another [continue localised card preapproval](#continue-localised-card-preapproval) call for this token is still running |
| ready | The card was approved and the paid checkout URL is available |
| rejected | The card was refused and the paid checkout cannot continue |
| expired | The token is empty, unknown, or has passed its one-day lifetime |
@@ -85,7 +85,7 @@ The state of one card preapproval flow. A localised recurring price is offered o
## Refund eligibility object
Whether the account's most recent purchase can still be refunded without operator involvement. When no refundable purchase was resolved, `eligible` is false, every invoice member is null, and `cooldown_expires_at` still reports an active cooldown unless the reason is `feature_unavailable`. A client reads `eligible` and `reason` before rendering any amount or timestamp.
Whether the account's most recent purchase can still be refunded without operator involvement. When no refundable purchase was resolved, `eligible` and `cancels_subscription` are false. `invoice_id`, `invoice_amount_paid_cents`, `currency`, `paid_at` and `refund_window_expires_at` are null. `cooldown_expires_at` still reports an active cooldown unless the reason is `feature_unavailable`. A client reads `eligible` and `reason` before rendering any amount or timestamp.
### Structure
@@ -158,7 +158,7 @@ The refund that [refund latest purchase](#refund-latest-purchase) created agains
| subscription_id<sup>3</sup> | ?string | Payment provider subscription that was cancelled along with the refund |
| status<sup>4</sup> | ?string | Provider status of the refund |
<sup>1</sup> Zero until the provider confirms the refund succeeded. Once confirmed it is the amount the provider accepted, which is one minor unit short of `invoice_amount_paid_cents` in the Pix case
<sup>1</sup> Zero until the provider confirms the refund succeeded. Once confirmed it is the amount the provider accepted, which is one minor unit short of `invoice_amount_paid_cents` when the charge was paid with Pix and the invoice amount is greater than one minor unit
<sup>2</sup> Reported as `usd` when the resolved invoice has no currency of its own
@@ -214,7 +214,7 @@ Creates a recurring premium checkout session and returns a [redirect URL](#redir
- An unverified email address returns 403 `PURCHASE_EMAIL_VERIFICATION_REQUIRED`.
- The purchase-disabled premium flag returns 403 `PREMIUM_PURCHASE_BLOCKED` with the reason `purchase_disabled`.
- A lifetime Visionary account cannot buy a recurring subscription and returns 403 `PREMIUM_PURCHASE_BLOCKED` with the reason `lifetime`.
- An account whose payment provider customer already holds a subscription in the `active`, `trialing`, `past_due`, `unpaid`, `incomplete` or `paused` state returns the same code with the reason `existing_subscription`, unless the conversion below applies.
- An account whose payment provider customer already holds a subscription in the `active`, `trialing`, `past_due`, `unpaid`, `incomplete` or `paused` state returns the same code with the reason `existing_subscription`, unless Fluxer converts the purchase into a scheduled billing cycle change, as described below.
`reason` is a top-level member of the error response, and an `existing_subscription` refusal also has the blocking status in the top-level `subscription_status` member.
@@ -279,7 +279,7 @@ Creates a setup mode session that captures and verifies a card before a localise
### Limitations
- The claimed account, verified email and purchase flag requirements of [create subscription checkout](#create-subscription-checkout) apply unchanged, with the same codes.
- An unresolvable country, meaning a request Fluxer cannot geolocate that also omits `country_code`, and a resolved price that is not a recurring price in a currency other than USD and EUR each return 400 `STRIPE_INVALID_PRODUCT_CONFIGURATION`.
- A request that Fluxer cannot geolocate and that omits `country_code` returns 400 `STRIPE_INVALID_PRODUCT_CONFIGURATION`. A resolved price that is not a recurring price in a currency other than USD and EUR also returns 400 `STRIPE_INVALID_PRODUCT_CONFIGURATION`.
- The submitted price is always recurring, so the lifetime block and the existing subscription block both apply and return 403 `PREMIUM_PURCHASE_BLOCKED` with the reason `lifetime` or `existing_subscription`.
### JSON body
@@ -323,7 +323,7 @@ The flow stays pending until [receive Stripe webhook](#receive-stripe-webhook) p
Reports the current state of a preapproval flow and creates the paid checkout session once the card is approved, returning a [localised card preapproval result](#localised-card-preapproval-result-object) object.
The continuation token is the credential, and no credential supplied on the request selects the flow.
The continuation token is the credential. Fluxer selects the flow from the token alone, and a credential supplied on the request selects no flow.
An empty token, an unknown token, and a token whose one-day flow has expired are all reported as `expired`, so the operation never discloses whether a flow exists.
@@ -333,10 +333,10 @@ An empty token, an unknown token, and a token whose one-day flow has expired are
- A recorded price that no longer belongs to the recorded country catalogue returns 400 `STRIPE_INVALID_PRODUCT_CONFIGURATION`.
- The claimed account, verified email, purchase flag, lifetime and existing subscription refusals all apply to the account that opened the flow.
Creating the paid session repeats the complete preparation that [create subscription checkout](#create-subscription-checkout) does, against the account, price and country recorded on the flow.
To create the paid session, Fluxer runs every check of [create subscription checkout](#create-subscription-checkout) and creates a checkout session the same way, using the account, price and country recorded on the flow.
:::caution[Approval is resolved once]
One continuation at a time can resolve an approved flow. While one call creates the paid session, another call for the same flow is reported as `pending`.
Only one call at a time creates the paid session for an approved flow. While one call creates the paid session, another call for the same flow is reported as `pending`.
:::
### JSON body
@@ -360,7 +360,7 @@ One continuation at a time can resolve an approved flow. While one call creates
A `pending`, `rejected` or `expired` result changes nothing. For an approved flow, Fluxer attempts to make the approved card the customer's default invoice payment method, then creates the paid checkout even when that update is unavailable.
It applies the checkout preparation described by [create subscription checkout](#create-subscription-checkout) with the approved price and country. The same token then always resolves to the same paid checkout URL. Entitlement changes only after the matching signed event reaches [receive Stripe webhook](#receive-stripe-webhook).
Fluxer runs the checks of [create subscription checkout](#create-subscription-checkout) and creates the checkout session with the approved price and country. The same token then always resolves to the same paid checkout URL. Entitlement changes only after the matching signed event reaches [receive Stripe webhook](#receive-stripe-webhook).
### Rate limit
@@ -456,7 +456,7 @@ A deployment with no configured payment provider answers with `eligible` false a
A failed payment-history lookup or an account with no payment history reports no refundable purchase.
:::note[The same object appears inside premium state]
[Get premium state](/http-api/premium/#get-premium-state) also returns `billing.refund_eligibility`, which can lag behind this endpoint.
[Get premium state](/http-api/premium/#get-premium-state) also returns `billing.refund_eligibility`, which Fluxer computes from the invoices it has stored. This endpoint reads the invoices from the payment provider, so the premium state value can be older.
:::
### Response
@@ -499,7 +499,7 @@ Once the provider confirms the refund succeeded, the subscription that produced
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [refund](#refund-object) object | The refund was created, or the idempotency key resolved an earlier identical refund |
| 200 | [refund](#refund-object) object | The refund was created, or the request repeats an earlier refund of this invoice and returns that refund |
| 400<sup>1</sup> | [error response](/http-api/#error-response) | The account has no refundable purchase, or the payment provider cannot refund it |
| 403 | [error response](/http-api/#error-response) | The purchase is outside the refund window, or the refund cooldown is active |
| 404 | [error response](/http-api/#error-response) | The authenticated account record no longer exists |
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
A call is the live voice session of a direct message or a group direct message. [Channels](/http-api/channels/) defines the channel itself, its recipient set, and the region list a caller can select.
Joining and leaving a call is a main Gateway operation. [Voice State Update](/gateway/commands/#voice-state-update) requests the placement, and [Call Create](/gateway/events/#call-create), [Call Update](/gateway/events/#call-update), and [Call Delete](/gateway/events/#call-delete) publish the resulting call state.
Joining and leaving a call is a main Gateway operation. A client joins or leaves a call by sending [Voice State Update](/gateway/commands/#voice-state-update), and [Call Create](/gateway/events/#call-create), [Call Update](/gateway/events/#call-update), and [Call Delete](/gateway/events/#call-delete) publish the resulting call state.
Every route on this page is user-only. Bot and OAuth2 credentials are rejected.
@@ -68,7 +68,7 @@ Returns the [call eligibility object](#call-eligibility-object) for a direct mes
<sup>1</sup> A guild channel ID is rejected with 400 `INVALID_CHANNEL_TYPE_FOR_CALL`
A direct message reports `ringable` as false when the other recipient's incoming call policy excludes the caller. That policy is the `incoming_call_flags` bitfield of the recipient's [user settings](/http-api/users/settings/). Fluxer reads the nobody flag, the friends-only flag, an existing friendship, a mutual friend, a mutual guild, and finally the everyone flag, in that order. The silent-everyone flag reports `silent` as true for a caller admitted by the mutual friend, mutual guild, or everyone branch.
A direct message reports `ringable` as false when the other recipient's incoming call policy excludes the caller. That policy is the `incoming_call_flags` bitfield of the recipient's [user settings](/http-api/users/settings/). Fluxer checks the policy in this order. The nobody flag rejects every caller. The friends-only flag admits only a friend. Under any other policy a friend is always admitted. A caller who shares a friend with the recipient is admitted when the friends-of-friends flag is set. A caller who shares a guild with the recipient is admitted when the guild-members flag is set. The everyone flag admits every remaining caller. Any other caller is rejected. The silent-everyone flag reports `silent` as true for a caller admitted by the mutual friend, mutual guild, or everyone branch.
For a recipient who has never stored settings, `ringable` is true and `silent` is false. A direct message that has lost its other recipient reports the same pair. In a group direct message `ringable` is true unless the caller is already connected to its call, and `silent` is always false.
@@ -157,7 +157,7 @@ Starts a direct message or group direct message call, or adds ringing recipients
- The caller must satisfy the [access rules](#access-rules).
- Every explicitly named recipient must be a current recipient other than the caller.
- A direct message also requires the caller to satisfy the direct message send policy against the other recipient.
- A direct message also requires that the caller is allowed to send the other recipient a direct message.
### Path parameters
@@ -74,7 +74,7 @@ An absent field is one this channel type does not own. A field present with `nul
A category owns no `parent_id`. A client that reads the absent key as `null` sees a channel whose parent was cleared.
:::caution[Age and warning fields report this channel alone]
The API enforces the resolved state, so a client must apply the inherited category state before it shows an age restriction or a content warning.
The API enforces the value resolved through this channel, then its parent category, then the guild, as described below. A client must resolve the value in the same order before it shows an age restriction or a content warning.
:::
An age restriction and a content warning both resolve through this channel first, then the parent category, and finally the guild. A category has no parent category and resolves through itself and then the guild.
@@ -221,7 +221,7 @@ Returns the [channel object](#channel-object) visible to the authenticated user.
- A private channel requires current recipient access.
- The personal notes channel requires ownership.
Fluxer enforces age verification only for a guild text, voice, or link channel. A guild category is never gated on it, even when it has the override its children inherit.
Fluxer enforces age verification only for a guild text, voice, or link channel. Fluxer never requires age verification for a guild category, even when the category has the override its children inherit.
### Path parameters
@@ -453,9 +453,9 @@ Fluxer trims a stored nickname, and a value that is null or empty after trimming
A guild channel change emits [Channel Update](/gateway/events/#channel-update) to every session subscribed to its guild. Replacing a category's permission overwrites also replaces them on each child whose overwrites still exactly match the category's previous values, with one Dispatch per changed child. A child whose overwrites had differed is left unchanged.
Changing `rate_limit_per_user` clears the channel's current slowmode state so the new interval applies immediately. Changing `rtc_region` on a guild voice channel moves its active voice connections to the selected region.
Changing `rate_limit_per_user` clears the remaining slowmode delay of every user in the channel, so each user's next message is checked against the new interval. Changing `rtc_region` on a guild voice channel moves its active voice connections to the selected region.
A request that changes at least one field creates a channel update audit log entry with no reason. Supplying `permission_overwrites` also creates the overwrite audit log entries the change implies.
A request that changes at least one field creates a channel update audit log entry with no reason. Supplying `permission_overwrites` also creates one overwrite create, update, or delete audit log entry for each overwrite the array adds, changes, or removes.
A group direct message change emits [Channel Update](/gateway/events/#channel-update) to every current recipient. A name or successful icon change creates a system message delivered with [Message Create](/gateway/events/#message-create), and replacing the icon permanently deletes the previous one. Ownership and nickname changes create no system message and no audit entry.
@@ -542,7 +542,7 @@ An account that stores neither a password nor a second factor is verified withou
Deleting a guild category first clears the `parent_id` of every child channel and emits [Channel Update](/gateway/events/#channel-update) for each one. The category itself is then deleted like any other guild channel.
Deleting a guild channel permanently removes its messages, attachments, invites and webhooks. Guild subscribers receive [Channel Delete](/gateway/events/#channel-delete), and the deletion appears in the audit log. A system, rules or AFK channel reference is cleared with [Guild Update](/gateway/events/#guild-update).
Deleting a guild channel permanently removes its messages, attachments, invites and webhooks. Guild subscribers receive [Channel Delete](/gateway/events/#channel-delete), and the deletion appears in the audit log. When the deleted channel is the guild's system, rules or AFK channel, Fluxer clears that guild setting and emits [Guild Update](/gateway/events/#guild-update).
Closing a direct message marks the channel closed for the caller alone and emits [Channel Delete](/gateway/events/#channel-delete) to that caller. The channel, its messages, and the other recipient's view are untouched.
@@ -600,7 +600,7 @@ Fluxer evaluates the target's admission policy in this order.
| Status | Body | Condition |
| --- | --- | --- |
| 204 | empty | Recipient was added, or was already a recipient |
| 400 | [error response](/http-api/#error-response) | CAPTCHA proof is missing or rejected, the caller and target are not friends and the request returns `NOT_FRIENDS_WITH_USER` |
| 400 | [error response](/http-api/#error-response) | CAPTCHA proof is missing or rejected, or the caller and target are not friends and the request returns `NOT_FRIENDS_WITH_USER` |
| 400 | [error response](/http-api/#error-response) | The group is already full and the request returns `MAX_GROUP_DM_RECIPIENTS` |
| 400 | [error response](/http-api/#error-response) | The channel is not a group direct message and the request returns `INVALID_CHANNEL_TYPE` |
| 403 | [error response](/http-api/#error-response) | Caller is not a recipient, or the target's admission policy rejects the caller, each returning `MISSING_ACCESS` |
@@ -735,7 +735,7 @@ A larger decimal string returns 400 `INVALID_FORM_BODY` with the code `INTEGER_O
### Side effects
Fluxer emits [Channel Update](/gateway/events/#channel-update) to every session subscribed to the guild, including when the submitted overwrite exactly matches the stored one. When the channel is a category, every child whose overwrites exactly matched the category's previous values receives the same replacement and its own Channel Update.
Fluxer emits [Channel Update](/gateway/events/#channel-update) to every session subscribed to the guild, including when the submitted overwrite exactly matches the stored one. When the channel is a category, every child whose overwrites exactly matched the category's previous values receives a copy of the category's new overwrites and its own Channel Update.
The operation records an overwrite create or update audit entry with no reason. An unchanged overwrite records no audit entry.
@@ -774,7 +774,7 @@ The operation is idempotent, does not require `VIEW_CHANNEL`, and never returns
### Side effects
Fluxer emits [Channel Update](/gateway/events/#channel-update) to every session subscribed to the guild, including when the identifier names no existing overwrite. Removing an overwrite that exists also records an overwrite delete audit entry with the previous overwrite state and no reason. An identifier that names no existing overwrite records no audit entry. When the target channel is a category, propagation follows the same exact-match rule as overwrite replacement.
Fluxer emits [Channel Update](/gateway/events/#channel-update) to every session subscribed to the guild, including when the identifier names no existing overwrite. Removing an overwrite that exists also records an overwrite delete audit entry with the previous overwrite state and no reason. An identifier that names no existing overwrite records no audit entry. When the target channel is a category, each child whose overwrites exactly matched the category's previous overwrites receives a copy of the category's new overwrites and its own Channel Update.
### Rate limit
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
A connection links a Fluxer account to a verified domain or Bluesky account. Visible connections appear in `connected_accounts` on the [full user profile object](/http-api/users/#full-user-profile-object).
Connection routes require a user token. Bot tokens and OAuth2 bearers receive 403 `ACCESS_DENIED`, except that [List connections](#list-connections) accepts a bearer with the `connections` [scope](/http-api/oauth2/#oauth2-scopes). Accounts with an outstanding required action receive 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. Connection IDs are not snowflakes.
Connection routes require a user token. Bot tokens and OAuth2 bearers receive 403 `ACCESS_DENIED`, except that [List connections](#list-connections) accepts a bearer with the `connections` [scope](/http-api/oauth2/#oauth2-scopes). Accounts with an outstanding [required action](/http-api/users/#required-actions) receive 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. Connection IDs are not snowflakes.
[Bluesky client metadata](#get-bluesky-client-metadata) and [JWKS](#get-bluesky-jwks) are public and contain no account data.
@@ -37,7 +37,7 @@ The pair of [type](#connection-types) and `id` identifies a connection. Both val
<sup>2</sup> For a `domain` connection the name is the submitted domain. For a `bsky` connection it is the Bluesky handle, refreshed by completing the [authorisation flow](#start-bluesky-authorisation)
<sup>3</sup> Starts true. A failed ownership recheck sets it to false
<sup>3</sup> Fluxer sets it to true when the proof succeeds and again each time a Bluesky authorisation completes. No later check sets it to false
<sup>4</sup> The stored value is the integer the client supplied and is not masked against the defined bits
@@ -116,7 +116,7 @@ Returned by [Start Bluesky authorisation](#start-bluesky-authorisation).
| Field | Type | Description |
| --- | --- | --- |
| authorize_url | string | URL to open for authorisation. Its origin depends on the account |
| authorize_url | string | URL to open for authorisation. Its origin depends on the submitted `handle` |
## List connections
@@ -317,7 +317,7 @@ Assigns the display order of the listed connections from their position in the a
Each named connection receives its zero-based array index as `sort_order`. The complete list is sent to the caller's sessions in [User Connections Update](/gateway/events/#user-connections-update), even if the order did not change.
The reorder is atomic. A rejected write changes no order and emits no event. A later notification failure can return an error after the order has been saved.
The reorder is atomic. A request that returns 409 `CONFLICT` changes no order and emits no event. When Fluxer saves the order and then fails to send [User Connections Update](/gateway/events/#user-connections-update), the request returns an error and the new order stays saved.
A partial array can leave two connections sharing a `sort_order`, which [List connections](#list-connections) resolves by stored order.
@@ -4,7 +4,7 @@ title: Deployment availability
description: The hosted-only routes and the instance flags that report deployment kind.
---
A small set of routes exists only on the deployment Fluxer hosts. An operator runs the same release, and most of the HTTP API is identical on both.
The routes listed under [Hosted-only routes](#hosted-only-routes) exist only on the deployment Fluxer hosts. A self-hosted deployment runs the same release, and most of the HTTP API is identical on both.
A self-hosted deployment does not register those routes. A request to one returns 404 `NOT_FOUND` with no feature-specific code, so a caller cannot tell an unavailable route from an unrecognised path.
@@ -14,13 +14,13 @@ Credentials, permissions, premium state and OAuth2 scopes do not change route av
Every deployment reports its kind in `self_hosted` on the [instance features object](/http-api/instance/#instance-features-object). The unauthenticated [instance discovery document](/http-api/instance/#get-instance-discovery) publishes it before a client holds any credential. `self_hosted` alone decides whether the API registers the routes below.
`stripe_enabled` on the same object reports the payment provider toggle alone. A hosted deployment that reports it false still serves every route in the table below. A deployment reporting `stripe_enabled` true with no provider secret key configured behaves exactly like one reporting it false.
`stripe_enabled` on the same object reports only the `integrations.stripe.enabled` configuration value. A hosted deployment that reports it false still serves every route in the table below. A deployment reporting `stripe_enabled` true with no provider secret key configured behaves exactly like one reporting it false.
:::caution[Read `self_hosted` for the deployment kind]
Neither flag promises that a provider-dependent operation succeeds.
:::
Without a provider client, the answer depends on the operation. An operation that has to reach the provider fails with 400 `STRIPE_PAYMENT_NOT_AVAILABLE`, and [Receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) fails with 400 `STRIPE_WEBHOOK_NOT_AVAILABLE`. These read operations report the absence in a 200 body instead:
When `stripe_enabled` is false or no provider secret key is configured, the answer depends on the operation. An operation that has to reach the provider fails with 400 `STRIPE_PAYMENT_NOT_AVAILABLE`, and [Receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) fails with 400 `STRIPE_WEBHOOK_NOT_AVAILABLE`. These read operations report the absence in a 200 body instead:
- [Get refund eligibility](/http-api/billing/#get-refund-eligibility) reports `eligible` false with the reason `feature_unavailable`.
- [Get current subscription price](/http-api/premium/#get-current-subscription-price) reports null.
@@ -68,4 +68,4 @@ A route every deployment registers can still produce a different answer on a sel
Premium state is the clearest case. Every deployment registers [Get premium state](/http-api/premium/#get-premium-state) and [Set premium perks disabled](/http-api/premium/#set-premium-perks-disabled). A self-hosted instance still reports premium state and still records the perks-disabled flag. The response repeats the deployment kind in `self_hosted` on the [effective premium state object](/http-api/premium/#effective-premium-state-object). That flag alone does not make `is_premium` true. A self-hosted deployment grants premium to every account only while its instance [premium mode](/admin-api/instance/#premium-modes) is `everyone`. That mode also overrides the perks-disabled flag, so `is_premium` stays true while `premium_perks_disabled` is true.
The other instance feature flags published by [instance discovery](/http-api/instance/#instance-features-object) work the same way. `voice_enabled`, `presigned_attachment_uploads`, and `emails_enabled` each switch off a capability that the surrounding routes still expose, so a client reads the flag.
The other instance feature flags published by [instance discovery](/http-api/instance/#instance-features-object) work the same way. `voice_enabled`, `presigned_attachment_uploads`, and `emails_enabled` each report whether a capability is switched on. The routes for that capability stay registered when the flag is false, so a client reads the flag before it uses them.
@@ -82,7 +82,7 @@ One page of matching listings, together with the total and the per-category coun
| total<sup>1</sup> | integer | The total number of guilds matching the query |
| category_counts<sup>2</sup> | array[[discovery category count](#discovery-category-count-object) object] | The match count for each category under the current filters |
<sup>1</sup> The count describes the complete match set, so it bounds paging through `offset`
<sup>1</sup> The count covers every match on every page. A client pages with `offset` until `offset` reaches `total`
<sup>2</sup> Computed with the `category` filter removed and every other filter applied, so the counts describe what selecting a different category would return. A category with no match is omitted, and the array is ordered by ascending category
@@ -358,7 +358,7 @@ A pending, rejected, removed, or absent application fails with 400 `DISCOVERY_NO
| 403 | [error response](/http-api/#error-response) | The caller is banned from the guild directly or by address and the request returns `USER_BANNED_FROM_GUILD` or `USER_IP_BANNED_FROM_GUILD` |
| 404 | [error response](/http-api/#error-response) | The approved application names a guild whose record no longer exists and the request returns `UNKNOWN_GUILD` |
<sup>1</sup> An account whose phone requirement was deferred is re-evaluated against the target guild at join time, so an account that satisfies the standing check can still be refused here
<sup>1</sup> An account without a verified phone number can have a phone requirement that Fluxer holds back until the account joins a guild. At join time Fluxer checks that requirement against the target guild. An account that passes the account-wide check can therefore still receive `ACCOUNT_SUSPICIOUS_ACTIVITY` here
:::note[A guild that never existed returns 400, not 404]
A guild with no approved listing returns `DISCOVERY_NOT_DISCOVERABLE`.
@@ -368,9 +368,9 @@ A guild with no approved listing returns `DISCOVERY_NOT_DISCOVERABLE`.
An account that is already a member receives the same 204 response with no Dispatch. Otherwise the operation creates the membership, records discovery as its join source, and adds the guild to the caller's settings and folder layout.
The joining account's sessions receive [Guild Create](/gateway/events/#guild-create). [Guild Member Add](/gateway/events/#guild-member-add) is dispatched guild-wide and reaches whichever sessions [event filtering](/gateway/event-filtering/) selects. A bot session always receives it, and a passive user session in a guild with more than 250 members does not receive it until [Lazy Request](/gateway/commands/#lazy-request) marks that guild active. The joining account receives [User Settings Update](/gateway/events/#user-settings-update) when the join changes its restricted guild set or its folder layout, and [User Guild Settings Update](/gateway/events/#user-guild-settings-update) when its account default hides muted channels.
The joining account's sessions receive [Guild Create](/gateway/events/#guild-create). [Guild Member Add](/gateway/events/#guild-member-add) is dispatched guild-wide and reaches whichever sessions [event filtering](/gateway/event-filtering/) selects. A bot session always receives it, and a passive user session in a guild with more than 250 members does not receive it until [Lazy Request](/gateway/commands/#lazy-request) marks that guild active. The joining account receives [User Settings Update](/gateway/events/#user-settings-update) when the join adds the guild to `restricted_guilds` in its [user settings](/http-api/users/settings/) or changes its folder layout, and [User Guild Settings Update](/gateway/events/#user-guild-settings-update) when its account default hides muted channels.
Unless join notifications are suppressed or no system channel is configured, the join emits a [USER_JOIN](/http-api/messages/#message-types) system message through [Message Create](/gateway/events/#message-create). No invite use is consumed.
Unless the guild sets the `SUPPRESS_JOIN_NOTIFICATIONS` [system channel flag](/http-api/guilds/#system-channel-flags) or has no system channel, the join emits a [USER_JOIN](/http-api/messages/#message-types) system message through [Message Create](/gateway/events/#message-create). No invite use is consumed.
### Rate limit
@@ -483,7 +483,7 @@ Every field is optional, and an omitted field preserves the stored value.
### Side effects
Editing preserves the listing's status and review details. Approved changes appear in [Search discovery guilds](#search-discovery-guilds), while pending listings remain absent. No guild feature changes.
Editing preserves the listing's status and review details. Changes to an approved listing appear in [Search discovery guilds](#search-discovery-guilds). A pending listing stays absent from search. No guild feature changes.
### Rate limit
@@ -8,12 +8,12 @@ import RouteHeader from '@/components/RouteHeader.astro';
A donation is a one-off or recurring payment, taken on an externally hosted checkout page. Fluxer tracks it by email address, and it grants no [premium](/http-api/premium/). [Billing](/http-api/billing/) defines the payment provider webhook that completes one.
None of the routes here takes a credential, and all are hosted-only, as [deployment availability](/http-api/deployment-availability/) describes. A valid credential presented anyway keys the rate limit to the account.
None of the routes here takes a credential, and all are hosted-only, as [deployment availability](/http-api/deployment-availability/) describes. When a request presents a valid credential anyway, Fluxer counts the request against that account's rate limit.
Fluxer uses a submitted address exactly as written and matches a donor by exact equality, so `[email protected]` and `[email protected]` address two different donors.
:::note[Donation management requires a completed donation]
Donation management becomes available after payment is confirmed.
Fluxer stores a donor only when the payment provider confirms a donation checkout. [Request donation management link](#request-donation-management-link) sends a link only to an address stored as a donor.
:::
## Donation currencies
@@ -96,7 +96,7 @@ An address that resolves to no donor receives no email.
### Side effects
An eligible donor receives an email. The 204 response does not guarantee delivery.
An address that resolves to a donor receives an email with the management link. The 204 response does not guarantee delivery.
Fluxer creates a single-use token of 64 lowercase hexadecimal characters, valid for 15 minutes, only when the address resolves to a donor. Issuing a new link deletes every earlier token for the same address, and a replaced link returns 400 `DONATION_MAGIC_LINK_INVALID`.
@@ -176,7 +176,7 @@ A recurring donation for an address with an active recurring donation returns th
Checkout opens on the provider's pages for the submitted amount, currency and interval. Completion returns the donor to the public donation success page, while cancellation returns to the public donation page.
Payment confirmation sends an email and enables [Request donation management link](#request-donation-management-link).
When the payment provider confirms the payment, Fluxer stores the address as a donor and sends a donation confirmation email to it. From then on, [Request donation management link](#request-donation-management-link) sends a link to that address.
### Rate limit
@@ -12,7 +12,7 @@ The download resource serves the desktop application builds a deployment has sto
Fluxer registers the routes under `/dl`, and the desktop routes under `/dl/desktop`. A client builds the URL against `api_client` from the [instance endpoints object](/http-api/instance/#instance-endpoints-object).
The prefix mounts at the root and at `/v1`, so `/v1/dl/desktop/...` resolves for the routes that name their own segments.
Fluxer mounts the prefix at the root and at `/v1`. Every desktop route below also answers under `/v1/dl/desktop/...`.
:::caution[The catch-all has no `/v1` form]
[Download stored object](#download-stored-object) requires the `/dl` prefix. Its `/v1/dl/...` equivalent returns 404.
@@ -173,19 +173,19 @@ A release feed filename is any of these:
The deployment settings below answer a download with 302.
A hosted deployment can redirect downloads from selected countries to GitHub release assets. Responses under this setting have `Cache-Control: private, no-store`.
A hosted deployment can list countries whose downloads of a file under `desktop/` Fluxer can redirect to GitHub release assets. Once the list is set, every response for such a file has `Cache-Control: private, no-store`, including a response Fluxer serves from storage.
A deployment that issues presigned download URLs answers a `GET` with 302 to a storage URL valid for 900 seconds. That redirect has `Cache-Control: no-store` and `Accept-Ranges: bytes`. The setting is off by default.
:::caution[A 302 has no bytes and no range]
The client follows `Location` to read the file. Fluxer produces the redirect before it reads any `Range` header, so range handling on a redirected download belongs to the target.
The client follows `Location` to read the file. Fluxer produces the redirect before it reads any `Range` header, so the server at `Location` answers any `Range` header on a redirected download.
:::
## Test builds
Every route accepts the `test` query parameter. `1` or `true`, matched without regard to case, selects test releases. Any other value is false.
On the desktop routes the flag replaces the prefix outright. On [Download stored object](#download-stored-object) it rewrites a key beginning `desktop/` and leaves any other key unchanged, and a path that already names `desktop-test/` resolves there with no flag at all.
On the desktop routes the flag makes Fluxer resolve every file under `desktop-test/`. On [Download stored object](#download-stored-object) it rewrites a key beginning `desktop/` and leaves any other key unchanged, and a path that already names `desktop-test/` resolves there with no flag at all.
A `url` and a `checksum_url` built for a request that sent the flag repeat `?test=1`, so a client following either one stays on the test prefix.
@@ -312,7 +312,7 @@ Streams the newest file at the coordinate for one package format.
| 404 | `Not Found` | No file resolved for the format at the coordinate |
| 416 | empty | The requested range is unsatisfiable |
The stored file keeps its own name, so the resolved filename has the release version even though the request named `latest`. A client that follows this route on every check reads a different file once the coordinate publishes a new release.
The stored file keeps its own name, so the resolved filename has the release version even though the request named `latest`. A client that follows this route on every check reads a different file once a newer release is stored at the coordinate.
### Response headers
@@ -493,7 +493,7 @@ A path outside `desktop/` or `desktop-test/` returns 404.
Fluxer also rejects the key when it is empty, when normalisation leaves it beginning `..` or `/`, or when any segment is `.`, `..`, or contains a NUL character. Each of those returns the same 404, so a caller cannot tell a traversal attempt from a missing object.
Both `desktop/stable/linux-x64/manifest.json` and `desktop/stable/linux/x64/manifest.json` path forms are supported.
A request can name `desktop/stable/linux-x64/manifest.json` or `desktop/stable/linux/x64/manifest.json`. For the hyphen form, Fluxer returns that object when storage holds it, and otherwise returns the object at the slash form.
### Response headers
@@ -26,7 +26,7 @@ Every bound below is a fixed constant of the instance, and none is resolved from
<sup>2</sup> Only [Upload entrance sound](#upload-entrance-sound) applies the lower bound
The [instance discovery](/http-api/instance/#limit-keys) document publishes a `feature_voice_entrance_sounds` limit key. That key gates the feature in the client. No route on this page checks it, so a library can be read, written, and played back regardless of its resolved value.
The [instance discovery](/http-api/instance/#limit-keys) document publishes a `feature_voice_entrance_sounds` limit key. The Fluxer client reads that key to decide whether it opens the clip upload dialog and whether it calls [Play entrance sound](#play-entrance-sound) after it connects to a voice channel. No route on this page checks it, so a library can be read, written, and played back regardless of its resolved value.
## Supported containers
@@ -316,7 +316,7 @@ Fluxer writes or removes the scope's selection. One clip can be selected in any
<RouteHeader method="POST" path="/v1/voice/channels/{channel_id}/entrance-sound" />
Fans the caller's chosen clip out to everyone else connected to a voice channel and returns 204 with an empty body. The caller must already hold a voice state in that channel and must own the clip. Emits an [ENTRANCE_SOUND_PLAY](/gateway/events/#entrance-sound-play) Gateway event.
Tells every other account connected to a voice channel to play the caller's chosen clip and returns 204 with an empty body. The caller must already hold a voice state in that channel and must own the clip. Emits an [ENTRANCE_SOUND_PLAY](/gateway/events/#entrance-sound-play) Gateway event.
Recipients fetch and play the clip locally from the URL in the event.
@@ -336,7 +336,7 @@ Recipients fetch and play the clip locally from the URL in the event.
| Status | Body | Condition |
| --- | --- | --- |
| 204 | empty | Fan-out was attempted for every other connected account |
| 204 | empty | Fluxer attempted a Dispatch to every other connected account |
| 400 | [error response](/http-api/#error-response) | Caller holds no voice state in the channel, including when the channel does not exist, and the request returns `ENTRANCE_SOUND_INVALID_SCOPE` at the `channel_id` path, or the account owns no such clip and the request returns `ENTRANCE_SOUND_NOT_FOUND` at the `sound_id` path |
:::caution[Connection is the only authorisation]
@@ -68,7 +68,7 @@ A validation failure whose elements have enumerated codes answers 400 with its e
### Default schema failure codes
A boundary schema constraint can name its own [validation code](#validation-error-code-registry). When it names none, Fluxer maps the failure to one of the codes below by the kind of constraint that failed.
A constraint in a route's request schema can name its own [validation code](#validation-error-code-registry). When it names none, Fluxer maps the failure to one of the codes below by the kind of constraint that failed.
| Constraint | Code | Description |
| --- | --- | --- |
@@ -110,7 +110,7 @@ Fluxer answers an unrecognised failure with 500 `INTERNAL_SERVER_ERROR` and a ge
## Client errors as an abuse signal
Repeated invalid requests or credentials can trigger a temporary IP ban. A `4xx` answer to a request with no authenticated user adds to that signal, weighted by status. A 429 weighs 3, a 401 weighs 0.75, a 403 weighs 0.5, and every other 4xx weighs 0.25. One request adds at most one signal, and a request from a private or exempt address adds none. Stop using a rejected credential and respect rate-limit responses instead of retrying unchanged requests.
Repeated invalid requests or credentials can trigger a temporary IP ban. A `4xx` answer to a request with no authenticated user adds to that signal, weighted by status. A 429 weighs 3, a 401 weighs 0.75, a 403 weighs 0.5, and every other 4xx weighs 0.25. One request adds at most one signal, and a request from a private or exempt address adds none. Stop using a rejected credential. Change a rejected request before sending it again, and after a 429 wait `retry_after` before the next attempt.
:::caution[An automatic ban answers every request for 24 hours]
A temporary ban lasts 24 hours by default. Requests from the banned address return 403 `GLOBAL_IP_TEMPORARILY_BANNED`. Use `expires_at` from the response when available.
@@ -6,7 +6,7 @@ description: The experiment assignments envelope, the revalidation and polling c
import RouteHeader from '@/components/RouteHeader.astro';
An experiment is one instance-wide rollout the operator configures and Fluxer resolves against one account. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. `voice_noise_suppression` is the only experiment defined today, and [Voice](/voice/) defines the placement protocol its assignment applies to.
An experiment is an instance-wide rollout that an operator configures. For each account, Fluxer works out from that configuration whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. `voice_noise_suppression` is the only experiment defined today, and [Voice](/voice/) defines the placement protocol its assignment applies to.
Every assignment is advice. A client that ignores one behaves as it does with the rollout off, and no route and no Gateway event reports what a client actually ran.
@@ -19,10 +19,10 @@ One resolution of every defined experiment against one account. Every field is p
| Field | Type | Description |
| --- | --- | --- |
| poll_interval_seconds | integer | Seconds to wait before revalidating, from 60 through 86400 |
| poll_jitter_percent | integer | How far to spread the wait around the interval, from 0 through 50 |
| poll_jitter_percent | integer | The largest random offset added to or subtracted from the wait, as a percentage of the wait, from 0 through 50 |
| assignments | [assignment map](#assignment-map-object) object | One entry for each experiment the server defines |
The polling fields apply to every experiment and account on the instance.
Every account on the instance receives the same `poll_interval_seconds` and `poll_jitter_percent`. One request refreshes every experiment.
## Assignment map object
@@ -36,7 +36,7 @@ One entry per experiment. The envelope reports this object even when it is empty
Ignore unknown experiments and treat a missing experiment as off.
This server version writes `voice_noise_suppression` on every response, including while the rollout is disabled. The disabled value is the first [resolution outcome](#resolution-outcomes) below, which reports `enabled` false and the stored `config_version`, so a client can tell an operator write from a no-op without a second request.
This server version writes `voice_noise_suppression` on every response, including while the rollout is disabled. The disabled value is the first [resolution outcome](#resolution-outcomes) below, which reports `enabled` false and the stored `config_version`, so a client that compares `config_version` with the value from its previous response can see that an operator saved the noise suppression configuration, even while the rollout stays disabled, and needs no second request for it.
## Noise suppression backends
@@ -91,7 +91,7 @@ A client branches on `user_targeted` rather than on `enabled_backends`, because
## Noise suppression guild override object
One backend replacement scoped to one guild. A guild named here replaces `backend` while the caller is connected to a voice channel of that guild.
One backend replacement scoped to one guild. While the caller is connected to a voice channel of that guild, the override's `backend` replaces the assignment's `backend`.
### Structure
@@ -22,7 +22,7 @@ Every field is always present.
| shards | integer | Recommended shard count, always `1` |
| session_start_limit | [session start limit](#session-start-limit-object) object | Fixed session start values |
Fluxer appends no query string, so a client appends the [connection parameters](/gateway/overview/#connection-parameters) itself. A client MUST NOT upgrade a published `ws` value, because a deliberately plain HTTP deployment advertises one.
Fluxer appends no query string, so a client appends the [connection parameters](/gateway/overview/#connection-parameters) itself. A client MUST NOT rewrite a published `ws` value to `wss`, because a deployment that deliberately serves plain HTTP advertises a `ws` value.
### Example
@@ -56,7 +56,7 @@ These are fixed compatibility values, not live usage counters.
<sup>1</sup> A bot does not need to pace Identify requests against this value
The limits the Gateway enforces are in [Session lifecycle](/gateway/limits-and-rate-limits/#session-lifecycle). Fluxer budgets Identify per source address and caps a user account at a fixed number of concurrent sessions. Neither bound is reported here.
The limits the Gateway enforces are in [Session lifecycle](/gateway/limits-and-rate-limits/#session-lifecycle). Fluxer rate limits Identify per source address and limits a user account to a fixed number of concurrent sessions. Neither bound is reported here.
:::caution[A remaining session start does not guarantee admission]
These values reserve no capacity. [Session admission](/gateway/limits-and-rate-limits/#session-lifecycle) can still hold or reject a connection.
@@ -76,7 +76,7 @@ The route checks the shape of the credential only. A well-formed value naming no
Validate a token with [Identify](/gateway/commands/#identify) or [Get bot application](/http-api/applications/#get-bot-application) instead.
A 200 has the informational [rate limit headers](/topics/rate-limits/#rate-limit-headers) only for the `Bot ` prefix. Fluxer keys the bare form and the `Bearer ` form on the client IP address.
A 200 has the informational [rate limit headers](/topics/rate-limits/#rate-limit-headers) only for the `Bot ` prefix. For the bare form and the `Bearer ` form, Fluxer keys the rate limit bucket on the client IP address.
### Response
@@ -12,7 +12,7 @@ Every route requires a session credential. A bot token and an OAuth2 bearer cred
## Provider availability
`klipy` is the only provider name a deployment binds. [Get instance discovery](/http-api/instance/#get-instance-discovery) publishes it as the [GIF provider](/http-api/instance/#gif-provider-object) object. An instance that has bound no provider key refuses every route on this page with 403 `FEATURE_TEMPORARILY_DISABLED`. That same key drives `gif_enabled` in [service availability](/http-api/instance/#service-availability-object), a flag an operator can also set by hand, so a client that reads `gif_enabled` as true can still receive the 403.
`klipy` is the only provider name a deployment binds. [Get instance discovery](/http-api/instance/#get-instance-discovery) publishes it as the [GIF provider](/http-api/instance/#gif-provider-object) object. An instance that has bound no provider key refuses every route on this page with 403 `FEATURE_TEMPORARILY_DISABLED`. `gif_enabled` in [service availability](/http-api/instance/#service-availability-object) reports whether that key is bound, unless an operator has set the flag by hand. A client that reads `gif_enabled` as true can therefore still receive the 403.
A provider outage, a request past its deadline, and an unreadable provider payload all return 503 `SERVICE_UNAVAILABLE`. The deadline is 12 seconds everywhere except [Register a GIF share](#register-a-gif-share), which uses 3 seconds. An operator can change both.
@@ -36,7 +36,7 @@ Every response under `/gifs`, `/tenor`, and `/klipy` has these headers, includin
## Result freshness
Results may be cached and can lag behind the provider. Repeating a request does not force a refresh.
Fluxer caches results, so a result can lag behind the provider. Repeating a request does not force a refresh.
## GIF object
@@ -62,7 +62,7 @@ A GIF object is one media item the active provider owns. Every URL in it resolve
<sup>2</sup> Copied from the `webm` entry of `media` when the provider returned one, and from the first entry it returned otherwise
<sup>3</sup> Empty when no format the provider returned could be proxied. No key is guaranteed, so a client walks a priority list
<sup>3</sup> Empty when no format the provider returned could be proxied. No key is guaranteed, so a client checks the format names it prefers in order and uses the first one present
<sup>4</sup> The active provider emits none, so the field is absent from every GIF these routes return. [Resolve GIF URLs](/http-api/memes/#resolve-gif-urls) is the operation that produces one
@@ -16,7 +16,7 @@ A gift records no recipient, so Fluxer binds the code to whichever eligible acco
## Gift object
A gift records a duration. Fluxer computes the entitlement window at redemption time from the redeemer's existing state.
A gift records a duration. Fluxer computes the entitlement window at redemption time. For a positive quantity, the entitlement anchor is the latest of the current time, the redeemer's current premium end and their existing gift extension end.
Both creation paths record a creator. A completed gift checkout records the purchaser. An Admin API gift records the system account with ID `0`, and no field names the administrator that requested it. No operation unredeems a code.
@@ -134,7 +134,7 @@ One redemption can be in flight for a code across the whole deployment, and a se
| X-Captcha-Token?<sup>1</sup> | string | The proof issued by the CAPTCHA provider |
| X-Captcha-Type?<sup>2</sup> | string | The CAPTCHA provider, either `hcaptcha` or `turnstile` |
<sup>1</sup> A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`. Verification is skipped when CAPTCHA is disabled, when the account has the exemption flag, or when the caller's contact has the exemption capability, as described by [CAPTCHA handling](/topics/captcha/)
<sup>1</sup> A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`. Verification is skipped when CAPTCHA is disabled, when the account has the exemption flag, or when the account's email address has the `captcha_exempt` account policy capability, as described by [CAPTCHA handling](/topics/captcha/)
<sup>2</sup> Any other value, including an omitted header, falls back to the instance's configured provider
@@ -143,7 +143,7 @@ One redemption can be in flight for a code across the whole deployment, and a se
| Status | Body | Condition |
| --- | --- | --- |
| 204 | empty | The gift was redeemed and the entitlement was applied |
| 400 | [error response](/http-api/#error-response) | CAPTCHA failed, the code is redeemed or a redemption is in flight, the account is unclaimed or holds lifetime entitlement, the payment provider rejected the subscription work, or the Visionary guild join for a lifetime gift failed |
| 400 | [error response](/http-api/#error-response) | CAPTCHA failed, the code is redeemed or a redemption is in flight, the account is unclaimed or holds lifetime entitlement, the payment provider rejected the change to the active subscription, or the Visionary guild join for a lifetime gift failed |
| 403 | [error response](/http-api/#error-response) | The email address is unverified, or purchases are disabled for the account |
| 404 | [error response](/http-api/#error-response) | No gift exists for the code, the gift was revoked, the authenticated account record no longer exists, or the configured Visionary guild does not exist |
@@ -151,9 +151,9 @@ One redemption can be in flight for a code across the whole deployment, and a se
The gift becomes redeemed, so [Get gift](#get-gift) reports `redeemed` as true and [List current user gifts](/http-api/users/gifts/#list-current-user-gifts) shows the redemption time and the redeemer to the buyer.
A positive quantity extends premium from the latest of the current time, the current premium end and the existing gift extension end. An eligible active subscription also has its trial or billing period extended without proration. A provider failure can return 400 `STRIPE_ERROR` and leave the code unredeemed.
A positive quantity extends premium from the latest of the current time, the current premium end and the existing gift extension end. When the account has a subscription with the payment provider and its subscription premium has not ended, Fluxer also extends that subscription's trial or billing period without proration. A provider failure can return 400 `STRIPE_ERROR` and leave the code unredeemed.
A quantity of `0` grants lifetime Visionary premium and immediately cancels an active subscription without proration or a final invoice. It can also assign a Visionary sequence and join the [Visionary guild](/http-api/premium/#rejoin-visionary-guild).
A quantity of `0` grants lifetime Visionary premium and immediately cancels an active subscription without proration or a final invoice. When the gift record has a Visionary sequence number, the redeemer receives that number as their lifetime Visionary sequence and joins the [Visionary guild](/http-api/premium/#rejoin-visionary-guild).
A failed Visionary guild join leaves the gift unredeemed. Guild limits return 400 `MAX_GUILDS` or `MAX_GUILD_MEMBERS`. Any subscription cancellation already completed is not reversed.
@@ -10,7 +10,7 @@ An audit log entry records one change made to a guild and the account that made
## Audit log reason
An audit-capable route accepts the `X-Audit-Log-Reason` request header, listed with the other [standard request headers](/http-api/#standard-request-headers). Fluxer reads the value verbatim and never percent-decodes it, so a caller that percent-encodes the reason stores and reads back the percent-encoded form.
A route marked Audit reason accepts the `X-Audit-Log-Reason` request header, listed with the other [standard request headers](/http-api/#standard-request-headers). Fluxer reads the value verbatim and never percent-decodes it, so a caller that percent-encodes the reason stores and reads back the percent-encoded form.
Fluxer trims the value. A value that is blank before trimming, empty after it, or longer than 512 characters after it counts as no reason at all. None of those values fails the request, so a reason that misses the bound is dropped and the operation still succeeds.
@@ -197,7 +197,7 @@ Fluxer serialises a permission mask or a snowflake as a decimal string. A bounde
:::
:::caution[A change list holds only the differing fields]
An update records one change object per differing field, so an unchanged field is absent. An identity field never appears in an update change list, though it does appear in a creation or deletion list, where only one side of the comparison exists.
An update records one change object per differing field, so an unchanged field is absent. The field that holds the entity's own ID, such as `channel_id` or `role_id`, never appears in an update change list, though it does appear in a creation or deletion list, where only one side of the comparison exists.
:::
### Change fields
@@ -44,7 +44,7 @@ The initial overwrite collection supplied when a channel is created. The stored
A larger decimal string returns 400 `INVALID_FORM_BODY` with the code `INTEGER_OUT_OF_INT64_RANGE`. A JSON number outside the safe integer range returns `INVALID_INTEGER_FORMAT`, and so does a string that is not all digits.
An undefined bit never fails the authority comparison described by [Create guild channel](#create-guild-channel). It is stored as supplied and echoed back by every later read of the channel.
A bit that names no defined permission never fails the authority comparison described by [Create guild channel](#create-guild-channel). It is stored as supplied and echoed back by every later read of the channel.
A bit set in both masks resolves to an allow during [permission computation](/http-api/permissions/#permission-computation).
@@ -73,7 +73,7 @@ One entry of the bulk hierarchy update. Entries are applied in array order, each
| preceding_sibling_id?<sup>3</sup> | ?snowflake | Sibling that sits directly before this channel, or null to place it first |
| lock_permissions?<sup>4</sup> | boolean | Whether to copy the destination category's overwrites onto the moved channel (default false) |
<sup>1</sup> Read only when `preceding_sibling_id` is omitted. Sending `preceding_sibling_id` at all, including as null, makes it inert
<sup>1</sup> Read only when `preceding_sibling_id` is omitted. When `preceding_sibling_id` is sent, including as null, Fluxer ignores `position`
<sup>2</sup> An omitted field keeps the channel's current parent. A category given any parent is refused with `CATEGORIES_CANNOT_HAVE_PARENTS`
@@ -191,7 +191,7 @@ Creates a guild channel and returns its [channel object](/http-api/channels/#cha
A value longer than 10000 characters is rejected with `STRING_LENGTH_INVALID` before normalisation runs. A parent that does not exist in this guild returns `INVALID_PARENT_CHANNEL`, and one that is not a category returns `PARENT_MUST_BE_CATEGORY`.
The guild holds at most the instance-configured `max_guild_channels` [limit](/http-api/instance/#limit-keys), defaulting to 500, and a parent category holds at most `max_channels_per_category`, defaulting to 50. Reaching the guild limit returns 400 `MAX_GUILD_CHANNELS` and reaching the category limit returns 400 `MAX_CATEGORY_CHANNELS`. Each message has the resolved limit. The new channel is created with no RTC region.
The guild holds at most the instance-configured `max_guild_channels` [limit](/http-api/instance/#limit-keys), defaulting to 500, and a parent category holds at most `max_channels_per_category`, defaulting to 50. Reaching the guild limit returns 400 `MAX_GUILD_CHANNELS` and reaching the category limit returns 400 `MAX_CATEGORY_CHANNELS`. Each error message states the resolved limit. The new channel is created with no RTC region.
The new channel takes a position derived from its siblings. A category, and a channel created with no parent, is placed after the highest position in the guild. A voice channel created inside a category is placed after the last voice sibling, or after the last text or link sibling when the category holds no voice channel. A text or link channel is placed after the last text or link sibling, and a channel with no such sibling is placed directly after the category itself. The rest of the guild is not renumbered, so two channels can hold the same stored position until the next [hierarchy update](#modify-guild-channel-positions) renumbers them.
@@ -239,7 +239,7 @@ Applies a guild channel hierarchy update and returns 204 with an empty body. Req
Concurrent hierarchy updates to the same guild can return 423 [`GENERAL_ERROR`](/http-api/errors/) with a `Retry-After` header of two seconds.
An unknown channel, parent or sibling rejects the entire request. Other failures can leave earlier entries applied. Moving a category moves its children with it, and neither the category nor its children count as destination siblings.
An unknown channel, parent or sibling rejects the entire request. Fluxer applies entries one at a time. When a later entry fails a check after that existence check, including the `lock_permissions` authority check, every earlier entry stays applied. Moving a category moves its children with it, and neither the category nor its children count as destination siblings.
After a move, positions run consecutively from 1 across the guild. Each category is followed by its children, with text and link channels before voice channels.
@@ -271,7 +271,7 @@ Channels at the guild root are exempt from this restriction.
| 400<sup>1</sup><sup>2</sup> | [error response](/http-api/#error-response) | An entry is structurally invalid, the destination category is full, or the caller has no enrolled authenticator in an elevated-MFA guild |
| 403 | [error response](/http-api/#error-response) | Caller lacks `MANAGE_CHANNELS`, is not a member of the guild, or set `lock_permissions` without sufficient authority in the moved channel, each returning `MISSING_PERMISSIONS` |
| 404 | [error response](/http-api/#error-response) | Guild does not exist, returning `UNKNOWN_GUILD` |
| 423 | [error response](/http-api/#error-response) | Another hierarchy update owns this guild, returning `GENERAL_ERROR` |
| 423 | [error response](/http-api/#error-response) | Another hierarchy update to this guild is in progress, returning `GENERAL_ERROR` |
<sup>1</sup> A structural failure returns `INVALID_FORM_BODY` and names the field with `CHANNEL_NOT_FOUND`, `INVALID_CHANNEL_ID`, `INVALID_PARENT_CHANNEL`, `PARENT_MUST_BE_CATEGORY`, `CATEGORIES_CANNOT_HAVE_PARENTS`, `PRECEDING_CHANNEL_MUST_SHARE_PARENT`, `CANNOT_POSITION_CHANNEL_RELATIVE_TO_ITSELF`, or `VOICE_CHANNELS_CANNOT_BE_ABOVE_TEXT_CHANNELS`. A full destination category returns the top-level `MAX_CATEGORY_CHANNELS`
@@ -283,7 +283,7 @@ Each entry that changes the order emits [Channel Update Bulk](/gateway/events/#c
When `lock_permissions` copies the destination category's overwrites, re-read the moved channel to obtain them. The copy has no Gateway event or audit entry.
The caller must hold [MANAGE_ROLES](/http-api/permissions/) in the moved channel and each deny bit the copy removes or allow bit it adds. Insufficient authority returns 403 `MISSING_PERMISSIONS`, but the channel move remains applied.
The copy requires [MANAGE_ROLES](/http-api/permissions/) in the moved channel. The caller must also hold every allow bit the copy adds and every deny bit the copy removes. Insufficient authority returns 403 `MISSING_PERMISSIONS`, but the channel move remains applied.
### Rate limit
@@ -236,7 +236,7 @@ Each successful item consumes one guild emoji slot and records an [`EMOJI_CREATE
Copies an existing emoji into the target guild and returns the new [guild emoji object](#guild-emoji-object) without `user`. Requires membership of the target guild and [CREATE_EXPRESSIONS](/http-api/permissions/) there. Emits a [Guild Emojis Update](/gateway/events/#guild-emojis-update) Gateway event in the target guild.
The name, animation state and image are copied unchanged. Membership of the source guild is not required, but it must have opted in with [CLONE_EMOJI_ENABLED](/http-api/guilds/#guild-features).
The name, animation state and image are copied unchanged. Membership of the source guild is not required. The source guild must have [CLONE_EMOJI_ENABLED](/http-api/guilds/#guild-features).
[Get emoji metadata](/http-api/expressions/#get-emoji-metadata) reports whether a source permits cloning.
@@ -309,7 +309,7 @@ An emoji that does not belong to the guild in the path returns 404 `UNKNOWN_EMOJ
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [guild emoji](#guild-emoji-object) object | Name was processed |
| 200 | [guild emoji](#guild-emoji-object) object | Name was set, including a name equal to the current one |
| 400<sup>1</sup> | [error response](/http-api/#error-response) | Path parameter or name is invalid |
| 403<sup>2</sup> | [error response](/http-api/#error-response) | Name is blocked, the guild is unavailable, or the caller is neither the uploader with `CREATE_EXPRESSIONS` nor a member holding `MANAGE_EXPRESSIONS` |
| 404 | [error response](/http-api/#error-response) | Emoji does not exist in that guild and the request returns `UNKNOWN_EMOJI` |
@@ -85,7 +85,7 @@ The result has no `avatar`, `banner`, `accent_color`, `mute`, `deaf`, `communica
## Guild member search supplemental object
A guild member search supplemental object has the join source the [guild member object](/http-api/guild-members/#guild-member-object) never exposes. Only a caller that can already manage the guild reads it.
A guild member search supplemental object has the join source the [guild member object](/http-api/guild-members/#guild-member-object) never exposes. Only a caller holding [MANAGE_GUILD](/http-api/permissions/) receives its values.
### Structure
@@ -210,7 +210,7 @@ An instance using Meilisearch returns an empty page beyond 10000 results. Instan
<sup>1</sup> The [error code](/http-api/errors/) is `MISSING_ACCESS` for an unavailable guild, `CONTENT_BLOCKED` for a blocked body string, and `MISSING_PERMISSIONS` otherwise
When results are being prepared, the response is 200 with an empty page and `indexing` set to true. Retry later. Search unavailability can also produce an empty page with `indexing` false, rather than `FEATURE_TEMPORARILY_DISABLED`.
When Fluxer is building the guild member index, the response is 200 with an empty page and `indexing` set to true. Retry later. This route has no `FEATURE_TEMPORARILY_DISABLED` response. When the search backend is unavailable, the route returns 200 with an empty page and `indexing` false.
:::note[`indexing` false does not mean zero matches]
An empty page with `indexing` false can mean no matches or that search is unavailable.
@@ -218,7 +218,7 @@ An empty page with `indexing` false can mean no matches or that search is unavai
### Side effects
The first search can begin preparing the guild's results. It does not change memberships.
A search in a guild whose member index needs building queues an indexing job and returns `indexing` true. It does not change memberships.
### Rate limit
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
A guild member is an account that has joined a guild. The membership has a nickname, avatar, role set, and moderation state that apply in that guild alone. Indexed member queries live on [Guild member search](/http-api/guild-member-search/), and bans on [Guild moderation](/http-api/guild-moderation/).
An unknown guild returns 404 `UNKNOWN_GUILD`, a non-member receives 403 `MISSING_PERMISSIONS`, and a guild marked unavailable returns 403 `MISSING_ACCESS`. [Transfer guild ownership](#transfer-guild-ownership) can also return 403 `ACCESS_DENIED` when the guild cannot be accessed.
An unknown guild returns 404 `UNKNOWN_GUILD`, a non-member receives 403 `MISSING_PERMISSIONS`, and a guild marked unavailable returns 403 `MISSING_ACCESS`. [Transfer guild ownership](#transfer-guild-ownership) also returns 403 `ACCESS_DENIED` when the guild exists in storage but the Gateway reports it as not found.
The modify operations return the resulting membership, [Transfer guild ownership](#transfer-guild-ownership) returns the updated guild, and every other mutation returns 204 with an empty body.
@@ -49,7 +49,7 @@ Every field except `user` describes state that belongs to the membership.
<sup>6</sup> Omitted while the stored value is 0, so absence means no flag is set and, for `mention_flags`, that the account-level preference applies
Guild avatars and banners can be hidden after the account loses its premium entitlement.
Fluxer marks a membership premium sanitised after its account loses premium, when the membership has a guild avatar, banner, bio, or accent colour. A premium sanitised membership reports `avatar` and `banner` as null.
A client compares `communication_disabled_until` against the current time, because a non-null value can name a moment that has already passed. A [communication timeout](/http-api/permissions/#communication-timeout) does not reduce the member's computed permission mask.
@@ -76,7 +76,7 @@ A client compares `communication_disabled_until` against the current time, becau
## Guild member profile flags
A guild profile asset has these states. A membership with no flag and no stored hash inherits the account-level asset. A stored hash sets a guild-specific asset. The flag below blocks inheritance and renders the default.
A guild profile asset has these states. A membership with no flag and no stored hash inherits the account-level asset. A stored hash sets a guild-specific asset. Each flag below blocks inheritance, so the client shows the default asset.
| Value | Name | Description |
| --- | --- | --- |
@@ -127,7 +127,7 @@ The request body shared by [Modify current guild member](#modify-current-guild-m
A `roles` entry that does not resolve to an existing role of the guild is dropped from the replacement, so a request naming only unknown roles clears the member's role set.
An `avatar` or `banner` base64 payload longer than 13981016 characters returns `BASE64_LENGTH_INVALID`, and a malformed one returns `INVALID_BASE64_FORMAT`. Fluxer then checks the decoded bytes against the instance-configured `avatar_max_size` [limit](/http-api/instance/#limit-keys), whose stock value is 10485760. The same limit applies to both fields. The decoded image must also pass the format allowlist and the animation rules of the asset policy for the field it sets. Pixel dimensions are not enforced. A value that is too large returns `IMAGE_SIZE_EXCEEDS_LIMIT`, and one whose format or animation is not allowed returns `INVALID_IMAGE_FORMAT`.
An `avatar` or `banner` base64 payload longer than 13981016 characters returns `BASE64_LENGTH_INVALID`, and a malformed one returns `INVALID_BASE64_FORMAT`. Fluxer then checks the decoded bytes against the instance-configured `avatar_max_size` [limit](/http-api/instance/#limit-keys), whose stock value is 10485760. The same limit applies to both fields. The decoded image must also be in a format the field accepts, and an animated AVIF is rejected. Pixel dimensions are not enforced. A value that is too large returns `IMAGE_SIZE_EXCEEDS_LIMIT`, and one whose format or animation is not allowed returns `INVALID_IMAGE_FORMAT`.
Null and any `communication_disabled_until` that is not in the future both clear the timeout. A time more than 365.25 days ahead returns `TIMEOUT_CANNOT_EXCEED_365_DAYS`, and a value that passes the schema but is not a real instant returns `INVALID_TIMEOUT_VALUE`. `timeout_reason` has no effect unless `communication_disabled_until` is also supplied.
@@ -318,7 +318,7 @@ A caller addressing their own user ID here follows the self-targeted rules of [M
:::
:::caution[A blocklisted `bio` or `pronouns` returns 403]
The request fails with `CONTENT_BLOCKED` even though the field is never written.
A request that supplies a blocked `bio` or `pronouns` for another member fails with `CONTENT_BLOCKED`, even though Fluxer never writes those fields for another member.
:::
### Path parameters
@@ -350,7 +350,7 @@ The body is a [guild member update object](#guild-member-update-object).
### Side effects
The operation has the same effects as [Modify current guild member](#modify-current-guild-member). Applying a non-empty role set also makes a temporary membership permanent.
The operation has the same effects as [Modify current guild member](#modify-current-guild-member). Applying a non-empty role set also makes a temporary membership permanent. A membership is temporary when the member joined through an [invite](/http-api/invites/#invite-object) whose `temporary` field is true.
### Rate limit
@@ -45,12 +45,12 @@ A guild ban object names the banned account, the moderator, the reason, and any
```
:::note[Fluxer never returns the ban address or email]
A ban can also prevent accounts using the same IP address or email from joining. These values are not exposed in the ban object.
A ban also stores the email address and the last active IP address of the banned account. Fluxer rejects an invite join by any account that matches either value. These values are not exposed in the ban object.
:::
An [invite](/http-api/invites/) rejected for an IP match returns 403 `USER_IP_BANNED_FROM_GUILD`. An account or email match returns 403 `USER_BANNED_FROM_GUILD`. Address exemptions can allow a join from a shared network. Ban checks differ for administrator actions, bot installation and automatic guild joins.
An [invite](/http-api/invites/) rejected for an IP match returns 403 `USER_IP_BANNED_FROM_GUILD`. An account or email match returns 403 `USER_BANNED_FROM_GUILD`. Fluxer skips the IP match when the joining account's address is on the operator's IP ban exemption list, or when IP lookup classifies that address as carrier-grade NAT or shared access. Adding a member through the Admin API, installing a bot through OAuth2, and the automatic join into the visionaries guild after a Stripe purchase skip every ban check. The automatic join into the single community checks the account and IP matches and skips the email match.
[Remove guild ban](#remove-guild-ban) releases both blocks. Permanently deleting the banned account deletes every guild ban it holds, and that releases both blocks in every guild at once.
[Remove guild ban](#remove-guild-ban) releases the IP address block and the email block. Permanently deleting the banned account deletes every guild ban it holds, which releases both of those blocks in every guild at once.
## List guild bans
@@ -95,7 +95,7 @@ Creates a guild ban, or replaces an existing one, and returns 204 with an empty
- The caller cannot ban themselves, and Fluxer reports a self-target as 404 `UNKNOWN_MEMBER`.
- Banning a target who is a member also requires role hierarchy authority over that member. The guild owner holds that authority over everyone, and no other caller holds it over the owner.
- A target who is not a member can still be banned, and the ban pre-empts a future join.
- A target who is not a member can still be banned, and Fluxer refuses a later join by that account.
A blocked `reason` returns 403 `CONTENT_BLOCKED`.
@@ -12,7 +12,7 @@ Every route names a guild in its path. A guild that has [UNAVAILABLE_FOR_EVERYON
Names, descriptions, tags and uploaded images must pass the instance's content policy. Blocked content returns 403 `CONTENT_BLOCKED`.
A guild that does not exist returns 404 `UNKNOWN_GUILD`. A caller who is not a current member of an existing guild returns 403 `MISSING_PERMISSIONS`, so guild existence is distinguishable from guild membership. [Modify guild sticker](#modify-guild-sticker) answers 404 `UNKNOWN_STICKER` for such a guild.
A guild that does not exist returns 404 `UNKNOWN_GUILD`. A caller who is not a current member of an existing guild returns 403 `MISSING_PERMISSIONS`, so guild existence is distinguishable from guild membership. [Modify guild sticker](#modify-guild-sticker) returns 404 `UNKNOWN_STICKER` for a guild that does not exist.
There is no single-sticker read scoped to a guild. [List guild stickers](#list-guild-stickers) returns the whole collection in one response.
@@ -132,7 +132,7 @@ One rejected item from a bulk create call, named and explained in display text.
<sup>2</sup> Rendered in the locale of the authenticated account, so its value changes with the caller's locale
There is no machine-readable code, so a client that needs to branch on the reason retries the item on its own.
There is no machine-readable code, so a client that needs to branch on the reason submits that item again through [Create guild sticker](#create-guild-sticker), which returns an error code.
## List guild stickers
@@ -10,7 +10,7 @@ A guild is a community with its own channels, roles, members, and configuration.
[List current user guilds](#list-current-user-guilds) and [Get guild](#get-guild) declare the `guilds` [OAuth2 scope](/http-api/oauth2/#oauth2-scopes), and a bearer credential without that scope receives 403 `MISSING_OAUTH_SCOPE`. Every other route rejects a bearer credential with 403 `ACCESS_DENIED`.
A guild that does not exist returns 404 `UNKNOWN_GUILD`. A non-member receives 403 `MISSING_PERMISSIONS`. A guild that exists but cannot be accessed can return 403 `ACCESS_DENIED`.
A guild that does not exist returns 404 `UNKNOWN_GUILD`. A non-member receives 403 `MISSING_PERMISSIONS`. A guild that exists in storage but that the Gateway reports as not found returns 403 `ACCESS_DENIED`.
An [unavailable guild](#guild-features) can still appear in [List current user guilds](#list-current-user-guilds). Members can still [leave](#leave-guild) or [delete their own messages](#bulk-delete-current-users-guild-messages).
@@ -93,7 +93,7 @@ A guild object contains the guild's configuration. The operation that returns it
<sup>14</sup> Only [Get guild](#get-guild) populates these fields
<sup>15</sup> Only [List current user guilds](#list-current-user-guilds) populates these fields, and only when `with_counts` is true. A guild whose counts are not currently held reports 0 for both
<sup>15</sup> Only [List current user guilds](#list-current-user-guilds) populates these fields, and only when `with_counts` is true. A guild for which the Gateway holds no cached counts reports 0 for both
:::note[An absent field means the operation omitted it]
`roles`, `emojis`, `stickers`, `channels`, and every count field arrive only from the operations named in the footnotes above. A guild returned without `roles` can still hold roles.
@@ -251,7 +251,7 @@ A creation template describes the roles and channels that [Create guild](#create
<sup>1</sup> A duplicate identifier in the same template rejects creation with 400 `GUILD_TEMPLATE_INVALID`
<sup>2</sup> The value 0 creates a text channel, 2 a voice channel, and 4 a category, and for an import from the other platform 5 creates a text channel and 13 a voice channel. Fluxer skips every other value, so the channel is not created
<sup>2</sup> The value 0 creates a text channel, 2 a voice channel, and 4 a category. The value 5, the announcement channel type of another platform, creates a text channel. The value 13, the stage channel type of another platform, creates a voice channel. Fluxer skips every other value, so the channel is not created
<sup>3</sup> The identifier is applied only when it resolves to a category in the same template, and every other value leaves the channel at the guild root
@@ -390,7 +390,7 @@ Each value in the guild's `features` array is a capability or availability flag.
| INVITE_SPLASH | Guild can use invite splash assets |
| INVITES_DISABLED<sup>1</sup> | Guild invite use is disabled |
| RAID_DETECTED | Raid detection is active and invites are restricted |
| TEXT_CHANNEL_FLEXIBLE_NAMES<sup>1</sup> | Text channel names accept the flexible naming policy |
| TEXT_CHANNEL_FLEXIBLE_NAMES<sup>1</sup> | Text channel names keep uppercase letters, spaces, and punctuation |
| HIDE_OWNER_CROWN<sup>1</sup> | Guild owner crown is hidden |
| MORE_EMOJI<sup>2</sup> | Legacy increased emoji slot allowance |
| MORE_STICKERS<sup>2</sup> | Legacy increased sticker slot allowance |
@@ -401,11 +401,11 @@ Each value in the guild's `features` array is a capability or availability flag.
| DISCOVERABLE | Guild is present in public discovery |
| PARTNERED | Guild has partnered status |
| VERIFIED | Guild has verified status |
| VIP_VOICE | Guild has VIP voice capability |
| VIP_VOICE | Guild can use voice regions that are restricted to VIP guilds |
| VOICE_E2EE | Guild voice channels support end-to-end encryption |
| UNAVAILABLE_FOR_EVERYONE<sup>4</sup> | Guild is unavailable to every account |
| UNAVAILABLE_FOR_EVERYONE_BUT_STAFF<sup>4</sup> | Guild is unavailable to every account without the instance staff flag |
| UNAVAILABLE_HIDDEN | Guild is hidden while it is forced unavailable |
| UNAVAILABLE_HIDDEN | While the guild is unavailable, the Gateway sends its unavailable guild entry with `unavailable_hidden: true` |
| VISIONARY | Guild has visionary status |
| LARGE_GUILD_OVERRIDE<sup>2</sup> | Guild is marked as a large guild |
| VERY_LARGE_GUILD<sup>5</sup> | Guild member capacity is raised |
@@ -414,7 +414,7 @@ Each value in the guild's `features` array is a capability or availability flag.
<sup>2</sup> The feature changes no HTTP API behaviour. An instance can name it in a filter of the ordered [limit configuration](/http-api/instance/#limit-keys), and the stock configuration names none of them
<sup>3</sup> The expression operations raise the slot ceiling directly, outside the instance limit configuration
<sup>3</sup> Emoji and sticker creation use a fixed slot ceiling of 999999 and ignore the instance limit configuration
<sup>4</sup> Guild and channel routes return 403 `MISSING_ACCESS`. `UNAVAILABLE_FOR_EVERYONE` includes the guild owner. `UNAVAILABLE_FOR_EVERYONE_BUT_STAFF` exempts accounts with the instance staff flag
@@ -494,7 +494,7 @@ Creates a guild owned by the caller. Requires a user session credential. Returns
- An unclaimed account is rejected with 400 `UNCLAIMED_ACCOUNT_CANNOT_CREATE_GUILDS`.
- An account without a verified email address is rejected with 403 `GUILD_CREATION_EMAIL_VERIFICATION_REQUIRED`.
- A caller already at the configured guild limit is rejected with 400 `MAX_GUILDS`.
- While the instance's single community policy is active, every caller is rejected with 400 `SINGLE_COMMUNITY_CANNOT_CREATE_GUILDS`.
- While `single_community_enabled` is true in the [instance policy](/admin-api/instance/#instance-policy-object), every caller is rejected with 400 `SINGLE_COMMUNITY_CANNOT_CREATE_GUILDS`.
### JSON body
@@ -601,7 +601,7 @@ The response has no `permissions` field. Read [List current user guilds](#list-c
| 403<sup>1</sup> | [error response](/http-api/#error-response) | Guild is unavailable, the bearer credential lacks the `guilds` scope, or the caller is not a member |
| 404 | [error response](/http-api/#error-response) | Guild does not exist and the request returns `UNKNOWN_GUILD` |
<sup>1</sup> The [error code](/http-api/errors/) is `MISSING_ACCESS` for an unavailable guild, `MISSING_OAUTH_SCOPE` for a missing scope, `MISSING_PERMISSIONS` for a non-member, and `ACCESS_DENIED` when the guild otherwise cannot be accessed
<sup>1</sup> The [error code](/http-api/errors/) is `MISSING_ACCESS` for an unavailable guild, `MISSING_OAUTH_SCOPE` for a missing scope, `MISSING_PERMISSIONS` for a non-member, and `ACCESS_DENIED` when the guild exists in storage but the Gateway reports it as not found
### Rate limit
@@ -715,9 +715,9 @@ Send the current array with the intended changes applied. A feature without the
Every successful request emits [Guild Update](/gateway/events/#guild-update) to every session that can see the guild, and a request that writes no field still emits it.
Only a change to an audited field records a [`GUILD_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the previous and new values. That entry emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) to sessions that can read the audit log.
A request that changes the stored value of at least one body field records a [`GUILD_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the previous and new values. That entry emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) to sessions that can read the audit log.
Removing `TEXT_CHANNEL_FLEXIBLE_NAMES` renames every guild text channel whose stored name does not satisfy the strict naming policy. When at least one channel is renamed, the removal emits one [Channel Update Bulk](/gateway/events/#channel-update-bulk) Dispatch with every channel in the guild.
Removing `TEXT_CHANNEL_FLEXIBLE_NAMES` renames every guild text channel whose stored name changes when Fluxer trims it, lowercases it, replaces each whitespace run with a hyphen, and removes disallowed punctuation. When at least one channel is renamed, the removal emits one [Channel Update Bulk](/gateway/events/#channel-update-bulk) Dispatch with every channel in the guild.
Replacing an image removes the previous asset. A failed replacement leaves the previous image unchanged.
@@ -733,7 +733,7 @@ Permanently deletes the guild and returns 204 with an empty body. Requires the g
Fluxer refuses a non-owner with 403 `MISSING_PERMISSIONS`. A bot can never own a guild, so a bot credential never satisfies the requirement.
A guild protected by an active single community policy cannot be deleted and returns 400 `SINGLE_COMMUNITY_CANNOT_DELETE`.
While `single_community_enabled` is true in the [instance policy](/admin-api/instance/#instance-policy-object), the guild named by `single_community_guild_id` cannot be deleted and returns 400 `SINGLE_COMMUNITY_CANNOT_DELETE`.
### Path parameters
@@ -772,7 +772,7 @@ Deletion removes the guild, memberships, roles, channels, messages, attachments,
### Side effects
Every member receives [Guild Delete](/gateway/events/#guild-delete). Guild settings are removed. Non-bot members also receive [User Settings Update](/gateway/events/#user-settings-update) with the guild removed from their folder layout.
Every member receives [Guild Delete](/gateway/events/#guild-delete). Fluxer deletes every member's [user guild settings](/http-api/users/settings/#user-guild-settings-object) for the guild. Non-bot members also receive [User Settings Update](/gateway/events/#user-settings-update) with the guild removed from their folder layout.
No audit log entry is recorded, because the audit log is destroyed with the guild. An `X-Audit-Log-Reason` header on this request is read and discarded.
@@ -786,7 +786,7 @@ No audit log entry is recorded, because the audit log is destroyed with the guil
Removes the authenticated account's membership and returns 204 with an empty body. Requires a current membership. Emits a [Guild Member Remove](/gateway/events/#guild-member-remove) Gateway event to the remaining guild sessions and a [Guild Delete](/gateway/events/#guild-delete) Gateway event to the leaving account's own sessions.
A caller with no current membership receives 404 `UNKNOWN_MEMBER` whether or not the guild exists. The guild owner cannot leave and receives 400 `INVALID_FORM_BODY` with the field code `CANNOT_LEAVE_GUILD_AS_OWNER`. A guild protected by an active single community policy cannot be left and returns 400 `SINGLE_COMMUNITY_CANNOT_LEAVE`. Setting `delete_messages` requires sudo mode, which a bot credential satisfies implicitly.
A caller with no current membership receives 404 `UNKNOWN_MEMBER` whether or not the guild exists. The guild owner cannot leave and receives 400 `INVALID_FORM_BODY` with the field code `CANNOT_LEAVE_GUILD_AS_OWNER`. While `single_community_enabled` is true in the [instance policy](/admin-api/instance/#instance-policy-object), the guild named by `single_community_guild_id` cannot be left and returns 400 `SINGLE_COMMUNITY_CANNOT_LEAVE`. Setting `delete_messages` requires sudo mode, which a bot credential satisfies implicitly.
### Path parameters
@@ -69,7 +69,7 @@ An empty string becomes `null` at any depth, and an empty nested object becomes
This normalisation applies to JSON and form bodies, query strings, path parameters, request headers, and cookies.
A nested object containing only `null` values also becomes `null`. The root object is preserved. An empty body is treated as `{}` and validated for required fields. Malformed JSON returns 400 `INVALID_FORM_BODY` with a validation error at path `body` and code `INVALID_FORMAT`.
A nested object containing only `null` values also becomes `null`. The root object never becomes `null`, even when it is empty or has only `null` values. An empty body is treated as `{}` and validated for required fields. Malformed JSON returns 400 `INVALID_FORM_BODY` with a validation error at path `body` and code `INVALID_FORMAT`.
:::caution[Message operations preserve empty values]
[Create message](/http-api/messages/#create-message), [Modify message](/http-api/messages/#modify-message), and [Execute webhook](/http-api/webhooks/#execute-webhook) do not apply this normalisation.
@@ -162,7 +162,7 @@ An operation that sets its own `Cache-Control` keeps that value. A response whos
## Rate limits
Every route consumes its own rate limit bucket and is also evaluated against one global bucket unless that bucket is exempt. A denial returns 429 `RATE_LIMITED`. [Rate limits](/topics/rate-limits/) defines the bucket scoping rules, the global allowance, the 429 body, the scope registry, and the complete `X-RateLimit-*` header contract.
Every route consumes its own rate limit bucket. A route that is not exempt from the global bucket also counts against one global bucket. A denial returns 429 `RATE_LIMITED`. [Rate limits](/topics/rate-limits/) defines the bucket scoping rules, the global allowance, the 429 body, the scope registry, and the complete `X-RateLimit-*` header contract.
:::note[These 429 responses have no `X-RateLimit-*` header]
A 429 `RESOURCE_LOCKED` response has `Retry-After: 1`, and a 429 `IP_AUTHORIZATION_RESEND_COOLDOWN` response has the remaining cooldown in whole seconds. A client that reads the bucket headers branches on `code`.
@@ -180,7 +180,7 @@ The CORS response policy is an allow-list of exactly two origins, the deployment
The paths below are readable from any origin. `/v1/webhooks/{webhook_id}/{token}` and `/v1/webhooks/{webhook_id}/{token}/messages/{message_id}` have a second cross-origin policy that allows any origin. Four of the methods registered on them refuse the first-party web client outright, and that refusal is defined by [Origin refusal](/http-api/webhooks/#origin-refusal).
[Get instance discovery](/http-api/instance/#get-instance-discovery) on `/.well-known/fluxer`, [Get OpenAPI document](/http-api/instance/#get-openapi-document) on `/v1/openapi.json`, and [Get client geolocation](/http-api/instance/#get-client-geolocation) on `/v1/ip` set `Access-Control-Allow-Origin: *` in the operation itself. The wildcard stands for any origin outside the allow-list, and for an allowed origin the policy replaces it with that exact origin and sends `Vary: Origin`.
[Get instance discovery](/http-api/instance/#get-instance-discovery) on `/.well-known/fluxer`, [Get OpenAPI document](/http-api/instance/#get-openapi-document) on `/v1/openapi.json`, and [Get client geolocation](/http-api/instance/#get-client-geolocation) on `/v1/ip` set `Access-Control-Allow-Origin: *` in the operation itself. A request whose `Origin` is absent or outside the allow-list receives `*`. For an allowed origin, the policy replaces `*` with that exact origin and sends `Vary: Origin`.
`Access-Control-Expose-Headers` is the value `X-Fluxer-Version, ETag`. Every other Fluxer response header, the rate limit headers and `X-Request-ID` included, is hidden from cross-origin script. `Access-Control-Allow-Headers` is `Content-Type, Authorization, X-Requested-With, Accept-Language, X-Request-ID, If-None-Match`, so a cross-origin client revalidates an [ETag](/http-api/experiments/#get-experiment-assignments) it was served.
@@ -255,7 +255,7 @@ Each entry identifies one failed input field. A 400 response whose top-level cod
}
```
Fluxer produces at most one entry for each distinct pair of `path` and `code`, so a field that fails several equivalent constraints appears once.
Fluxer produces at most one entry for each distinct pair of `path` and `code`, so a field that fails several constraints with the same `code` appears once.
## Resource pages
@@ -61,7 +61,7 @@ Each value is an absolute URL supplied by the operator. A value can be a bare or
<sup>2</sup> The value is the configured Gateway endpoint and its scheme is `ws` or `wss` as the operator configured it
<sup>3</sup> A deployment that configures a separate static asset domain derives that value over `https` on the configured domain and without the port the other endpoints have
<sup>3</sup> When a deployment configures a separate static asset domain, the value is `https://` followed by that domain, with no port
Every value is the exact origin the deployment advertises, including its scheme and any explicit port, and a deliberately plain HTTP deployment publishes `http` and `ws` values here.
@@ -193,10 +193,10 @@ Whether this deployment runs as one community, and whether direct messages exist
| Field | Type | Description |
| --- | --- | --- |
| single_community | boolean | Whether this deployment runs as one community that every account joins |
| single_community_guild_id<sup>1</sup> | ?snowflake | The stock community guild ID, or null |
| single_community_guild_id<sup>1</sup> | ?snowflake | The ID of the guild every account joins in single-community mode, or null |
| direct_messages_disabled | boolean | Whether direct messages and friend requests are disabled for the whole deployment |
<sup>1</sup> The identifier is published only while `single_community` is true and a stock community has been chosen
<sup>1</sup> The identifier is published only while `single_community` is true and the deployment has designated a community guild
## Service availability object
@@ -258,10 +258,10 @@ The document lets a client present the correct bounds before it attempts an oper
:::
:::caution[An unauthenticated reader resolves only the unfiltered outcome]
The document publishes neither the requesting account's traits nor any guild feature set. A client supplies that context from its own authenticated state.
The document publishes neither the requesting account's traits nor any guild feature set. A client supplies the account's traits and the guild's feature names itself, from data it received while authenticated.
:::
Nothing validates a rule's `traits` filter against `traitDefinitions`, so a rule can filter on a name that collection omits. A self-hosted deployment whose premium mode grants every account the stock limits publishes an empty collection and has no rule that filters on `premium`.
Nothing validates a rule's `traits` filter against `traitDefinitions`, so a rule can filter on a name that collection omits. A self-hosted deployment whose [premium mode](/admin-api/instance/#premium-modes) is `everyone` publishes an empty collection and has no rule that filters on `premium`.
## Limit rule object
@@ -353,7 +353,7 @@ Each key names one limit. A key whose name begins with `feature_` is a feature g
| max_webhooks_per_guild | Maximum webhooks per guild, in guild scope |
| sticker_max_size | Maximum file size for sticker uploads in bytes, in guild scope |
<sup>1</sup> Emoji limits are enforced as one shared total against `max_guild_emojis`, and the aliases exist only so an older client reads a plausible value
<sup>1</sup> Emoji limits are enforced as one shared total against `max_guild_emojis`, and the aliases exist only for older clients. The default of each alias equals the `max_guild_emojis` default
<sup>2</sup> Sticker limits are enforced as one shared total against `max_guild_stickers`
@@ -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. Creation and listing operations return it when the caller has management access to the target, and [Get invite](#get-invite) never does.
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.
### Structure
@@ -154,7 +154,7 @@ An `Authorization` header that cannot be resolved is ignored, and the route retu
Codes from [Create channel invite](#create-channel-invite) are case-sensitive. Custom invite codes are case-insensitive.
:::note[Every admission gate runs at accept time]
Resolving an invite proves that the record exists and that its target is describable. [Accept invite](#accept-invite) evaluates bans, disabled invites, exhausted uses, missing presence, and target capacity.
A 200 response means the invite code exists and none of the 404 conditions in the response table below applies. [Accept invite](#accept-invite) checks bans, the guild's invites-disabled feature, exhausted uses, the active presence a temporary invite requires, and whether the target is full.
:::
:::note[Lookup does not guarantee admission]
@@ -223,15 +223,15 @@ Accepting an exhausted invite returns 404 `UNKNOWN_INVITE` and makes the code st
| 403 | [error response](/http-api/#error-response) | A deferred phone verification requirement becomes due and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` |
| 404 | [error response](/http-api/#error-response) | The named guild no longer exists and the request returns `UNKNOWN_GUILD` |
| 404 | [error response](/http-api/#error-response) | The target channel no longer exists and the request returns `UNKNOWN_CHANNEL` |
| 404 | [error response](/http-api/#error-response) | The code resolves to no record, the record is exhausted, or the invite names no reachable target, each returning `UNKNOWN_INVITE` |
| 404 | [error response](/http-api/#error-response) | The code resolves to no record, the record is exhausted, a guild invite names no guild, or a group direct message invite names no channel or a channel that no longer exists, each returning `UNKNOWN_INVITE` |
### Side effects
An account already in the target receives the invite unchanged without consuming a use or emitting a Dispatch. A new admission consumes one use and removes the invite when its limit is reached. A failed admission consumes no use. If the invite's channel has been deleted, the account can still join and consume a use even though the response is 404 `UNKNOWN_CHANNEL`.
When the account is already in the target, the route returns 200 with the invite object. It consumes no use and emits no Dispatch. A new admission consumes one use and removes the invite when its limit is reached. A failed admission consumes no use. If the invite's channel has been deleted, the account can still join and consume a use even though the response is 404 `UNKNOWN_CHANNEL`.
A guild admission creates the membership, records whether the join used a custom invite URL or an instant invite, and adds the guild to the caller's settings and folder layout. A temporary invite marks the membership temporary.
The joining account receives [Guild Create](/gateway/events/#guild-create). [Guild Member Add](/gateway/events/#guild-member-add) is guild-wide and reaches every session connected to the guild, subject to the [event filtering](/gateway/event-filtering/) gates, which suppress it for a passive session in a large guild. The joining account receives [User Settings Update](/gateway/events/#user-settings-update) when the join changes its stored settings, and [User Guild Settings Update](/gateway/events/#user-guild-settings-update) when its `default_hide_muted_channels` setting is enabled. Unless join notifications are suppressed or no system channel exists, the operation creates a join system message and delivers [Message Create](/gateway/events/#message-create).
The joining account receives [Guild Create](/gateway/events/#guild-create). [Guild Member Add](/gateway/events/#guild-member-add) is guild-wide and reaches every session connected to the guild, subject to the [event filtering](/gateway/event-filtering/) gates, which withhold it from a passive user session in a guild with more than 250 members. The joining account receives [User Settings Update](/gateway/events/#user-settings-update) when the join changes its stored settings, and [User Guild Settings Update](/gateway/events/#user-guild-settings-update) when its `default_hide_muted_channels` setting is enabled. Unless the guild's `system_channel_flags` has [SUPPRESS_JOIN_NOTIFICATIONS](/http-api/guilds/#system-channel-flags) or the guild has no system channel, the operation creates a join system message and delivers [Message Create](/gateway/events/#message-create).
A group direct message admission adds the caller as a recipient and creates a recipient addition system message. The joining account receives [Channel Create](/gateway/events/#channel-create), the recipients the group already held receive [Channel Recipient Add](/gateway/events/#channel-recipient-add), and every recipient receives [Message Create](/gateway/events/#message-create).
@@ -305,7 +305,7 @@ Creates an invite for a channel, or returns an existing equivalent invite. Retur
This operation does not require elevated multi-factor authentication.
:::note[Guild affiliation determines the invite type]
A channel that belongs to a guild creates a guild invite. Any other channel the caller can resolve creates a type `1` invite, including a direct message and the personal notes channel. Only a group direct message admits an account afterwards.
A channel that belongs to a guild creates a guild invite. Any other channel the caller can resolve creates a type `1` invite, including a direct message and the personal notes channel. Of these type `1` invites, only one whose channel is a group direct message admits an account through [Accept invite](#accept-invite).
:::
### Path parameters
@@ -6,13 +6,13 @@ description: Saved favourite media and batch GIF URL resolution.
import RouteHeader from '@/components/RouteHeader.astro';
A meme is an image, video, or audio asset saved as a favourite. Its copy remains available after the source URL or message disappears.
A meme is an image, video, or audio asset saved as a favourite. Fluxer stores a copy of the media with the meme. The copy remains available after the source URL or the source message is gone.
The routes here are user-only and operate on the caller's own collection. Fluxer rejects a bot token with 403 `ACCESS_DENIED`. [Create message](/http-api/messages/#create-message) sends a stored meme through its `favorite_meme_id` field, and the [GIF provider API](/http-api/gifs/) searches the configured provider for a GIF to save.
## Collection limits
Fluxer caps a collection at the instance-configured `max_favorite_memes` [limit](/http-api/instance/#limit-keys), with a fallback of 50. Both creation operations check the count before they touch the channel, the message, or the source URL, so an account at its limit is refused with 400 `MAX_FAVORITE_MEMES` even when the rest of the request would have failed. The error body has `max_favorite_memes` reporting the ceiling reached.
Fluxer caps a collection at the instance-configured `max_favorite_memes` [limit](/http-api/instance/#limit-keys), with a fallback of 50. Both creation operations check the count before they look up the channel or the message and before they fetch the source URL, so an account at its limit is refused with 400 `MAX_FAVORITE_MEMES` even when the rest of the request would have failed. The error body has `max_favorite_memes` reporting the ceiling reached.
A meme has at most `max_favorite_meme_tags` tags, with a fallback of 10. Fluxer rejects a longer list with 400 `INVALID_FORM_BODY` and the validation code `MAX_FAVORITE_MEME_TAGS_EXCEEDED` at the `tags` path.
@@ -101,7 +101,7 @@ One descriptor describes one encoding of one provider GIF. A provider returns se
| width<sup>1</sup> | integer | The width of this format in pixels |
| height<sup>1</sup> | integer | The height of this format in pixels |
<sup>1</sup> A non-negative value of at most 2147483647. A descriptor with an empty `src` or `proxy_src`, or a zero dimension, is unusable as a preview and is skipped
<sup>1</sup> A non-negative value of at most 2147483647. A descriptor with an empty `src` or `proxy_src`, or a zero dimension, is skipped when Fluxer picks the [preview format](#resolved-gif-entry-object) for Resolve GIF URLs
A client submitting a format map to [Save meme from URL](#save-meme-from-url) supplies all members of every descriptor, and a descriptor missing one fails validation with 400 `INVALID_FORM_BODY`.
@@ -199,7 +199,7 @@ Fetches an absolute media URL, stores a durable Fluxer copy, and returns the cre
<sup>2</sup> An omitted or null value is an empty tag list
<sup>3</sup> Honoured together only when `gif_provider` names the configured provider. Otherwise Fluxer extracts a slug from `url` with the configured provider
<sup>3</sup> Fluxer uses the supplied pair only when both are present and `gif_provider` names the configured provider. Otherwise Fluxer extracts a slug from `url` with the configured provider
<sup>4</sup> Stored only when the request resolved to a provider GIF and the map is non-empty, and discarded otherwise
@@ -234,7 +234,7 @@ The operation emits one [Favorite Meme Create](/gateway/events/#favorite-meme-cr
Copies one attachment or one embed asset from a readable message into the caller's own collection and returns the created [meme object](#meme-object). Requires [VIEW_CHANNEL](/http-api/permissions/) in a guild channel. Emits a [Favorite Meme Create](/gateway/events/#favorite-meme-create) Gateway event.
A guild caller without [READ_MESSAGE_HISTORY](/http-api/permissions/) can select media only from a message on or after the guild's message history cutoff. Attachments and embeds in the message's forwarded snapshots are selectable alongside its own.
A guild caller without [READ_MESSAGE_HISTORY](/http-api/permissions/) can select media only from a message on or after the guild's [message history cutoff](/http-api/messages/). Attachments and embeds in the message's forwarded snapshots are selectable alongside its own.
### Path parameters
@@ -288,7 +288,7 @@ A guild caller that can view the channel but lacks `READ_MESSAGE_HISTORY` receiv
### Side effects
The meme gets its own copy of the selected asset. Reading the source message renews its attachment decay deadline when attachment decay is enabled. Its content is otherwise unchanged.
The meme gets its own copy of the selected asset. Reading the source message renews its attachment decay deadline when attachment decay is enabled. The source message is otherwise unchanged.
The operation emits one [Favorite Meme Create](/gateway/events/#favorite-meme-create) with the created object to the caller's own sessions.
@@ -421,7 +421,7 @@ A client uses it to turn stored URL-only favourite GIFs into renderable picker e
| --- | --- | --- |
| Accept-Language?<sup>1</sup> | string | The preferred locale used for a GIF provider lookup |
<sup>1</sup> Resolved against the [supported locale registry](/topics/locales/#supported-locales), replaced by the account's own stored locale, and defaulting to `en-US`
<sup>1</sup> Fluxer uses the account's stored locale when it has one. Otherwise it matches this header against the [supported locale registry](/topics/locales/#supported-locales), and it falls back to `en-US`
Fluxer derives a two-letter country from the requesting address by geolocation, with `US` as the fallback.
@@ -445,7 +445,7 @@ An unresolved URL still returns an entry with its signed proxy URL and an empty
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | Every URL was resolved |
| 200 | response body | One entry was returned for each URL, including URLs that did not resolve |
| 403 | [error response](/http-api/#error-response) | Caller is a bot or presents a bearer credential and the request returns `ACCESS_DENIED` |
| 403 | [error response](/http-api/#error-response) | The account has an outstanding required action and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` |
@@ -14,7 +14,7 @@ A text-bearing channel is a [channel type](/http-api/channels/#channel-types) th
Fluxer resolves the channel before it applies any per-route authorisation. A private channel returns 404 `UNKNOWN_CHANNEL` to a caller who is not a recipient. A guild channel returns 403 `MISSING_PERMISSIONS` to a non-member and to a member without [VIEW_CHANNEL](/http-api/permissions/). A guild an operator has marked unavailable returns 403 `MISSING_ACCESS` before the route runs. A guild that no Gateway process serves returns 404 `UNKNOWN_GUILD`, or 403 `ACCESS_DENIED` when its stored record survives.
The message read, create, modify, and delete routes, [Bulk delete messages](#bulk-delete-messages), [Bulk delete own messages](#bulk-delete-own-messages), and [List pinned messages](#list-pinned-messages) also enforce age verification on an effectively age restricted guild channel. They return 403 `NSFW_CONTENT_AGE_RESTRICTED` until the account satisfies it. The attachment routes, the pin routes, the reaction routes, and [Indicate typing](#indicate-typing) resolve the channel without that check.
The message read, create, modify, and delete routes, [Bulk delete messages](#bulk-delete-messages), [Bulk delete own messages](#bulk-delete-own-messages), and [List pinned messages](#list-pinned-messages) also enforce age verification on an age restricted guild channel. A guild channel is age restricted when its own setting says so. A channel with no setting of its own uses its parent category's setting, and then the guild's. They return 403 `NSFW_CONTENT_AGE_RESTRICTED` until the account satisfies it. The attachment routes, the pin routes, the reaction routes, and [Indicate typing](#indicate-typing) resolve the channel without that check.
[Clear channel read state](#clear-channel-read-state) and [Acknowledge message](#acknowledge-message) resolve no channel. [Acknowledge pins](#acknowledge-pins) reads the channel without checking the caller's access. None of the three checks a permission.
@@ -61,7 +61,7 @@ A message object is the full stored form of one post in a channel.
<sup>4</sup> Empty while `SUPPRESS_EMBEDS` is set on the message
<sup>5</sup> An attachment that a resolved embed owns is excluded from this array and is exposed through that embed's media object instead
<sup>5</sup> An attachment that a [rich embed input](#rich-embed-input-objects) names with an `attachment://` URL in `image` or `thumbnail` is excluded from this array. The attachment's URL is in that embed's media object instead
<sup>6</sup> Echoed only in the create message response and its originating [Message Create](/gateway/events/#message-create) Gateway Dispatch, and never stored on the message
@@ -69,7 +69,7 @@ A message object is the full stored form of one post in a channel.
A webhook-authored message has no stored author user. Fluxer builds its author from the webhook, with the webhook ID as `id`, the stored webhook name as `username`, the discriminator `0000`, the stored webhook avatar hash as `avatar`, and `bot` true. A message whose stored author ID no longer resolves to an account is served with a deleted-user placeholder. The placeholder has that ID, the discriminator `0000`, and no avatar.
`mentions`, `mention_roles`, `embeds`, `attachments`, and `stickers` are always present and can be empty. Every optional field above is omitted entirely, and `referenced_message` is the one that can instead be present and null. A message whose author user and webhook are both absent is omitted from every read.
`mentions`, `mention_roles`, `embeds`, `attachments`, and `stickers` are always present and can be empty. A field marked `?` above is omitted from the object when it has no value. `referenced_message` is the one optional field that can also be present and null. A message whose author user and webhook are both absent is omitted from every read.
:::note[A forward has immutable copies in `message_snapshots`]
A reply reference resolves into `referenced_message`, and a forward has none. Fluxer captures the snapshots at forward time, and they never follow later edits to the source. [Message reference types](#message-reference-types) tells the two apart.
@@ -131,7 +131,7 @@ A reply reference resolves into `referenced_message`, and a forward has none. Fl
<sup>1</sup> The only flag a guild moderator can change on a message they did not author
<sup>2</sup> Setting this flag binds the message to the voice message contract described under [Create message](#create-message)
<sup>2</sup> A message with this flag has the voice message restrictions listed under [Create message](#create-message)
These bits are also the complete sendable set. A create masks the supplied value down to them, and a modify replaces only them and leaves every other stored bit untouched. A bit outside this table is discarded, and the request still succeeds.
@@ -462,7 +462,7 @@ Every optional field above is omitted entirely.
## Embed provider object
Fluxer sets a provider only from resolved embed metadata.
Only an embed the unfurler builds from a link has a provider. A rich embed input cannot set one.
### Structure
@@ -660,7 +660,7 @@ One declared upload inside a [Request attachment upload URLs](#request-attachmen
<sup>1</sup> A decimal string is converted to the integer
<sup>2</sup> The issued capability and the stored attachment both use the media type derived from `filename`
<sup>2</sup> The issued upload URL and the stored attachment both use the media type derived from `filename`
### Singlepart attachment upload object
@@ -680,7 +680,7 @@ Returned when the declared `file_size` is at most 10 MiB.
<sup>1</sup> This value is the `upload_filename` of a [pre-uploaded attachment](#pre-uploaded-attachment-object) in a later [Create message](#create-message) or [Modify message](#modify-message) request
<sup>2</sup> The capability is bound to the derived media type and to exactly `file_size` bytes, so a `PUT` of any other length is rejected
<sup>2</sup> The upload URL accepts only the derived media type and exactly `file_size` bytes, so a `PUT` of any other length is rejected
### Multipart attachment upload object
@@ -698,7 +698,7 @@ Returned when the declared `file_size` exceeds 10 MiB.
| upload_mode | string | Always `multipart` |
| upload_id | string | Object storage multipart upload identifier of 1 through 1,024 characters |
| part_size<sup>1</sup> | integer | Byte size of every part except the last |
| parts | array[[attachment upload part](#attachment-upload-part-object) object] | Presigned part capabilities in ascending part number (1-10,000 entries) |
| parts | array[[attachment upload part](#attachment-upload-part-object) object] | Presigned part upload URLs in ascending part number (1-10,000 entries) |
<sup>1</sup> The plan targets 20 parts, so `part_size` is one twentieth of `file_size` rounded up to a whole mebibyte, and it is never smaller than 10 MiB. The last part is the remainder
@@ -713,7 +713,7 @@ One presigned part of a multipart upload plan.
| part_number | integer | One-based part number |
| upload_url<sup>1</sup> | string | Presigned URL that accepts exactly this part |
<sup>1</sup> Each capability is bound to the exact byte count of its part, so a `PUT` of any other length is rejected
<sup>1</sup> Each part upload URL accepts only the exact byte count of its part, so a `PUT` of any other length is rejected
### Multipart upload completion item object
@@ -758,7 +758,7 @@ The page has no cursor field. The next request repeats the operation with `befor
| Field | Type | Description |
| --- | --- | --- |
| items | array[[partial user](/http-api/users/#partial-user-object) object] | Users in ascending cursor order |
| items | array[[partial user](/http-api/users/#partial-user-object) object] | Users in ascending user ID order |
| has_more | boolean | Whether another page exists |
| next_after<sup>1</sup> | ?snowflake | User ID cursor for the next page |
@@ -842,7 +842,7 @@ Returns a bulk message response object holding bounded message windows from seve
### Limitations
- This is a user-only operation, and a bot credential is refused with 403 `ACCESS_DENIED`.
- Fluxer resolves and authorises each requested channel independently, under exactly the same boundary as [List channel messages](#list-channel-messages).
- Fluxer resolves and authorises each requested channel independently, with exactly the same channel checks as [List channel messages](#list-channel-messages).
- An inaccessible channel fails the whole request, so the response has no window for any channel.
### JSON body
@@ -915,7 +915,7 @@ Plans from 1 through 10 attachment uploads. Returns a [singlepart](#singlepart-a
- The channel must support messages, and one that does not fails with 400 `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL`.
- A guild channel requires [SEND_MESSAGES](/http-api/permissions/) and [ATTACH_FILES](/http-api/permissions/), and a timed-out member is refused with 403 `COMMUNICATION_DISABLED`.
Fluxer checks each declared `file_size` separately against the attachment size limit it resolves for the caller and the guild context. A declaration above that limit returns 400 `FILE_SIZE_TOO_LARGE` with the resolved ceiling. The resolved ceiling defaults to 26214400 bytes, the 25 MiB non-premium allowance, and 524288000 bytes, the 500 MiB premium allowance. A bot credential is clamped to 52428800 bytes, the 50 MiB bot ceiling, even when the resolved limit is higher.
Fluxer checks each declared `file_size` separately against the attachment size limit it resolves for the caller and the guild context. A declaration above that limit returns 400 `FILE_SIZE_TOO_LARGE` with the resolved ceiling. By default the resolved ceiling is 26214400 bytes (25 MiB) for a caller without premium and 524288000 bytes (500 MiB) for a caller with premium. A bot credential is clamped to 52428800 bytes, the 50 MiB bot ceiling, even when the resolved limit is higher.
### Path parameters
@@ -1028,7 +1028,7 @@ Creates a message from a JSON body or from multipart form data. Returns the crea
### Limitations
- The channel must be a text-bearing type.
- A guild channel requires [VIEW_CHANNEL](/http-api/permissions/) and [SEND_MESSAGES](/http-api/permissions/), an untimed-out membership, and any membership verification the guild requires.
- A guild channel requires [VIEW_CHANNEL](/http-api/permissions/) and [SEND_MESSAGES](/http-api/permissions/), an untimed-out membership, and a membership that meets the guild's [verification level](/http-api/guilds/#verification-levels).
- A timed-out member is refused with 403 `COMMUNICATION_DISABLED`.
- Embeds require [EMBED_LINKS](/http-api/permissions/), attachments require [ATTACH_FILES](/http-api/permissions/), a favourite meme requires both, and active everyone mentions require [MENTION_EVERYONE](/http-api/permissions/).
- Slowmode applies to a non-bot caller unless they hold [BYPASS_SLOWMODE](/http-api/permissions/).
@@ -1061,7 +1061,7 @@ The request body is read as a multipart form when `Content-Type` contains `multi
<sup>1</sup> Echoed back on the created message and on its [Message Create](/gateway/events/#message-create) Dispatch so a client can match the result to its optimistic entry. An integer is converted to its decimal string form
<sup>2</sup> The schema sets no length bound. Exceeding the effective one returns 400 `INVALID_FORM_BODY` with `CONTENT_EXCEEDS_MAX_LENGTH` on the path `content`
<sup>2</sup> The schema sets no length bound. Exceeding the effective `max_message_length` described below returns 400 `INVALID_FORM_BODY` with `CONTENT_EXCEEDS_MAX_LENGTH` on the path `content`
<sup>3</sup> Fluxer keeps only the bits in [message flags](#message-flags) and silently drops every other bit
@@ -1091,7 +1091,7 @@ A nonce is remembered for the authenticated identity for five minutes, so a clie
Reusing a nonce in the same channel inside that window returns the message the first request created. Reusing it in a different channel fails with 404 `UNKNOWN_MESSAGE`. The replay check runs after authorisation and body validation, so a retry that is otherwise invalid still fails, and a retry after the window has passed creates a second message.
A reply applies the caller's message history cutoff to its target, and a forward applies none to its source. A reply whose target is a system message fails with the field code `CANNOT_REPLY_TO_SYSTEM_MESSAGE`, and a reference that resolves to no message fails with 404 `UNKNOWN_MESSAGE`.
A reply from a guild caller without [READ_MESSAGE_HISTORY](/http-api/permissions/) fails with 404 `UNKNOWN_MESSAGE` when its target was created before the guild's message history cutoff, or when the guild has no cutoff. A forward has no cutoff check on its source. A reply whose target is a system message fails with the field code `CANNOT_REPLY_TO_SYSTEM_MESSAGE`, and a reference that resolves to no message fails with 404 `UNKNOWN_MESSAGE`.
Every `FORWARD` reference has `channel_id`. One that does not is refused by body validation before the operation runs, with 400 `INVALID_MESSAGE_DATA` on a JSON body and a per-field 400 `INVALID_FORM_BODY` on a multipart body.
@@ -1127,7 +1127,7 @@ A body that cannot be parsed as a multipart form fails with the field code `FAIL
| More than one file under one index | `MULTIPLE_FILES_FOR_INDEX_NOT_ALLOWED` |
| More files than the resolved limit | `TOO_MANY_FILES` |
Each is a field code inside a 400 `INVALID_FORM_BODY` body. A `FILE_INDEX_EXCEEDS_MAXIMUM` element reports a `maxIndex` member, 10000 against the absolute bound and the resolved limit minus one otherwise.
Each is a field code inside a 400 `INVALID_FORM_BODY` body. A `FILE_INDEX_EXCEEDS_MAXIMUM` element has a `maxIndex` member. It is 10000 when the index is below 0 or above 10000, and the resolved limit minus one when the index is at or above that limit.
The `attachments` array inside `payload_json` uses [direct multipart attachment metadata](#direct-multipart-attachment-metadata-object), and an entry with `upload_filename` is instead read as a [pre-uploaded attachment](#pre-uploaded-attachment-object). An entry whose `id` matches a supplied file index supplies that file's metadata, and two entries claiming the same file fail with the field code `DUPLICATE_ATTACHMENT_IDS_NOT_ALLOWED`.
@@ -1346,7 +1346,7 @@ Deletes one attachment from the caller's own message. Returns 204 with an empty
- Only the message author can use this operation, and a guild moderator must use [Modify message](#modify-message) instead.
- This route cannot address an attachment that a resolved embed owns.
- When the attachment was the message's last remaining payload, the [Delete message](#delete-message) effects apply.
- When the attachment is the message's only attachment and the message has no content, embeds, or stickers, Fluxer deletes the whole message with the [Delete message](#delete-message) effects.
### Path parameters
@@ -1370,7 +1370,7 @@ The response is 204 whether the message survives or is deleted, so the emitted G
### Side effects
The operation permanently deletes the stored attachment object, purges it from media delivery, removes it from the message, and advances the message's edit timestamp. It then emits [Message Update](/gateway/events/#message-update) to every session that can see the channel. When the removal leaves the message with no payload, the complete [Delete message](#delete-message) effect set applies instead, including its Dispatches and guild audit log entry.
The operation permanently deletes the stored attachment object, purges it from media delivery, removes it from the message, and advances the message's edit timestamp. It then emits [Message Update](/gateway/events/#message-update) to every session that can see the channel. When the removed attachment was the message's only attachment and the message has no content, embeds, or stickers, Fluxer deletes the whole message instead, with the complete [Delete message](#delete-message) effect set, including its Dispatches and guild audit log entry.
### Rate limit
@@ -1539,7 +1539,7 @@ The deletion has no age boundary and no selection. There is no cancellation and
### Side effects
Each matching message, with its attachments and reactions, is permanently deleted and removed from search. Deletions emit batched [Message Delete Bulk](/gateway/events/#message-delete-bulk) Dispatches.
Each matching message, with its attachments and reactions, is permanently deleted and removed from search. Fluxer emits one [Message Delete Bulk](/gateway/events/#message-delete-bulk) Dispatch for each batch of at most 100 deleted messages.
The operation emits no [Channel Pins Update](/gateway/events/#channel-pins-update) and no individual [Message Delete](/gateway/events/#message-delete), and it writes no guild audit log entry.
@@ -1810,7 +1810,7 @@ Adds the authenticated identity's reaction. Returns 204 with an empty body. Emit
### Limitations
- The caller must be able to view the text-bearing channel, must not be timed out, and must reach the message under the message history cutoff.
- The caller must be able to view the text-bearing channel, must not be timed out. A guild caller without [READ_MESSAGE_HISTORY](/http-api/permissions/) can react only to a message on or after the guild's message history cutoff, and to no message when the guild has no cutoff.
- A non-bot caller must have a verified email, and one that does not receives 403 `REACTION_EMAIL_VERIFICATION_REQUIRED`.
- Creating a new emoji reaction group in a guild requires [ADD_REACTIONS](/http-api/permissions/), while adding to an existing group does not.
- A custom emoji whose source guild is not the channel's guild requires the `feature_global_expressions` entitlement, and a caller without it receives the field code `CUSTOM_EMOJIS_REQUIRE_PREMIUM_OUTSIDE_SOURCE`.
@@ -1836,7 +1836,7 @@ Adds the authenticated identity's reaction. Returns 204 with an empty body. Emit
<sup>1</sup> See [reaction session correlation](#reaction-emoji-path-value)
:::note[Permission is checked only for a new group]
`ADD_REACTIONS`, Unicode emoji validation, custom emoji existence, and the external emoji checks apply only when the request would open a new group. Joining an existing group is authorised by channel access alone.
`ADD_REACTIONS`, Unicode emoji validation, custom emoji existence, and the external emoji checks apply only when the request would open a new group. Joining an existing group skips them. Every other check in the list above still runs, including the timeout, the message history cutoff, and email verification.
:::
[Reaction object](#reaction-object) states the group and user ceilings for a message, and reaching either returns 400 `MAX_REACTIONS`.
@@ -10,7 +10,7 @@ OAuth2 is how a user grants an application access to their account. The same con
## Authorisation code grant
The grant begins at [Authorise application](#authorise-application). That route redirects the browser to the Fluxer consent interface, unless the caller asked for a silent authorisation. [Grant OAuth2 consent](#grant-oauth2-consent) records the decision, issues one authorisation code, and returns the callback URL that has it. The client presents that code to [Exchange OAuth2 token](#exchange-oauth2-token) with its client credentials and receives an access and refresh token pair.
The grant begins at [Authorise application](#authorise-application). That route redirects the browser to the Fluxer consent interface, unless the request sets `prompt=none`. [Grant OAuth2 consent](#grant-oauth2-consent) records the decision, issues one authorisation code, and returns the callback URL that has it. The client presents that code to [Exchange OAuth2 token](#exchange-oauth2-token) with its client credentials and receives an access and refresh token pair.
Proof Key for Code Exchange, or PKCE, supports the `S256` and `plain` challenge methods, and Fluxer binds the challenge to the code when it issues the code. A `code_challenge` supplied without a `code_challenge_method` is treated as `plain`. An exchange for a code that has a challenge supplies a matching `code_verifier`, and an exchange for a code that has none ignores any verifier it is given.
@@ -18,11 +18,11 @@ Proof Key for Code Exchange, or PKCE, supports the `S256` and `plain` challenge
Treat authorisation codes, client secrets, and tokens as opaque credentials. Retrying an interrupted exchange with the same inputs is rejected as an invalid grant. Restart the grant instead.
An authorisation code expires 10 minutes after it is issued, an access token after 7 days, and a refresh token after 30 days. Every refresh exchange issues a replacement with a new 30 day life, so a grant ends 30 days after its last refresh.
An authorisation code expires 10 minutes after it is issued, an access token after 7 days, and a refresh token after 30 days. Every refresh exchange issues a new refresh token that expires 30 days after it is issued, so a grant ends 30 days after its last refresh.
## Route authentication
Every route with a delegated identity is user-only unless the operation says otherwise. Fluxer rejects a bot token and an OAuth2 bearer with 403 `ACCESS_DENIED`, and accepts an account with an outstanding required action. The token, introspection, and revocation routes authenticate the client application instead, so their [rate limit](/topics/rate-limits/) is keyed by client IP address.
Every route on this page that authenticates a user is user-only unless the operation says otherwise. Fluxer rejects a bot token and an OAuth2 bearer with 403 `ACCESS_DENIED`, and accepts an account with an outstanding required action. The token, introspection, and revocation routes authenticate the client application instead, so their [rate limit](/topics/rate-limits/) is keyed by client IP address.
## OAuth2 string normalisation
@@ -58,7 +58,7 @@ An unrecognised scope value is rejected when the authorisation request is comple
Scope order has no semantic meaning, so a client MUST compare scopes as a set.
:::
Fluxer sorts the `scope` string of an [OAuth2 token](#oauth2-token-object) and of an [OAuth2 introspection](#oauth2-introspection-object) into the registry order above. The `scopes` array of a [current OAuth2 authorisation](#current-oauth2-authorisation-object) has the stored scopes in no defined order, with unrecognised values removed. The `scopes` array of an [OAuth2 authorisation](#oauth2-authorisation-object) is in no defined order, and it can hold `bot` and an unrecognised value once more than one refresh token contributes.
Fluxer sorts the `scope` string of an [OAuth2 token](#oauth2-token-object) and of an [OAuth2 introspection](#oauth2-introspection-object) into the registry order above. The `scopes` array of a [current OAuth2 authorisation](#current-oauth2-authorisation-object) has the stored scopes in no defined order, with unrecognised values removed. The `scopes` array of an [OAuth2 authorisation](#oauth2-authorisation-object) is in no defined order, and it can hold `bot` or an unrecognised value when the user holds more than one live refresh token for the application.
## OAuth2 error response object
@@ -73,7 +73,7 @@ The failure envelope the OAuth2 routes return.
<sup>1</sup> Always present, and some descriptions are a bare API error code identifier
These members are the whole body, so the routine that reads the ordinary [error response](/http-api/#error-response) cannot parse it. Fluxer returns every OAuth2 failure with status 400. A client MUST key on `error` and never on `error_description`.
These members are the whole body, so the body has none of the `code` and `message` members of the ordinary [error response](/http-api/#error-response). Fluxer returns every OAuth2 failure with status 400. A client MUST key on `error` and never on `error_description`.
The consent, token, introspection, and revocation routes return this object for a grant, client authentication, scope, redirect, response type, or permission mask failure. Every other failure, including request validation, authentication, authorisation, and rate limiting, uses the ordinary [error response](/http-api/#error-response) with an [API error code](/http-api/errors/).
@@ -191,7 +191,7 @@ The liveness and scope report Fluxer returns for one presented token.
Apart from `username` and `exp`, every optional field above is present when `active` is true and absent when it is false. An active access token has `exp` and an active refresh token does not.
Fluxer reports an unknown token, an expired token, and a token issued to another application as inactive. An inactive body has only `active` set to false, and names no reason. Introspection never resolves the account behind the token, so an active result reports on the token alone.
Fluxer reports an unknown token, an expired token, and a token issued to another application as inactive. An inactive body has only `active` set to false, and names no reason. Fluxer does not look up the user account during introspection, so an active result describes the token only and says nothing about the state of that account.
## Current OAuth2 authorisation object
@@ -206,7 +206,7 @@ The application, scopes, and expiry behind the access token the caller presented
| expires | ISO8601 timestamp | The expiry time of the access token |
| user?<sup>2</sup> | [OAuth2 authorised user](#oauth2-authorised-user-object) object | The account the access token was issued for |
<sup>1</sup> Emitted in no defined order, and `bot` is retained here even though it grants no delegated read
<sup>1</sup> Emitted in no defined order, and `bot` stays in the array even though it grants no bearer access
<sup>2</sup> Present only when the token grants `identify` and the account still exists, so a token restricted to `bot` never has one
@@ -295,7 +295,7 @@ The application named by an [OAuth2 authorisation](#oauth2-authorisation-object)
<sup>1</sup> Null when the application has no bot account or that account has no avatar. The stored hash is reported unfiltered, so an animated hash retains its `a_` prefix
<sup>2</sup> An application has no independent description
<sup>2</sup> An application record has no description field, so this value is always null
## OAuth2 consent response object
@@ -309,7 +309,7 @@ The callback URL [Grant OAuth2 consent](#grant-oauth2-consent) returns after it
<sup>1</sup> The URL has the `code` query parameter, and the normalised caller `state` when one was supplied
Fluxer builds the URL from the registered redirect URI the request resolved to, which is the Fluxer application endpoint when a bot-only authorisation supplied none. The code records the URL serialisation of that resolved URI, and the client later presents that exact serialisation to [Exchange OAuth2 token](#exchange-oauth2-token).
Fluxer builds the URL from the registered redirect URI the request resolved to, which is the configured Fluxer web app URL when a bot-only authorisation supplied none. The code records the URL serialisation of that resolved URI, and the client later presents that exact serialisation to [Exchange OAuth2 token](#exchange-oauth2-token).
## Authorise application
@@ -336,7 +336,7 @@ Starts an authorisation request. Fluxer returns a redirect for every query that
<sup>1</sup> The value must parse as an absolute URL with a scheme and a host, and must match a registered redirect URI before a code is issued
<sup>2</sup> Both values are forwarded to the consent interface unchanged, and their mutual exclusion is enforced by [Grant OAuth2 consent](#grant-oauth2-consent)
<sup>2</sup> Both values are forwarded to the consent interface unchanged, and only [Grant OAuth2 consent](#grant-oauth2-consent) rejects a request that supplies both
<sup>3</sup> The value is forwarded to the consent interface unchanged and is parsed as an integer only by [Grant OAuth2 consent](#grant-oauth2-consent)
@@ -360,7 +360,7 @@ An error redirect has `error` and the normalised `state` when the caller supplie
The error redirect targets the supplied `redirect_uri` only when the named application has registered that exact string, so this route cannot bounce a browser to an arbitrary origin.
:::
Fluxer compares the two strings byte for byte, so a differing scheme, port, trailing slash, query, or fragment does not match. Every other case redirects to the Fluxer application endpoint. That covers an omitted redirect URI, an unknown application, and an application with no registered URI at all.
Fluxer compares the two strings byte for byte, so a differing scheme, port, trailing slash, query, or fragment does not match. Every other case redirects to the configured Fluxer web app URL. That covers an omitted redirect URI, an unknown application, and an application with no registered URI at all.
### Side effects
@@ -447,7 +447,7 @@ The request body is `application/x-www-form-urlencoded` or `multipart/form-data`
A refresh exchange consumes the presented refresh token, so a client that loses the response MUST restart the authorisation code grant.
:::
Earlier access tokens for the same user and application survive a refresh and expire on their own schedule.
Access tokens issued earlier for the same user and application stay valid after a refresh. Each one expires 7 days after it is issued.
### Request headers
@@ -619,11 +619,11 @@ Revoking a token deletes every access token and every refresh token its user hol
| client_id?<sup>2</sup> | snowflake | The client ID, supplied when HTTP Basic is not used |
| client_secret?<sup>2</sup> | string | The client secret, supplied when HTTP Basic is not used (1-256 UTF-16 code units after [normalisation](#oauth2-string-normalisation)) |
<sup>1</sup> Only `refresh_token` matches the presented value against refresh tokens, and access tokens are still tried when no refresh token matches. An absent hint and an `access_token` hint match access tokens only
<sup>1</sup> A `refresh_token` hint makes Fluxer look for a matching refresh token first, then for a matching access token when no refresh token matches. An absent hint and an `access_token` hint make Fluxer look for a matching access token only
<sup>2</sup> Supplying either field together with a well-formed HTTP Basic `Authorization` header is rejected as `invalid_request`
A client that presents a refresh token MUST send `token_type_hint=refresh_token`, or the revocation takes no effect.
A client that presents a refresh token MUST send `token_type_hint=refresh_token`, or Fluxer matches no token and deletes nothing.
### Response
@@ -34,7 +34,7 @@ Every bit Fluxer defines appears below.
| 1 &lt;&lt; 13 | MANAGE_MESSAGES<sup>4</sup> | Delete another member's message, bulk delete messages, remove another member's reaction, and edit the flags and attachments of another member's message |
| 1 &lt;&lt; 14 | EMBED_LINKS | Have a link in the caller's own message expanded into an embed |
| 1 &lt;&lt; 15 | ATTACH_FILES | Attach a file to a message |
| 1 &lt;&lt; 16 | READ_MESSAGE_HISTORY | Read messages that were sent before the current access began |
| 1 &lt;&lt; 16 | READ_MESSAGE_HISTORY | Read messages sent before the guild's `message_history_cutoff`, or any message when the guild has no cutoff |
| 1 &lt;&lt; 17 | MENTION_EVERYONE | Use the everyone and here mentions in a guild channel |
| 1 &lt;&lt; 18 | USE_EXTERNAL_EMOJIS | Use an emoji owned by another guild in a message or a reaction |
| 1 &lt;&lt; 20 | CONNECT | Connect to a guild voice channel |
@@ -105,7 +105,7 @@ An operation that asserts an elevated permission the caller holds but cannot use
[List guild roles](#list-guild-roles) asserts no permission, so it never produces `TWO_FACTOR_REQUIRED`.
:::note[The reported mask keeps every elevated bit]
Elevation gates the operation. A `permissions` field reports what the caller's roles grant, so a client hiding an action must compare the guild [MFA level](/http-api/guilds/#mfa-levels) and its own enrolment state.
The MFA requirement blocks the operation and leaves the mask unchanged. A `permissions` field reports what the caller's roles grant, so a client hiding an action must compare the guild [MFA level](/http-api/guilds/#mfa-levels) and its own enrolment state.
:::
## Permission computation
@@ -158,7 +158,7 @@ A caller manages a role when the caller's rank is strictly above the role's rank
Fluxer enforces hierarchy independently of `MANAGE_ROLES`. A caller who holds the permission without outranking the target role receives 403 `MISSING_PERMISSIONS`. A caller whose only `MANAGE_ROLES` comes from the everyone role has no assigned role, so they can manage no role.
:::caution[A non-owner cannot grant a permission they lack]
A requested bit outside the caller's own effective guild permissions fails the whole request with `MISSING_PERMISSIONS`. Fluxer runs that check after it resolves the unassigned and feature-gated bits.
A requested bit outside the caller's own effective guild permissions fails the whole request with `MISSING_PERMISSIONS`. Fluxer runs that check after it discards unassigned bits and handles feature-gated bits as [Feature-gated permission bits](#feature-gated-permission-bits) describes.
:::
## Guild role object
@@ -213,7 +213,7 @@ Each entry names one role and the rank the caller requests for it.
| id | snowflake | The ID of the role |
| position?<sup>1</sup> | integer | The rank the caller requests for the role, where a larger value ranks higher |
<sup>1</sup> Fluxer ranks a role whose entry omits the field by its stored `position` in the same sort, so a role that requested a position above that value still displaces it
<sup>1</sup> Fluxer sorts a role whose entry omits the field by its stored `position`, in the same sort as the other entries. A role whose entry requests a higher position than that stored value is placed above it
### Example
@@ -304,7 +304,7 @@ Creates a role and returns its [guild role object](#guild-role-object). Requires
| color? | integer | The colour of the role as a 24-bit RGB integer (0-16777215, default 0) |
| permissions?<sup>1</sup> | decimal string \| integer | The [permission](#permissions) bitfield the role grants |
<sup>1</sup> An omitted field copies the current permissions of the everyone role unchanged. A supplied value is intersected with the set of defined bits, resolved against the caller's [feature declaration](#feature-gated-permission-bits), then checked against the caller's own permissions
<sup>1</sup> An omitted field copies the current permissions of the everyone role unchanged. A supplied value is intersected with the set of defined bits, Fluxer clears `VIEW_CHANNEL_MEMBERS` unless the caller's [feature declaration](#feature-gated-permission-bits) includes `view_channel_members_permission`, then checks the value against the caller's own permissions
### Response
@@ -363,7 +363,7 @@ Every field is optional, and an omitted field leaves the stored value unchanged.
<sup>1</sup> The field is accepted and then ignored when the target is the everyone role, which cannot be renamed, hoisted, given a member list position, or made mentionable
<sup>2</sup> The supplied value becomes the complete stored mask. Fluxer intersects it with the set of defined bits and resolves it against the caller's [feature declaration](#feature-gated-permission-bits) before checking the caller's authority
<sup>2</sup> The supplied value becomes the complete stored mask. Before it checks the caller's authority, Fluxer intersects it with the set of defined bits and keeps each feature-gated bit at its stored value unless the caller's [feature declaration](#feature-gated-permission-bits) names the feature for that bit
### Response
@@ -543,7 +543,7 @@ A deleted role cannot be restored, and its permissions, colour, and hoist state
### Side effects
The operation removes the role from every member that held it, removes the role record, and frees the guild role slot it occupied. It creates a [ROLE_DELETE](/http-api/guild-audit-logs/#audit-actions) guild audit log entry with the supplied audit reason and emits [Guild Role Delete](/gateway/events/#guild-role-delete) to every session that can see the guild. Subsequent member reads and [Guild Member Update](/gateway/events/#guild-member-update) payloads reflect the removed role.
The operation removes the role from every member that held it, removes the role record, and frees the guild role slot it occupied. It creates a [ROLE_DELETE](/http-api/guild-audit-logs/#audit-actions) guild audit log entry with the supplied audit reason and emits [Guild Role Delete](/gateway/events/#guild-role-delete) to every session that can see the guild. The deleted role no longer appears in `roles` on a later member read or [Guild Member Update](/gateway/events/#guild-member-update) payload.
### Rate limit
@@ -174,7 +174,7 @@ The complete value is null in each of these cases.
| No future phase | The schedule has no phase starting after now |
| No target cycle | Neither the future phase price nor the schedule metadata resolves it |
| No change | The resolved target billing cycle equals the current one |
| Read failed | The payment provider read failed, which is logged and absorbed |
| Read failed | The payment provider read failed, and the request still succeeds |
### Example
@@ -195,7 +195,7 @@ The complete value is null in each of these cases.
## Premium state object
The account's entitlement, the premium checks that apply to it, 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.
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.
A client gates a premium feature on `effective.is_premium`.
@@ -260,7 +260,7 @@ The effective state decides whether premium features are available. It differs f
| Field | Type | Description |
| --- | --- | --- |
| is_premium<sup>1</sup> | boolean | Whether premium access checks are active |
| is_premium<sup>1</sup> | boolean | Whether premium features are available to the account |
| premium_type<sup>2</sup> | ?integer | The effective [premium type](#premium-types) |
| premium_since<sup>2</sup> | ?ISO8601 timestamp | The effective time premium access started |
| premium_until<sup>3</sup> | ?ISO8601 timestamp | The time premium access ends, including any stacked gift extension, or null for a lifetime entitlement |
@@ -299,7 +299,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 |
| subscription | ?[billing subscription](#billing-subscription-object) object | The most relevant mirrored subscription |
| 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 |
| payment_methods<sup>3</sup> | array[[billing payment method](#billing-payment-method-object)] | The stored payment methods |
@@ -444,7 +444,7 @@ The checkout catalogue resolved for the request. A client can render prices with
<sup>1</sup> The request-time geolocation country, falling back to the `country_code` query value uppercased, or null when neither resolves
<sup>2</sup> Null unless the deployment configures a complete recurring pair and a complete gift pair for the currency preferences that apply, so one missing gift price nulls the whole catalogue. The amounts come from the mirrored price rows
<sup>2</sup> Null unless the deployment configures a complete recurring pair and a complete gift pair for a currency in the currency preference order of `country_code`, which is the default order when `country_code` is null, so one missing gift price nulls the whole catalogue. The amounts come from the mirrored price rows
Fluxer reports an unresolvable catalogue as null here, and the request still succeeds. [Get price IDs](#get-price-ids) answers 400 `STRIPE_ERROR` for the same condition.
@@ -452,7 +452,7 @@ Fluxer reports an unresolvable catalogue as null here, and the request still suc
<RouteHeader method="GET" path="/v1/premium/price-ids" unauthenticated />
Returns the [price IDs](#price-ids-object) object for a country. A supplied credential only keys the rate limit, so an unrecognised credential falls back to the client IP address.
Returns the [price IDs](#price-ids-object) object for a country. Fluxer uses a supplied credential only to count the request against that user's rate limit, so an unrecognised credential falls back to the client IP address.
The deployment must configure a complete recurring pair and a complete gift pair for at least one currency that the requested country prefers. Fluxer answers 400 `STRIPE_ERROR` when it does not.
@@ -484,7 +484,7 @@ Returns the [premium state](#premium-state-object) object for the authenticated
The route is available even when no payment provider is configured.
:::note[One read serves the whole billing screen]
`billing.current_subscription_price` and `billing.refund_eligibility` cover the same ground as [get current subscription price](#get-current-subscription-price) and [get refund eligibility](/http-api/billing/#get-refund-eligibility), and `billing.pending_subscription_change` has no route of its own.
`billing.current_subscription_price` and `billing.refund_eligibility` are the same objects returned by [get current subscription price](#get-current-subscription-price) and [get refund eligibility](/http-api/billing/#get-refund-eligibility), and `billing.pending_subscription_change` has no route of its own.
:::
:::note[Billing details may take time to update]
@@ -508,7 +508,7 @@ The route is available even when no payment provider is configured.
### Side effects
The read can update the customer's default payment method to match the subscription. It does not change entitlement.
The read can change a default payment method. When the customer has a default that differs from the subscription's, Fluxer sets the subscription's default to the customer's. When only the subscription has a default, Fluxer sets it as the customer's default. It does not change entitlement.
### Rate limit
@@ -568,7 +568,7 @@ When the requested value changes, every account session receives [User Update](/
<RouteHeader method="POST" path="/v1/premium/customer-portal" />
Creates a billing portal session for the authenticated account and returns a [redirect URL](/http-api/billing/#redirect-url-object) object. The returned URL is the only handle on the session.
Creates a billing portal session for the authenticated account and returns a [redirect URL](/http-api/billing/#redirect-url-object) object. The object has no session identifier, so the returned URL is the only way to open the session.
An account that never had a payment provider customer receives 400 `STRIPE_NO_PURCHASE_HISTORY`. A deployment that configures no payment provider receives 400 `STRIPE_PAYMENT_NOT_AVAILABLE`, and a provider call that fails or answers without a URL receives 400 `STRIPE_ERROR`.
@@ -651,7 +651,7 @@ A deployment that configures no payment provider receives 400 `STRIPE_PAYMENT_NO
### Side effects
Fluxer sets the subscription to cancel at the current period end, sets the account's cancellation flag, and refreshes the mirrored subscription row. Every account session receives [User Update](/gateway/events/#user-update). The ordinary path writes no premium end, so the recorded end is whatever the account already has.
Fluxer sets the subscription to cancel at the current period end, sets the account's cancellation flag, and refreshes the mirrored subscription row. Every account session receives [User Update](/gateway/events/#user-update). For a subscription with no schedule, Fluxer writes no premium end, so the recorded end is whatever the account already has.
A subscription that has a schedule discards its pending billing cycle change and ends in cancellation at the current period end. Fluxer then rewrites the premium end from the refreshed subscription. Premium access continues through the recorded end, and [receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) later applies the final downgrade.
@@ -722,7 +722,7 @@ A monthly to yearly change credits the first invoice of the new phase with the s
### Side effects
An immediate change clears pending cancellation, replaces the subscription price, and updates the billing cycle and premium end. A period end change clears pending cancellation and schedules the target cycle to begin at the current period end without proration. A monthly to yearly change adds the applicable credit to the scheduled phase.
An immediate change clears pending cancellation, replaces the subscription price, and updates the billing cycle and premium end. A period end change clears pending cancellation and schedules the target cycle to begin at the current period end without proration. A monthly to yearly change credits the first invoice of the scheduled phase with the smaller of the two amounts, except during a trial.
A scheduled change becomes visible as `billing.pending_subscription_change` in [get premium state](#get-premium-state). Every account session receives [User Update](/gateway/events/#user-update). Requesting the already active cycle changes nothing.
@@ -754,7 +754,7 @@ A subscription set to cancel has no next billing period, so the switch would hav
| 400<sup>1</sup> | [error response](/http-api/#error-response) | The payment provider is not configured, the account has no stored subscription, or the provider rejected the request |
| 404 | [error response](/http-api/#error-response) | The authenticated account record no longer exists and the request returns `UNKNOWN_USER` |
<sup>1</sup> A refusal that the account can act on is reported as a 200 with `status` `ineligible`, not as an error
<sup>1</sup> A missing payment provider returns `STRIPE_PAYMENT_NOT_AVAILABLE` and a missing stored subscription returns `STRIPE_NO_ACTIVE_SUBSCRIPTION`, so this route never returns the `feature_unavailable` or `no_active_subscription` reason. Every other refusal is a 200 with `status` `ineligible`
### Switch result
@@ -54,7 +54,7 @@ Only an acknowledgement produces a later Dispatch. A message acknowledgement emi
## Read state acknowledgement object
One entry in the body of [Acknowledge read states](#acknowledge-read-states). It names the channel whose entry changes, the message the account has read through, and the mention count that survives the read.
One entry in the body of [Acknowledge read states](#acknowledge-read-states). It names the channel whose entry changes, the message the account has read through, and the mention count to store for the channel.
### Structure
@@ -69,7 +69,7 @@ One entry in the body of [Acknowledge read states](#acknowledge-read-states). It
<sup>2</sup> Fluxer does not resolve the message either. An entry equal to the stored watermark still produces a write, so it resets the stored mention count
<sup>3</sup> Fluxer stores the submitted value verbatim, so an acknowledgement leaving mentions above the watermark must send how many remain
<sup>3</sup> Fluxer stores the submitted value verbatim, so a client that leaves unread mentions after `message_id` must send the number of those mentions
<sup>4</sup> With `manual` false an entry strictly below the stored watermark is discarded whole
@@ -135,13 +135,13 @@ When any entry is manual or has a positive mention count, entries for the same c
### Side effects
Each entry updates its channel watermark according to `manual` and sets the submitted mention count.
With `manual` true, an entry sets the channel watermark to `message_id`. With `manual` false, Fluxer discards an entry strictly below the stored watermark and leaves the stored entry unchanged. Every entry that is not discarded sets the watermark to `message_id` and stores the submitted mention count.
One [Message ACK](/gateway/events/#message-ack) goes to the caller's sessions for every submitted entry, including an unchanged one. It reports the resulting `message_id`, `mention_count`, and `version`. It also includes the submitted `manual` value when any entry is manual or has a positive mention count.
A successful acknowledgement can have no corresponding Dispatch. Reconcile from a later read when needed.
When Fluxer fails to send a Message ACK, the request still succeeds and no Dispatch arrives for that entry. A client reconciles from the entries this route returns or from a new Ready.
Every entry also clears delivered notifications for its channel through the submitted `message_id`, so a discarded or rewound entry clears a different range than the entry stores.
Every entry also clears delivered push notifications for its channel through the submitted `message_id`. Fluxer uses the submitted value even when it discards the entry and keeps a higher stored watermark.
### Rate limit
@@ -29,7 +29,7 @@ Fluxer copies the reported content when the report is submitted. A message repor
The Digital Services Act routes require no credential. Fluxer still resolves a valid credential presented anyway, so the request draws on that account's rate limit allowance.
Every route bucket is keyed by the authenticated user ID when a credential resolves and by client IP address otherwise. Report creation applies further policy allowances keyed by the reporter.
Every route bucket is keyed by the authenticated user ID when a credential resolves and by client IP address otherwise. Report creation also limits how many reports one reporter creates each hour. The rate limit section of each route that creates a report lists those limits.
## Content screening
@@ -57,7 +57,7 @@ Fluxer returns this receipt when it records a submission.
<sup>1</sup> Always `pending`
<sup>2</sup> A Digital Services Act notice has its filing time, even when its verification code was issued much earlier
<sup>2</sup> For a Digital Services Act notice, this is the time the notice was filed, even when its verification code was issued much earlier
### Example
@@ -250,7 +250,7 @@ A reporter without `READ_MESSAGE_HISTORY` can report only a message at or after
The operation writes one report with a frozen evidence snapshot. The snapshot records the reporter's account ID and email address, the message author's identity and avatar hash, the channel and guild identity, and the guild and channel NSFW and content warning state.
The snapshot also records a conversation window of up to 25 messages before the reported message, the reported message itself, and up to 25 messages after it. Each context message records its author identity, content, timestamps, type, flags, mentions, embeds, and sticker items, and each attachment is copied byte for byte. A context message whose author no longer exists is dropped from the window, and an attachment whose copy fails is dropped without failing the request. A reporter without `READ_MESSAGE_HISTORY` receives a window trimmed to the guild's message history cutoff.
The snapshot also records a conversation window of up to 25 messages before the reported message, the reported message itself, and up to 25 messages after it. Each context message records its author identity, content, timestamps, type, flags, mentions, embeds, and sticker items, and each attachment is copied byte for byte. A context message whose author no longer exists is dropped from the window, and an attachment whose copy fails is dropped without failing the request. When the reporter lacks `READ_MESSAGE_HISTORY`, the window holds only messages created at or after the guild's message history cutoff, and holds no messages when the guild sets no cutoff.
### Rate limit
@@ -503,7 +503,7 @@ An unknown code returns 404 `UNKNOWN_INVITE`. A code that resolves to an invite
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [report](#report-object) object | The notice was recorded |
| 400 | [error response](/http-api/#error-response) | The ticket is unknown or expired and the request returns `INVALID_DSA_TICKET`, or the target could not be resolved to the reported subject and the request returns `INVALID_DSA_REPORT_TARGET` |
| 400 | [error response](/http-api/#error-response) | The ticket is unknown or expired and the request returns `INVALID_DSA_TICKET`, or a supplied Fluxer tag or invite code is malformed or does not match the reported message author, user, or guild and the request returns `INVALID_DSA_REPORT_TARGET` |
| 403 | [error response](/http-api/#error-response) | A body string is blocked by content moderation and the request returns `CONTENT_BLOCKED` |
| 404 | [error response](/http-api/#error-response) | The referenced channel, message, user, guild, or invite does not exist |
@@ -10,7 +10,7 @@ Message search matches indexed messages against a text query and a filter set. O
Fluxer resolves the requested scope to the set of channels the caller may search, then runs the query over that set. A private channel search covers the authenticated account alone, and the set holds a guild channel only while the caller can read its history.
Term matching and typo tolerance can vary by deployment. Filter matching is exact.
Term matching and typo tolerance depend on the instance's search backend, Elasticsearch or Meilisearch. Filter matching is exact.
## Message search result object
@@ -31,9 +31,9 @@ The body a completed search returns, holding one page of matching messages.
<sup>2</sup> Contains one entry for each distinct channel represented in `messages`, and never contains a channel with no returned message
<sup>3</sup> Counted over the whole resolved scope. A single channel context without [READ_MESSAGE_HISTORY](/http-api/permissions/) reports the number returned
<sup>3</sup> Counted over the whole resolved scope. In a single channel context without [READ_MESSAGE_HISTORY](/http-api/permissions/), `total` is the number of messages in `messages`
<sup>4</sup> Present only when the backend produced a cursor. A single channel context, a reconciled offset page, and a Meilisearch instance each return none
<sup>4</sup> Present only when the backend produced a cursor. A single channel context, a Meilisearch instance, and a page Fluxer rebuilt after it found deleted messages in the backend results each return none
[Search messages](#search-messages) never honours a supplied cursor, so page through a result set with `page` alone.
@@ -53,7 +53,7 @@ Deleted messages are omitted. When using `page`, the total counts only existing
## Search indexing object
The other 200 body. Fluxer returns it when a channel the search needs has never been indexed, or when that index predates the instance forced reindex point.
The other 200 body. Fluxer returns it when a channel the search needs has never been indexed, or when that channel was last indexed before 2026-05-23 17:30:00 UTC, a fixed date built into Fluxer.
### Structure
@@ -174,7 +174,7 @@ Every scope that can include a guild channel calls the main Gateway. A Gateway c
### JSON body
Every field is optional, though the default `current` scope requires a context. An omitted filter is not applied.
Every field is optional, though the default `current` scope requires `context_guild_id` or `context_channel_id`. An omitted filter is not applied.
| Field | Type | Description |
| --- | --- | --- |
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
A Go Live stream is a screen share on a member's existing voice connection. A stream key names one stream. The operations here record a region preference and manage the stream's JPEG preview image.
Every route on this page is user-only. Bot and OAuth2 credentials are rejected.
Every route on this page is user-only. Fluxer rejects a bot token and an OAuth2 bearer credential with 403 `ACCESS_DENIED`.
## Stream lifecycle
@@ -38,15 +38,15 @@ Fluxer checks the scope segment against the resolved channel. A guild-scoped key
The route then compares the channel segment against the resolved channel ID, and a mismatch returns 400 `STREAM_KEY_CHANNEL_MISMATCH`. On the routes that have a body `channel_id`, a caller MUST send the same value in the body and in the key.
That comparison is the last check of the read boundary. Channel lookup, the scope check, the channel type check, and the `CONNECT` check all run first, so a body `channel_id` naming a channel the caller cannot see answers 404 `UNKNOWN_CHANNEL` or 403.
Fluxer runs that comparison after every other [read access](#access-rules) check. Channel lookup, the scope check, the channel type check, and the `CONNECT` check all run first, so a body `channel_id` naming a channel the caller cannot see returns 404 `UNKNOWN_CHANNEL` or one of the 403 codes listed under [Access rules](#access-rules).
## Access rules
The boundaries below apply across this page.
[Get stream preview](#get-stream-preview) requires read access. Every other route on this page requires mutation access.
Read access matches [Get channel](/http-api/channels/#get-channel) for the [resolved channel](#stream-key). A guild channel also requires the [CONNECT](/http-api/permissions/) permission. The caller does not have to own the stream, so any member who can join the channel can read its preview.
Mutation access requires everything read access does. A guild channel also requires the [STREAM](/http-api/permissions/) permission. The caller must hold a voice state in exactly the resolved channel and the connection identifier the key names. Fluxer rejects a caller that holds no such voice state with 403 `ACCESS_DENIED`, even when it owns the channel.
Mutation access requires everything read access does. A guild channel also requires the [STREAM](/http-api/permissions/) permission. The caller must hold a voice state whose channel is the resolved channel and whose `connection_id` is the connection segment of the key. Fluxer rejects a caller that holds no such voice state with 403 `ACCESS_DENIED`, even when it owns the channel.
:::caution[A preview can exist with no live stream]
A caller can record a region and upload a preview for any voice connection it holds, whether or not it is streaming.
@@ -114,7 +114,7 @@ Records the preferred media region for a stream and returns 204 with an empty bo
| --- | --- | --- |
| region?<sup>1</sup> | string | The RTC region ID of 1 through 64 characters |
<sup>1</sup> An omitted field stores no region for this stream
<sup>1</sup> An omitted field removes any region already stored for this stream
A region identifier is the `id` of an [RTC region object](/http-api/channels/#rtc-region-object), as returned by [List RTC regions](/http-api/channels/#list-rtc-regions).
@@ -182,7 +182,7 @@ The response `Content-Type` is the media type recorded when the preview was writ
Replaces the stream's preview with a JPEG sent inline as base64 and returns 204 with an empty body. The caller must satisfy the [mutation access](#access-rules) boundary.
When the image should travel out of band, use [Create stream preview upload URL](#create-stream-preview-upload-url).
To upload the image bytes with a separate `PUT` request, use [Create stream preview upload URL](#create-stream-preview-upload-url).
### Path parameters
@@ -37,7 +37,7 @@ The UTF-8 encoding of a submitted document cannot exceed 8388608 bytes, and the
<sup>3</sup> Measured in characters of the submitted string
An account holds no theme quota. The route bucket is the only bound on how many themes an account creates, and no route here checks an [instance limit key](/http-api/instance/#limit-keys).
An account holds no theme quota. The `theme:share:create` rate limit bucket is the only bound on how many themes an account creates, and no route here checks an [instance limit key](/http-api/instance/#limit-keys).
## Content screening
@@ -91,7 +91,7 @@ The object has the identifier and nothing else.
### Side effects
Fluxer stores the UTF-8 encoding of the submitted document with the content type `text/css; charset=utf-8`. The identifier addresses the document by the time the caller receives it.
Fluxer stores the UTF-8 encoding of the submitted document with the content type `text/css; charset=utf-8`. Fluxer finishes storing the document before it returns the identifier.
The [Media Proxy](/media-proxy/overview/) serves the document from `/themes/{id}.css`.
@@ -12,7 +12,7 @@ The route is user-only. Fluxer rejects a bot token and an OAuth2 bearer credenti
## Site resolvers
Supported sites include the following. Preview availability depends on the site and deployment configuration. Other public URLs can produce previews from their page metadata or media content.
The table below lists every site resolver. Whether a site produces a preview depends on the site, and the Klipy and YouTube resolvers also require an API key configured on the instance. Other public URLs can produce previews from their page metadata or media content.
| Site | Matched URL |
| --- | --- |
@@ -62,7 +62,7 @@ A refused or failed fetch does not by itself fail the operation. When Fluxer pro
## Cache behaviour
This endpoint requests a fresh resolution. Messages may reuse an earlier preview or receive one in a later message update.
This endpoint reads no cached result and resolves the URL again. A message can show a cached preview for the same URL, or receive its preview later in a message update.
## Resolve URL embeds
+12 -12
View File
@@ -62,7 +62,7 @@ A system account never receives the deleted representation. An account only sche
## Public user flags
`flags` holds the publicly visible subset of the account's stored flags. `STAFF` is also absent from an account with the staff-hidden flag.
`flags` holds the publicly visible subset of the account's stored flags. `STAFF` is also removed from `flags` while the account has the stored `STAFF_HIDDEN` flag.
| Value | Name | Description |
| --- | --- | --- |
@@ -79,7 +79,7 @@ The account-wide preference applied when another account replies to one of this
| Value | Name | Description |
| --- | --- | --- |
| 0 | NO_PREFERENCE | Respect the sender's intent on each reply, with no warning when the mention is toggled |
| 0 | NO_PREFERENCE | The sender chooses whether each reply mentions this account, with no warning when the mention is toggled |
| 1 | PREFER_MENTION | Replies mention this account by default, and the sender is warned when they disable the mention |
| 2 | PREFER_NO_MENTION | Replies omit the mention by default, and the sender is warned when they enable it |
@@ -171,15 +171,15 @@ The private representation of the current account, returned by [Get current user
<sup>11</sup> `premium_until` is the later of the subscription end and any stacked gift extension. Fluxer reports it even when premium entitlements are no longer active
<sup>12</sup> While the value is set, premium entitlements remain active until it passes, and it replaces the default grace interval that otherwise follows `premium_until`
<sup>12</sup> After `premium_until` passes, premium entitlements stay active until this time. While the value is null, they stay active for 3 days after `premium_until`
<sup>13</sup> The flag is set when the account changes its username or discriminator while holding non-lifetime premium, and it marks the discriminator for reselection when that premium access ends
<sup>13</sup> The flag is set when the account changes its username or discriminator while holding non-lifetime premium, and after that premium access ends, Fluxer replaces the discriminator with a newly generated one the next time the account starts a Gateway session
<sup>14</sup> The field is declared but never populated, so it is absent from every response
<sup>15</sup> The value drives the 24-hour cooldown on changing the default voice activity sharing state
<sup>16</sup> The array is empty when the account has no stored suspicious activity flag, when it has no email address, or when its contact is exempt from required actions
<sup>16</sup> The array is empty when the account has no stored suspicious activity flag, when it has no email address, or when the account policy exempts its email address from required actions
<sup>17</sup> A bot account is always permitted, and a user account is permitted only when its recorded date of birth places it at 18 years or older
@@ -192,7 +192,7 @@ The private representation of the current account, returned by [Get current user
:::
:::caution[An outstanding required action blocks ordinary routes]
While `required_actions` is non-empty, the ordinary login requirement returns 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. The error body's `data` member has `suspicious_activity_flags`, rebuilt from the surviving list.
While `required_actions` is non-empty, every route that requires a signed-in account, except the recovery routes below, returns 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. The error body's `data` member has `suspicious_activity_flags`, with one bit set for each entry in `required_actions`.
:::
Recovery routes stay open to a restricted account. They are [Get current user](/http-api/users/current-user/#get-current-user), [Modify current user](/http-api/users/current-user/#modify-current-user), [Get current user settings](/http-api/users/settings/#get-current-user-settings), every [email change](/http-api/users/email-and-password/) and [phone verification](/http-api/users/phone-verification/) route, [Resend email verification](/http-api/authentication/#resend-email-verification), session listing and termination, and the OAuth2 routes.
@@ -246,7 +246,7 @@ Each entry is an exact string in the `required_actions` array of the [user objec
| REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE | The account needs to verify either its email address or its phone number again |
| REQUIRE_INBOUND_PHONE_VERIFICATION<sup>1</sup> | The account needs to complete phone verification by sending an SMS to the instance's inbound number |
<sup>1</sup> The entry appears only while a phone requirement is also outstanding. A stored inbound requirement with no other phone clause produces an ordinary verified phone requirement
<sup>1</sup> The entry appears only while a phone requirement is also outstanding. A stored inbound requirement with no other phone requirement adds `REQUIRE_VERIFIED_PHONE` to the array
## Profile field privacy flags
@@ -262,7 +262,7 @@ A field whose flags value is `0` is visible to nobody, including on the owner's
The complete account-wide settings record, with one field per setting. `synced_preferences` holds a base64-encoded Protobuf snapshot defined by the [user settings Protobuf reference](/http-api/users/settings-protobuf/). Fluxer stores that snapshot for clients and never interprets it.
Update semantics belong to the [user settings update object](/http-api/users/settings/#user-settings-update-object). Both settings operations return this object, and so do [Ready](/gateway/events/#ready-object) and [User Settings Update](/gateway/events/#user-settings-update). Every field below is always present.
The [user settings update object](/http-api/users/settings/#user-settings-update-object) lists the fields an update accepts and what each supplied or omitted field does to the stored value. Both settings operations return this object, and so do [Ready](/gateway/events/#ready-object) and [User Settings Update](/gateway/events/#user-settings-update). Every field below is always present.
### Structure
@@ -521,7 +521,7 @@ A guild member profile is the per-guild profile customisation of the target acco
<sup>1</sup> The value is forced to `null` when profile privacy restricts the viewer
<sup>2</sup> The value is `null` while the member's premium-dependent guild customisation is sanitised. An animated banner hash has the `a_` prefix
<sup>2</sup> Fluxer sets the value to `null` when the account no longer has premium and the membership has a guild avatar, banner, biography, or accent colour. It stays `null` until the account joins the guild again. An animated banner hash has the `a_` prefix
## Mutual guild object
@@ -557,9 +557,9 @@ The profile read returned by [Get user profile](#get-user-profile) for one targe
<sup>1</sup> The pair requires the `guild_id` query parameter and requires both the caller and the target to be members of that guild
<sup>2</sup> All are omitted while the profile is restricted or the target hides the premium badge, `premium_since` is also omitted while the target hides the activation time, and `premium_lifetime_sequence` while it hides the sequence
<sup>2</sup> All three are omitted while the profile is restricted or the target hides the premium badge. `premium_since` is also omitted while the target hides the activation time. `premium_lifetime_sequence` is also omitted while the target hides the sequence
<sup>3</sup> The collection is omitted unless its query parameter is true, and always when the target is the caller
<sup>3</sup> Each array is omitted unless its query parameter, `with_mutual_friends` or `with_mutual_guilds`, is true. Both are always omitted when the target is the caller
<sup>4</sup> The array is empty when the profile is restricted. It otherwise holds only verified connections whose [profile field privacy flags](#profile-field-privacy-flags) admit the caller
@@ -640,7 +640,7 @@ A read that passes the access check can still be restricted. A restricted read n
### Side effects
Reading a profile can clear expired premium state after its grace period. That change emits no Gateway Dispatch and does not prevent the profile from being returned.
When the target's premium access and its grace period have both ended, reading the profile clears the target's stored premium subscription fields. That change emits no Gateway Dispatch and does not prevent the profile from being returned.
### Rate limit
@@ -206,7 +206,7 @@ A guild configuring [message_history_cutoff](/http-api/guilds/#guild-object) sti
### Side effects
The cleanup deletion emits no [Saved Message Delete](/gateway/events/#saved-message-delete).
When this read deletes an entry whose message no longer exists, Fluxer emits no [Saved Message Delete](/gateway/events/#saved-message-delete).
### Rate limit
@@ -31,9 +31,9 @@ Returns the current [user](/http-api/users/#user-object) object.
### Limitations
- An OAuth2 bearer credential must hold the `identify` [scope](/http-api/oauth2/#oauth2-scopes), and one without it is rejected with 403 [`MISSING_OAUTH_SCOPE`](/http-api/errors/), whose body has `required_scope`.
- A bearer receives the neutralised representation, and `email` only when it also holds the `email` scope.
- A bearer receives the [user object](/http-api/users/#user-object) with the zero values that page lists for a bearer read, and `email` only when it also holds the `email` scope.
This route alone admits an account with an outstanding [required action](/http-api/users/#required-actions), so a client can read and poll the actions it has to complete.
An account with an outstanding [required action](/http-api/users/#required-actions) can call this route, so a client can read and poll the actions it has to complete.
### Response
@@ -123,7 +123,7 @@ An omitted field leaves its stored value untouched, and an explicit `null` clear
To skip the body proof fields, send the sudo token in the `X-Fluxer-Sudo-Mode-JWT` request header.
A body that has no `email_token` and nothing beyond `mfa_method`, `mfa_code`, `webauthn_response`, and `webauthn_challenge` returns the current account unchanged. That check runs before the required action barrier, so it succeeds even while an action is outstanding. Supplying `password` alone is an update with nothing to change, and it still emits [User Update](/gateway/events/#user-update).
A body that has no `email_token` and nothing beyond `mfa_method`, `mfa_code`, `webauthn_response`, and `webauthn_challenge` returns the current account unchanged. That check runs before Fluxer checks required actions, so it succeeds even while an action is outstanding. Supplying `password` alone is an update with nothing to change, and it still emits [User Update](/gateway/events/#user-update).
Fluxer rejects an account with an outstanding required action. The one exemption is a body that has `email_token` and nothing beyond the sudo verification fields. Any other defined field removes the exemption.
@@ -141,7 +141,7 @@ An animated avatar requires the animated avatar entitlement and is otherwise rej
The username, display name, biography, and pronouns are matched against the instance profile substring blocklist, and a match returns 403 [`CONTENT_BLOCKED`](/http-api/errors/). An account with the `STAFF` flag is exempt. The biography and pronouns are also scanned against the phrase and URL blocklists.
The global content filter applied to every POST, PUT, and PATCH JSON body runs the same pair of blocklists over the whole body before the handler. It reads `username`, `global_name`, `email`, `timezone`, and `discriminator` when supplied as a string, alongside the biography and pronouns. It skips `avatar`, `banner`, `password`, `new_password`, `email_token`, `mfa_code`, `webauthn_response`, `webauthn_challenge`, and every field whose name ends in `_flags`, and it ignores any value shorter than 3 characters. A match returns the same 403 `CONTENT_BLOCKED`, and no flag exempts an account from it.
The global content filter applied to every POST, PUT, and PATCH JSON body runs the phrase and URL blocklists over the whole body before the handler. It reads `username`, `global_name`, `email`, `timezone`, and `discriminator` when supplied as a string, alongside the biography and pronouns. It skips `avatar`, `banner`, `password`, `new_password`, `email_token`, `mfa_code`, `webauthn_response`, `webauthn_challenge`, and every field whose name ends in `_flags`, and it ignores any value shorter than 3 characters. A match returns the same 403 `CONTENT_BLOCKED`, and no flag exempts an account from it.
### Secondary controls
@@ -172,7 +172,7 @@ Supplying `new_password` on a claimed account deletes every other authentication
### Side effects
A successful change emits [User Update](/gateway/events/#user-update) to the account. When a [partial user object](/http-api/users/#partial-user-object) field changes, other accounts observe the new value. Changing the username, discriminator, or display name also reindexes the account's guild member records in every guild whose member index exists. A body that takes the early return changes nothing and emits no Dispatch. Any other accepted body emits the Dispatch even when no stored value changed.
A successful change emits [User Update](/gateway/events/#user-update) to the account. When a [partial user object](/http-api/users/#partial-user-object) field changes, other accounts observe the new value. Changing the username, discriminator, or display name also updates the account in guild member search for every guild whose members are indexed. A body with no `email_token` and no field beyond the sudo verification fields changes nothing and emits no Dispatch. Any other accepted body emits the Dispatch even when no stored value changed.
### Rate limit
@@ -45,7 +45,7 @@ The account-level codes `ACCESS_DENIED`, `ACCOUNT_SUSPICIOUS_ACTIVITY`, `SUDO_MO
## Email send controls
Each send-bearing operation passes through its own route bucket and a longer email-send control keyed by the account. Exhausting either produces HTTP 429 even when the other has room.
Each operation that sends an email counts against its own route bucket and against a 15-minute email-send control for the account. Exhausting either produces HTTP 429 even when the other has room.
| Control | Allowance | Operations |
| --- | --- | --- |
@@ -56,7 +56,7 @@ Each send-bearing operation passes through its own route bucket and a longer ema
## Email change ticket states
The email change ticket is pending-original, pending-new, or completed. A step allowed in one state is rejected in every other with the transition code named below.
The email change ticket is pending-original, pending-new, or completed. A step allowed in one state is rejected in every other with the code named in the paragraphs below.
| Current state | Operation | Next state |
| --- | --- | --- |
@@ -68,7 +68,7 @@ The email change ticket is pending-original, pending-new, or completed. A step a
A ticket created without original email verification starts in the pending-new state and receives its `original_proof` directly from [Start email change](#start-email-change). Requesting or verifying a new address while the ticket is still pending original fails with `ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST`.
Verifying the original stage on a ticket that never required it fails with `ORIGINAL_VERIFICATION_NOT_REQUIRED`. Resending the original code fails with `ORIGINAL_EMAIL_ALREADY_VERIFIED` on a cleared ticket and on a ticket that never required the stage, so the response does not tell the two apart. On a cleared ticket, verifying the original address again returns the same `original_proof`.
Verifying the original stage on a ticket that never required it fails with `ORIGINAL_VERIFICATION_NOT_REQUIRED`. Resending the original code fails with `ORIGINAL_EMAIL_ALREADY_VERIFIED` on a ticket that has passed original address verification and on a ticket that never required the stage, so the response does not tell the two apart. On a ticket that has passed original address verification, verifying the original address again returns the same `original_proof`.
Resending the new address code before an address has been requested fails with `NO_NEW_EMAIL_REQUESTED`. A pending-original ticket returns the same code.
@@ -114,7 +114,7 @@ The state of a newly created email change ticket.
## New email request object
The state of a ticket with a candidate address bound to it and a code in flight to that address.
The state of a ticket with a candidate address bound to it and a code sent to that address.
### Structure
@@ -192,7 +192,7 @@ The replacement session [Complete password change](#complete-password-change) re
| token<sup>1</sup> | string | The authentication token for the replacement session |
| auth_session_id_hash | string | The base64url-encoded hash of the replacement authentication session |
<sup>1</sup> The same operation destroys the token the client authenticated with, so use this value before issuing another request
<sup>1</sup> The same operation destroys the token the client authenticated with, so the client sends every later request with this value
## Start email change
@@ -202,7 +202,7 @@ Creates an email change ticket. Returns an [email change start](#email-change-st
Only an account that holds an email address or is unclaimed can start an email change. Every other account fails with `MUST_HAVE_EMAIL_TO_CHANGE_IT`.
Original address verification applies only when the account holds a verified email address. A bounce clears the verified state. An account that has done nothing since the bounce gets a ticket that skips the original stage. [Apply email change](#apply-email-change) marks the address verified again without clearing the bounced marker. An account that has applied a change since the bounce therefore gets a ticket that does require the stage.
Original address verification applies only when the account holds a verified email address. A bounce clears the verified state. An account whose address has not been verified again since the bounce gets a ticket that skips the original stage. [Apply email change](#apply-email-change) marks the address verified again without clearing the bounced marker. An account that has applied a change since the bounce therefore gets a ticket that does require the stage.
Sending an original address code counts against the original address control.
@@ -291,10 +291,10 @@ Fluxer trims the address and then checks it in order.
- An empty address fails with `EMAIL_IS_REQUIRED`.
- An address equal to the ticket's recorded original fails with `NEW_EMAIL_MUST_BE_DIFFERENT`.
- A domain without DNS eligibility fails with `INVALID_EMAIL_ADDRESS`.
- A domain that publishes no mail exchange or address records fails with `EMAIL_DOMAIN_CANNOT_RECEIVE_MAIL`.
- An address another account already owns fails with `EMAIL_ALREADY_IN_USE`.
Ownership is checked again when the resulting email token is applied, so passing here reserves nothing. Calling the operation again with a different address replaces the pending address on the same ticket.
Ownership is checked again when the resulting email token is applied, so a successful request here does not reserve the address. Calling the operation again with a different address replaces the pending address on the same ticket.
The send counts against the new address control, and the ticket applies its own 30-second cooldown.
@@ -429,7 +429,7 @@ The body extends the [sudo verification object](/http-api/users/mfa/#sudo-verifi
The body has no profile field and no `new_password` field, and Fluxer strips unknown keys before it handles the request.
:::caution[An unclaimed account never sends `password` here]
Sudo mode does not apply to it, so Fluxer reads the field as an ordinary account update. Such an account can update only `new_password`, `has_dismissed_premium_onboarding`, and `has_unread_gift_inventory`. The request fails with `UNCLAIMED_ACCOUNTS_CAN_ONLY_SET_EMAIL_VIA_TOKEN` on the path `password`.
Sudo mode does not apply to an unclaimed account, so Fluxer reads `password` as an ordinary account update field. Such an account can update only `new_password`, `has_dismissed_premium_onboarding`, and `has_unread_gift_inventory`. The request fails with `UNCLAIMED_ACCOUNTS_CAN_ONLY_SET_EMAIL_VIA_TOKEN` on the path `password`.
:::
An unclaimed account that also wants a first password makes a single [Modify current user](/http-api/users/current-user/#modify-current-user) call with `email_token` and `new_password`.
@@ -465,7 +465,7 @@ Every one of them requires the bounced marker. An account without it is rejected
The flow also requires the account to hold no verified address, which is the state a bounce leaves. [Apply email change](#apply-email-change) marks an address verified without clearing the bounced marker. An account that took the ordinary flow after its bounce has both states, so it gets `ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST` here and changes its address through the ordinary flow.
[Ticket and code contract](#ticket-and-code-contract) applies to its codes, cooldown, and new address send control.
The codes and the 30-second cooldown in this flow follow [Ticket and code contract](#ticket-and-code-contract). The sends in this flow count against the new address control.
## Request replacement email for bounced address
@@ -473,7 +473,7 @@ The flow also requires the account to hold no verified address, which is the sta
Starts the recovery flow. Returns a [new email request](#new-email-request-object) object on success.
This operation creates a ticket and binds the replacement address in the same call. The route accepts suspicious account state. Fluxer checks the address exactly as [Request new email](#request-new-email) does: it must pass DNS eligibility, belong to no other account, and differ from the bounced address.
This operation creates a ticket and binds the replacement address in the same call. The route accepts suspicious account state. Fluxer checks the address exactly as [Request new email](#request-new-email) does: the address must have a domain that publishes mail exchange or address records, belong to no other account, and differ from the bounced address.
The send counts against the new address control.
@@ -656,7 +656,7 @@ The ticket moves to its verified state and stores a verification proof. The acco
Consumes a verified ticket and replaces the account password. Returns a [password change completion](#password-change-completion-object) object on success. Emits an [Auth Session Change](/gateway/events/#auth-session-change) Gateway event with the replacement token.
No sudo verification applies. A ticket outside its verified state or a proof that does not match fails with `INVALID_OR_EXPIRED_TICKET` or `INVALID_PROOF_TOKEN`. A replacement password in the public breached-password corpus is rejected with `PASSWORD_IS_TOO_COMMON`. The ticket is marked completed once the password is written, so a retry fails with `TICKET_ALREADY_COMPLETED`.
No sudo verification applies. A ticket outside its verified state fails with `INVALID_OR_EXPIRED_TICKET`. A proof that does not match fails with `INVALID_PROOF_TOKEN`. A replacement password in the public breached-password corpus is rejected with `PASSWORD_IS_TOO_COMMON`. The ticket is marked completed once the password is written, so a retry fails with `TICKET_ALREADY_COMPLETED`.
### JSON body
@@ -11,7 +11,7 @@ Multi-factor authentication asks for a second proof of identity after the accoun
Every route here requires a non-bot user session. Fluxer rejects an account in suspicious activity state with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`, including on the sudo routes.
:::caution[A challenge is one-use and context-bound]
Fluxer consumes a backup code, a WebAuthn registration challenge, and a WebAuthn sudo challenge on first use. A challenge is bound to the issuing user and operation context and expires after five minutes, so a registration challenge cannot be redeemed as a sudo assertion.
Fluxer consumes a backup code, a WebAuthn registration challenge, and a WebAuthn sudo challenge on first use. A challenge is bound to the user it was issued to and to the operation it was issued for: registration, sudo, MFA login, or discoverable login. It expires after five minutes, so a registration challenge cannot be redeemed as a sudo assertion.
:::
## TOTP parameters
@@ -93,7 +93,7 @@ The accepted proof depends on the authenticators the account has configured.
The `totp` method reads `mfa_code` as a current authenticator code or an unconsumed backup code and requires the account to hold a TOTP secret. The `webauthn` method reads `webauthn_response` and `webauthn_challenge` together and requires a registered credential.
A request that has no accepted proof fails with 403 `SUDO_MODE_REQUIRED`. Its body has the [sudo mode methods object](#sudo-mode-methods-object) members, and a client can present the correct challenge. A proof that is present but wrong fails instead with 400 `INVALID_FORM_BODY` and a [validation error](/http-api/#validation-error-object) entry. A mismatched password produces path `password` with code `INVALID_PASSWORD`. A rejected TOTP code, backup code, or WebAuthn assertion produces path `mfa_code` with code `INVALID_MFA_CODE`, so a client cannot tell which of the three was rejected.
A request that has no accepted proof fails with 403 `SUDO_MODE_REQUIRED`. Its body has the [sudo mode methods object](#sudo-mode-methods-object) members, so a client can tell which proof to ask the user for. A proof that is present but wrong fails instead with 400 `INVALID_FORM_BODY` and a [validation error](/http-api/#validation-error-object) entry. A mismatched password produces path `password` with code `INVALID_PASSWORD`. A rejected TOTP code, backup code, or WebAuthn assertion produces path `mfa_code` with code `INVALID_MFA_CODE`, so a client cannot tell which of the three was rejected.
The `totp` method also consumes a per-account allowance of 10 multi-factor attempts in 15 minutes, shared by every sudo-gated operation on every resource. Fluxer charges the allowance before it checks the code, so a wrong code and a correct code both draw on it. A correct code resets the counter to zero. While it is exhausted, a correct code returns the same `INVALID_MFA_CODE` entry as a wrong one. The `webauthn` method draws on no allowance. The login MFA allowances on [HTTP authentication](/http-api/authentication/) are counted separately.
@@ -202,7 +202,7 @@ The backup codes and the proof a verified challenge returns.
An account holds at most 10 WebAuthn credentials. Both [Create WebAuthn registration options](#create-webauthn-registration-options) and [Register WebAuthn credential](#register-webauthn-credential) enforce this limit.
Fluxer verifies every assertion against the relying party identifier and the allowed origins configured for the instance. The credential's signature counter strictly increases, unless both the stored and reported counters are zero, which is how an authenticator without a counter appears.
Fluxer verifies every assertion against the relying party identifier and the allowed origins configured for the instance. Fluxer rejects an assertion whose reported signature counter is not greater than the stored counter. The exception is a stored and a reported counter of zero, which is how an authenticator without a counter appears.
### Structure
@@ -243,7 +243,7 @@ Fluxer never returns the credential's public key, signature counter, or reported
## WebAuthn assertion object
The browser WebAuthn `PublicKeyCredential` serialisation. Its field names are camelCase, because the boundary accepts the exact structure the WebAuthn client API produces.
The browser WebAuthn `PublicKeyCredential` serialisation. Its field names are camelCase, because Fluxer accepts the exact structure the WebAuthn client API produces.
### Structure
@@ -337,7 +337,7 @@ Every operation that issues a WebAuthn authentication challenge returns this obj
| allowCredentials?<sup>3</sup> | array[[WebAuthn credential descriptor](#webauthn-credential-descriptor-object) object] | Credentials accepted for this operation |
| userVerification<sup>4</sup> | string | User verification requirement, one of `discouraged`, `preferred`, or `required` |
<sup>1</sup> Expires five minutes after issue and is bound to the exact context that issued it, so a sudo challenge cannot be redeemed as a login assertion
<sup>1</sup> Expires five minutes after issue and is bound to the operation that issued it, so a sudo challenge cannot be redeemed as a login assertion
<sup>2</sup> Every operation emits the fixed value `60000`, a standard PublicKeyCredential request option
@@ -478,7 +478,7 @@ The body extends the [sudo verification object](#sudo-verification-object) with
### Side effects
TOTP is enabled and 10 backup codes are issued. Any backup code the account already held stays in place, so the response has only the 10 new codes. The resulting authenticator types are also applied to every bot account owned by the user. [User Update](/gateway/events/#user-update) reaches the user's sessions and each owned bot whose authenticator types changed.
TOTP is enabled and 10 backup codes are issued. Any backup code the account already held stays in place, so the response has only the 10 new codes. Fluxer also copies the account's authenticator types to every bot account the user owns. [User Update](/gateway/events/#user-update) reaches the user's sessions and each owned bot whose authenticator types changed.
### Rate limit
@@ -516,7 +516,7 @@ Disabling TOTP invalidates every backup code on the account, including the code
### Side effects
TOTP is disabled and every backup code stops working. Fluxer removes the TOTP authenticator type, along with the unassigned legacy authenticator value `1` when the account still held it. The resulting authenticator types are also applied to every bot account owned by the user. [User Update](/gateway/events/#user-update) goes to the user's sessions and each owned bot whose authenticator types changed.
TOTP is disabled and every backup code stops working. Fluxer removes the TOTP authenticator type, along with the unassigned legacy authenticator value `1` when the account still held it. Fluxer also copies the account's authenticator types to every bot account the user owns. [User Update](/gateway/events/#user-update) goes to the user's sessions and each owned bot whose authenticator types changed.
### Rate limit
@@ -775,7 +775,7 @@ Fluxer consumes the registration challenge before it re-checks the credential li
### Side effects
The credential is added to the account. When it is the account's first WebAuthn credential, the WebAuthn authenticator type is also applied to every bot account owned by the user.
The credential is added to the account. When it is the account's first WebAuthn credential, Fluxer also copies the account's authenticator types, which now include WebAuthn, to every bot account the user owns.
[WebAuthn Credentials Update](/gateway/events/#webauthn-credentials-update) reaches the user's sessions with the complete current credential summaries. When the authenticator types changed, [User Update](/gateway/events/#user-update) also reaches the user and each affected owned bot.
@@ -24,7 +24,7 @@ A single note the caller holds for one target account.
| --- | --- | --- |
| note<sup>1</sup> | string | The note text the caller stored for the target account |
<sup>1</sup> Never null and never the empty string. A stored note is always 1 to 256 characters after normalisation, and every clearing form deletes the record
<sup>1</sup> Never null and never the empty string. A stored note is always 1 to 256 characters after normalisation, and an omitted, null, or empty `note` deletes the stored note
## User notes record
@@ -100,7 +100,7 @@ A target with no stored note returns 404 `UNKNOWN_USER`. The same status covers
Creates, replaces, or deletes the caller's note for one target account. Returns 204 with an empty body. Emits a [User Note Update](/gateway/events/#user-note-update) Gateway event.
The supplied text becomes the complete stored note. An unresolved target ID returns 404 `UNKNOWN_USER`, even when the request would only delete a note. The caller can store a note for any account that exists, including a bot account, its own account, and an account it holds no relationship with.
The supplied text becomes the complete stored note. A target ID that names no account returns 404 `UNKNOWN_USER`, even when the request would only delete a note. The caller can store a note for any account that exists, including a bot account, its own account, and an account it holds no relationship with.
:::caution[An omitted, null, or empty note deletes it]
An empty string is [read as `null`](/http-api/#input-normalisation), so the forms are indistinguishable. Any value that normalises to the empty string, such as one made only of whitespace, fails validation with `STRING_LENGTH_INVALID`. There is no separate delete route.
@@ -69,7 +69,7 @@ Keep phone numbers, verification codes, and challenge codes out of logs, analyti
## Provider lookup verdicts
Outbound SMS requires the number to pass the following checks. The first matching verdict applies. A number routed directly to an inbound challenge does not need this lookup.
Outbound SMS requires the number to pass the following checks. Fluxer evaluates the rows in order and uses the verdict of the first row whose condition matches. A number routed directly to an inbound challenge does not need this lookup.
| Order | Condition | Verdict |
| --- | --- | --- |
@@ -112,7 +112,7 @@ A reason explains why the attempt could not complete over outbound SMS.
## SMS delivery object
Confirmation that the SMS provider accepted a one-time code for delivery. [Send phone verification](#send-phone-verification) returns it on an outbound result, and [Verify phone code](#verify-phone-code) then takes the number itself.
Confirmation that the SMS provider accepted a one-time code for delivery. [Send phone verification](#send-phone-verification) returns it on an outbound result. The client then sends the same number and the delivered code to [Verify phone code](#verify-phone-code).
### Structure
@@ -213,7 +213,7 @@ Several independent policies can force the inbound channel, and each one overrid
An attempt that survives that step passes through phone attempt risk controls keyed by account, by client IP address, and by /24 network. A hard block returns 429 `PHONE_RATE_LIMIT_EXCEEDED` with a `Retry-After` of 86400. A captcha decision returns 400 `CAPTCHA_REQUIRED`, and this route reads no [CAPTCHA](/topics/captcha/) solution header, so the attempt cannot be retried with a solution. An inbound decision returns a challenge with the `behavioural_risk` reason.
An earlier attempt against the same account or number can record a provider cooldown, and a later request under that cooldown returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. The destination is then resolved through [provider lookup](#provider-lookup-verdicts). A number that has already completed verification twice is refused with 400 `PHONE_ALREADY_USED`.
When the SMS provider throttles an earlier send or code check for the same account or number, Fluxer records a cooldown. A request during that cooldown returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. The destination is then resolved through [provider lookup](#provider-lookup-verdicts). A number that has already completed verification twice is refused with 400 `PHONE_ALREADY_USED`.
Further controls bound outbound delivery to 3 sends per 6 hours for each account and 3 sends per 5 days for each number. Either denial returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. The provider can also refuse the send. An invalid destination returns 400 `INVALID_PHONE_NUMBER`, and a throttle returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. Every other refusal, an unreachable provider included, returns 400 `SMS_VERIFICATION_UNAVAILABLE`. On an instance where the inbound flow or its receiving number is unconfigured, an attempt routed inbound returns the same code.
@@ -290,7 +290,7 @@ The configured SMS provider checks the number and the code together, so the prov
<sup>1</sup> Stripped of control and formatting characters and trimmed before the length check, and the normalised value is what the provider checks
:::caution[A third verification consumes the code before refusing]
A number can complete verification twice, and each success refreshes that record for a further 31 days. A third attempt is refused with 400 `PHONE_ALREADY_USED`, but only after the provider has accepted the code.
A number can complete verification twice, and each success restarts a 31-day window in which Fluxer counts successes for that number. A third attempt is refused with 400 `PHONE_ALREADY_USED`, but only after the provider has accepted the code.
:::
### Response
@@ -11,7 +11,7 @@ A private channel is a conversation outside any guild. It is either a direct mes
The routes here take a user session or a bot token. An OAuth2 bearer credential receives 403 `ACCESS_DENIED`, and an account with an outstanding required action receives 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`.
:::note[Open state is per participant]
A direct message channel exists once, and each participant holds its own open state. Closing one side leaves the channel and its messages in place, so reopening the pair resolves the stored channel.
A direct message channel exists once, and each participant holds its own open state. Closing one side leaves the channel and its messages in place, so a later [Create private channel](#create-private-channel) request for the same pair returns that stored channel.
:::
## Preloaded messages object
@@ -82,7 +82,7 @@ One body field selects the channel type. `recipient_id` opens a direct message.
A request supplying `recipients` also consumes the `user:group_dm:create` bucket and is subject to the CAPTCHA check below, whatever the resulting participant count.
Instance policy can disable private channel creation entirely, in which case every request returns 400 `DIRECT_MESSAGES_DISABLED` after the body has been validated and the group DM bucket and CAPTCHA have been applied.
When the instance [community policy](/http-api/instance/#community-policy-object) sets `direct_messages_disabled` to true, every request returns 400 `DIRECT_MESSAGES_DISABLED` after the body has been validated and the group DM bucket and CAPTCHA have been applied.
An unclaimed account holds no password and did not arrive through SSO. Fluxer rejects an unclaimed caller with 400 `UNCLAIMED_ACCOUNT_CANNOT_JOIN_GROUP_DMS` when `recipients` is supplied, and with 400 `UNCLAIMED_ACCOUNT_CANNOT_SEND_DIRECT_MESSAGES` otherwise. An ordinary caller then needs a verified email address, failing which the request returns 403 `DIRECT_MESSAGE_EMAIL_VERIFICATION_REQUIRED`. A bot caller is exempt from the email requirement and is never unclaimed.
@@ -110,7 +110,7 @@ Every participant, the caller included, holds fewer open group DMs than the inst
Fluxer then evaluates each other recipient independently and collects every failure, using the [unaddable reasons](#group-dm-unaddable-reasons) above. One failing recipient rejects the whole request with 400 `GROUP_DM_RECIPIENTS_NOT_ADDABLE`. The response has the rejected entries in the top-level `unaddable_recipients` member and the accepted user IDs in `addable_recipients`, so the caller can retry with a smaller set.
An ordinary caller never adds a recipient across a block in either direction, because blocking deletes the friendship both ways and a friend request crosses no block.
An ordinary caller cannot add a recipient it has blocked or a recipient that has blocked it. Blocking deletes the friendship on both accounts, and a friend request between the two accounts creates no new friendship while either block exists.
:::note[Group DM admission reads no block state]
Fluxer holds a bot caller to no friendship. A recipient that has blocked the bot is added when the two share a guild and the recipient's [group DM add permission flags](/http-api/users/#group-dm-add-permission-flags) admit it.
@@ -220,7 +220,7 @@ A guild channel the caller cannot see fails resolution first and returns 403 `MI
Fluxer appends a newly pinned channel after every channel already pinned. A channel later closed, left, or deleted stays in the pinned set, and only [Unpin private channel](#unpin-private-channel) or deletion of the caller's account removes an entry.
:::caution[Unpinning needs the channel to resolve]
Unpinning resolves the channel first, so an entry whose channel the caller can no longer resolve stays in the pinned set.
Unpinning resolves the channel first, so an entry for a deleted channel, or for a group DM the caller is no longer a recipient of, stays in the pinned set.
:::
:::note[Pinning is idempotent and the Dispatch is unconditional]
@@ -11,7 +11,7 @@ A relationship describes a friendship, block, or pending friend request from the
The routes here are user-only. A bot or OAuth2 bearer credential receives 403 `ACCESS_DENIED`, and an account with an outstanding required action receives 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`.
:::note[Relationship records are directional]
A friendship is a FRIEND record on both accounts. A pending request is `OUTGOING_REQUEST` on the sender and `INCOMING_REQUEST` on the recipient. A block exists only on the account that created it, so the blocked account observes nothing.
A friendship is a FRIEND record on both accounts. A pending request is `OUTGOING_REQUEST` on the sender and `INCOMING_REQUEST` on the recipient. A block exists only on the account that created it, so the block adds no record to the blocked account and sends it no [Relationship Add](/gateway/events/#relationship-add).
:::
A deleted account has the deleted flag with no pending deletion timestamp, and is not a valid friend request target. An account inside a scheduled deletion window has a timestamp, so it stays a valid target.
@@ -171,13 +171,13 @@ A target the caller has blocked returns 400 `CANNOT_SEND_FRIEND_REQUEST_TO_BLOCK
<sup>1</sup> The friend source evaluation is skipped when the target has no stored settings, which admits the request as though the target permitted requests from anyone
:::caution[A flagged caller receives an indistinguishable success]
A caller with the `SPAMMER` [public user flag](/http-api/users/#public-user-flags), or one whose send trips the deployment's direct contact spam policy, gets a one-sided request. Fluxer writes only the caller's `OUTGOING_REQUEST` record. [Relationship Add](/gateway/events/#relationship-add) reaches the caller alone. The response is an ordinary `OUTGOING_REQUEST` object.
A caller with the `SPAMMER` [public user flag](/http-api/users/#public-user-flags), or one whose send triggers the deployment's direct contact spam policy, gets a one-sided request. Fluxer writes only the caller's `OUTGOING_REQUEST` record. [Relationship Add](/gateway/events/#relationship-add) reaches the caller alone. The response is an ordinary `OUTGOING_REQUEST` object.
:::
That path also skips the rules that follow it: the target's block of the caller, the target's [friend source flags](/http-api/users/#friend-source-flags), the app store reviewer rule, the [relationship limit](#relationship-limit), and friendly bot auto-acceptance. The caller's own block of the target is still enforced and still returns 400 `CANNOT_SEND_FRIEND_REQUEST_TO_BLOCKED_USER`. The policy path can also set the `SPAMMER` flag on the caller, which makes every later send one-sided.
The one-sided request path also skips the rules that follow it: the target's block of the caller, the target's [friend source flags](/http-api/users/#friend-source-flags), the app store reviewer rule, the [relationship limit](#relationship-limit), and friendly bot auto-acceptance. The caller's own block of the target is still enforced and still returns 400 `CANNOT_SEND_FRIEND_REQUEST_TO_BLOCKED_USER`. When the direct contact spam policy suppresses a send and its action is `flag_spammer`, Fluxer also sets the `SPAMMER` flag on the caller, which makes every later send one-sided.
:::note[The suppression entries enter at different points]
A caller that already has the flag enters before the pending request lookup, so it can hold both an `INCOMING_REQUEST` and an `OUTGOING_REQUEST` record for one account. A caller suppressed during the call enters after the lookup and accepts the pending request normally.
Fluxer checks for the `SPAMMER` flag before it looks for a pending request from the target. A caller that already has the flag gets a one-sided request even when the target has a pending request, so it can hold both an `INCOMING_REQUEST` and an `OUTGOING_REQUEST` record for one account. Fluxer applies the direct contact spam policy after that lookup, so a caller without the flag accepts a pending request from the target normally.
:::
### Path parameters
@@ -209,7 +209,7 @@ The body can be omitted, in which case it is treated as an empty object.
A new request writes matching `OUTGOING_REQUEST` and `INCOMING_REQUEST` records and emits [Relationship Add](/gateway/events/#relationship-add) to both accounts. Both records are written with `share_voice_activity` set to true.
When the target has already sent the caller a request, the operation accepts that request. Acceptance replaces both pending records with FRIEND records and emits [Relationship Update](/gateway/events/#relationship-update) to both accounts, each side taking its own sharing default. No acceptance path on this route opens a direct message channel, unlike [Accept request or block user](#accept-request-or-block-user).
When the target has already sent the caller a request, the operation accepts that request. Acceptance replaces both pending records with FRIEND records and emits [Relationship Update](/gateway/events/#relationship-update) to both accounts, and each FRIEND record has `share_voice_activity` set to its owning account's `default_share_voice_activity`. No acceptance path on this route opens a direct message channel, unlike [Accept request or block user](#accept-request-or-block-user).
A bot target that has `FRIENDLY_BOT` without `FRIENDLY_BOT_MANUAL_APPROVAL` accepts in the same call, so the caller observes [Relationship Add](/gateway/events/#relationship-add) followed by [Relationship Update](/gateway/events/#relationship-update). Both flags are read only on a bot account. The response body of that call is the bot's own record, and its `id` and `user` name the caller and its `share_voice_activity` is the bot's default. The Dispatch the caller receives has the caller's record.
@@ -378,7 +378,7 @@ The body can be omitted, in which case it is treated as an empty object and the
Each matched request removes the caller's `INCOMING_REQUEST` record and the sender's `OUTGOING_REQUEST` record, and emits [Relationship Remove](/gateway/events/#relationship-remove) to the caller and to the sender. A call that matches nothing changes nothing.
A failed request can leave some requests removed without returning a count. Read the relationships again to reconcile.
A call that fails partway can remove some incoming friend requests and return no count. Call [List relationships](#list-relationships) again to read what remains.
### Rate limit
@@ -32,8 +32,8 @@ Fluxer reports every entry in the resulting `errors` array against the path `syn
| --- | --- | --- |
| Encoded string longer than 699052 characters | 400 `INVALID_FORM_BODY` | `CONTENT_EXCEEDS_MAX_LENGTH` and `INVALID_FORMAT` |
| Encoded string outside the base64 alphabet | 400 `INVALID_FORM_BODY` | `INVALID_FORMAT` |
| Decoded message above 524288 bytes | 400 `INVALID_FORM_BODY` | `TOO_LARGE` |
| Bytes that do not decode as `SyncedPreferences` | 400 `INVALID_FORM_BODY` | `INVALID_FORMAT` |
| Decoded message above 524288 bytes | 400 `VALIDATION_ERROR` | `TOO_LARGE` |
| Bytes that do not decode as `SyncedPreferences` | 400 `VALIDATION_ERROR` | `INVALID_FORMAT` |
An over-length string draws two entries for the one path.
@@ -204,7 +204,7 @@ Field numbers 42 and 43 are reserved, together with the names `attachment_media_
| --- | --- | --- |
| 0 | HDR_DISPLAY_MODE_UNSPECIFIED | No explicit mode is selected |
| 1 | HDR_DISPLAY_MODE_FULL | Display HDR media without limiting its range |
| 2 | HDR_DISPLAY_MODE_STANDARD | Display HDR media using the standard presentation |
| 2 | HDR_DISPLAY_MODE_STANDARD | Limit HDR media to standard dynamic range |
## Accessibility overrides object
@@ -419,7 +419,7 @@ One entry names one grouping in the favourites view. The client chooses the iden
## Recent mentions settings object
The `recent_mentions` field controls which mentions appear in the recent mentions view. The filters combine, and `include_guilds` selects by channel.
The `recent_mentions` field controls which mentions appear in the recent mentions view. Each filter set to false removes its mentions, and a mention appears only when no filter removes it. `include_guilds` tests the channel the mention is in.
### Structure
@@ -609,7 +609,7 @@ The `sound` field controls sound playback, master volume, and per-sound override
### Sound identifiers
Both maps are keyed by an arbitrary string, and Fluxer stores and returns any key unchanged. The first-party client uses the identifiers below, and a key outside this set has no defined playback effect.
Both maps are keyed by an arbitrary string, and Fluxer stores and returns any key unchanged. The first-party client uses the identifiers below, and the first-party client ignores a key outside this set.
| Value | Description |
| --- | --- |
@@ -22,7 +22,7 @@ Every route here is user-only and rejects a bot or OAuth2 bearer credential with
<sup>1</sup> The account stays connected and keeps receiving Gateway traffic while this value is stored
:::note[The settings boundary accepts only a chosen status]
Gateway presence also uses offline, which describes an account with no active session. The settings boundary rejects offline in both `status` and `status_resets_to`, and invisible is the stored form of appearing offline while connected.
Gateway presence also uses offline, which describes an account with no active session. [Modify current user settings](#modify-current-user-settings) rejects offline in both `status` and `status_resets_to`, and invisible is the stored form of appearing offline while connected.
:::
## Theme values
@@ -193,8 +193,8 @@ Further members are accepted and ignored. `guild_positions` is an array of at mo
| Field | Type | Description |
| --- | --- | --- |
| status? | string | [Presence status](#presence-status-values) |
| status_resets_at?<sup>1</sup> | ?ISO8601 timestamp \| integer | The moment at which the scheduled reset applies, or null to clear it |
| status_resets_to? | ?string | [Presence status](#presence-status-values) applied by the scheduled reset, or null to clear it |
| status_resets_at?<sup>1</sup> | ?ISO8601 timestamp \| integer | The time at which the first-party client sets `status` to `status_resets_to`, or null to clear it |
| status_resets_to? | ?string | [Presence status](#presence-status-values) that the first-party client sets at `status_resets_at`, or null to clear it |
| theme? | string | [Theme value](#theme-values) |
| locale?<sup>2</sup> | string | Interface [locale](/topics/locales/#supported-locales) |
| restricted_guilds?<sup>3</sup> | array[snowflake] | The guilds where member direct messages are restricted (max 200) |
@@ -238,9 +238,9 @@ Further members are accepted and ignored. `guild_positions` is an array of at mo
<sup>4</sup> `flags` is a legacy alias for `friend_source_flags`, which wins when both are supplied. `friend_source_flags` is bounded to 0 through 2147483647 while `flags` accepts any integer
<sup>5</sup> The stored value is collapsed before it is written. `FRIENDS_ONLY` wins over `NOBODY`, and either collapse preserves `SILENT_EVERYONE`
<sup>5</sup> Fluxer reduces the value before it stores it. A value with `FRIENDS_ONLY` is stored as `FRIENDS_ONLY`, and a value with `NOBODY` and no `FRIENDS_ONLY` is stored as `NOBODY`. Both keep `SILENT_EVERYONE` when it is supplied, and any other value is stored unchanged
<sup>6</sup> `FRIENDS_ONLY`, then `NOBODY`, then `EVERYONE` are tested in that order against the already collapsed value, so `EVERYONE` applies only when neither of the other two is supplied
<sup>6</sup> A value with `FRIENDS_ONLY`, `NOBODY`, or `EVERYONE` is stored as one of those flags alone. `FRIENDS_ONLY` wins over the other two, and `NOBODY` wins over `EVERYONE`. Any other value is stored unchanged
<sup>7</sup> The array replaces the layout in full
@@ -254,19 +254,19 @@ Further members are accepted and ignored. `guild_positions` is an array of at mo
<sup>12</sup> The field is staff-only
<sup>13</sup> The value runs to at most 699052 characters of base64 and decodes to at most 524288 bytes, the 512 KiB ceiling. A longer string fails at the boundary, an oversized decode fails with `TOO_LARGE`, and an undecodable value fails with `INVALID_FORMAT`
<sup>13</sup> The value runs to at most 699052 characters of base64 and decodes to at most 524288 bytes, the 512 KiB ceiling. A longer string returns 400 `INVALID_FORM_BODY`, an oversized decode fails with `TOO_LARGE`, and an undecodable value fails with `INVALID_FORMAT`
Fluxer decodes an accepted `synced_preferences` value, re-encodes it canonically, and stores the result, so a snapshot with only zero values is stored as cleared and read back as the empty string.
:::caution[A rebuilt folder layout drops unresolved references]
The submitted array replaces the layout in full and is retained as sent, so a client that rebuilds it from only the items it can render loses the rest. Send unresolved references back and omit them only from the rendered view.
The submitted array replaces the stored layout in full, and Fluxer keeps guild IDs it cannot resolve. A client that rebuilds the array from only the guilds it can render deletes every other guild ID from the stored layout. Send those guild IDs back, and hide them only in the rendered view.
:::
:::note[Fluxer edits the layout on its own]
Joining a guild prepends it to the uncategorised folder unless some folder already lists it. Leaving or losing a guild removes its ID from every folder and drops any non-uncategorised folder left empty. Each edit emits [User Settings Update](/gateway/events/#user-settings-update).
:::
Fluxer prepends an empty uncategorised folder to a submitted layout that omits one. A guild ID listed in more than one folder is kept only in the highest-ranked folder. A name is the primary key, so a named folder outranks an unnamed one whatever its identity, and an ordinary folder outranks the uncategorised folder only when both are named or both unnamed. The earliest folder in the submitted order wins a tie.
Fluxer prepends an empty uncategorised folder to a submitted layout that omits one. A guild ID listed in more than one folder is kept only in the highest-ranked folder. Fluxer ranks the folders in two steps. A folder with a non-blank name outranks a folder without one. Between two folders that are both named or both unnamed, an ordinary folder outranks the uncategorised folder. The earliest folder in the submitted order wins a tie.
## Guild folder input object
@@ -308,7 +308,7 @@ A custom status as submitted with a settings update.
<sup>3</sup> `emoji_name` is ignored without validation when `emoji_id` has a value. An `emoji_id` of null leaves `emoji_name` in place, and it is then exactly one Unicode emoji
A custom emoji ID that does not resolve fails with `CUSTOM_EMOJI_NOT_FOUND` against `custom_status.emoji_id`. Fluxer drops the resolved emoji for an account without the global expression entitlement, so the request succeeds with text alone.
A custom emoji ID that does not resolve fails with `CUSTOM_EMOJI_NOT_FOUND` against `custom_status.emoji_id`. Fluxer drops the resolved emoji for an account whose `feature_global_expressions` [limit](/http-api/instance/#limit-keys) resolves to 0, so the request succeeds with text alone.
:::caution[Hide expired custom statuses]
An expired status can remain in settings without an expiry Dispatch. Compare `expires_at` with the current time before rendering. A new Gateway session omits the expired status.
@@ -362,7 +362,7 @@ Modifies the account-wide settings and returns the complete [user settings](/htt
An account with no stored settings record returns 404 `UNKNOWN_USER`. A locale change also emits a [User Update](/gateway/events/#user-update) Gateway event.
An unresolvable custom status emoji returns 400 `INVALID_FORM_BODY` with `CUSTOM_EMOJI_NOT_FOUND` against `custom_status.emoji_id`. The trusted domain, age restriction, and synced preferences decisions return 400 `VALIDATION_ERROR` with their own codes in `errors`.
An unresolvable custom status emoji returns 400 `INVALID_FORM_BODY` with `CUSTOM_EMOJI_NOT_FOUND` against `custom_status.emoji_id`. Rejections of `trusted_domains`, of an age-restricted filter, and of an oversized or undecodable `synced_preferences` value return 400 `VALIDATION_ERROR` with their own codes in `errors`.
### JSON body
@@ -383,7 +383,7 @@ Fluxer emits [User Settings Update](/gateway/events/#user-settings-update) to th
Every [User Settings Update](/gateway/events/#user-settings-update) republishes the caller's current [Presence Update](/gateway/events/#presence-update) to eligible sessions, so a custom status change becomes visible without a further request.
A status of invisible forces every one of the caller's sessions to invisible. A later status of online, idle, or dnd releases them only when at least one session is currently invisible.
A status of invisible forces every one of the caller's sessions to invisible. When at least one session is invisible, a later status of online, idle, or dnd sets every one of the caller's sessions to that status.
### Rate limit
@@ -421,7 +421,7 @@ Outside the window the cooldown restarts on every accepted request, including on
### Side effects
An accepted request writes `default_share_voice_activity` and then walks every friendship. A friendship whose caller-side value already equals the submitted value is skipped, and its stored `version` does not advance. Every friendship, skipped or rewritten, produces one [Relationship Update](/gateway/events/#relationship-update) to the caller and one to the friend, so re-sending the current value still sends the full set out. The friend's Dispatch is emitted only when the reciprocal friendship exists.
An accepted request writes `default_share_voice_activity` and then walks every friendship. A friendship whose caller-side value already equals the submitted value is skipped, and its stored `version` does not advance. Every friendship, skipped or rewritten, produces one [Relationship Update](/gateway/events/#relationship-update) to the caller and one to the friend, so re-sending the current value still sends those Dispatches for every friendship. The friend's Dispatch is emitted only when the reciprocal friendship exists.
It then records the change time in `last_voice_activity_sharing_change_at` and emits [User Update](/gateway/events/#user-update) to the caller.
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
A webhook posts messages into one guild channel under a name and avatar of its own.
Management routes take a user session token or a bot token. Every other webhook operation uses the webhook ID and secret token in its own path as the complete credential and ignores the `Authorization` header.
Management routes take a user session token or a bot token. Every other webhook operation uses the webhook ID and secret token in its own path as the complete credential. A sent `Authorization` header grants no access there, and Fluxer still resolves it, as [Rate limit keying](#rate-limit-keying) describes..
The [Messages resource](/http-api/messages/) defines the message, rich embed input, and message reference input objects.
@@ -26,7 +26,7 @@ Fluxer scans a submitted webhook `name` against the instance phrase and URL bloc
Every route bucket on this page is keyed on the caller identity as well as on the path parameter in its bucket name. The caller identity is the authenticated account when a request resolves one and the client IP address otherwise, so a token-authenticated operation is bounded per address.
Fluxer resolves an `Authorization` credential on a token-authenticated operation for this keying alone. [Rate limits](/topics/rate-limits/) defines the keying in full.
When a request to a token-authenticated operation sends an `Authorization` header, Fluxer reads that credential only to choose the rate limit key. [Rate limits](/topics/rate-limits/) defines the keying in full.
The [global HTTP limit](/topics/rate-limits/) applies to every management route. [Get webhook with token](#get-webhook-with-token), [Update webhook with token](#update-webhook-with-token), and [Delete webhook with token](#delete-webhook-with-token) consume it under the caller identity. Every other route on this page is exempt and is bounded only by its own route bucket.
@@ -36,7 +36,7 @@ Fluxer refuses a call from the official web client on [Execute webhook](#execute
A request that sends no `Origin`, or any other `Origin` value, passes the check. It runs after the route rate limit and before path validation.
Both token paths also have a second CORS policy that answers every origin, which the [cross-origin request contract](/http-api/#cross-origin-requests) states in full.
`/v1/webhooks/{webhook_id}/{token}` and `/v1/webhooks/{webhook_id}/{token}/messages/{message_id}` also have a second CORS policy that allows every origin, which the [cross-origin request contract](/http-api/#cross-origin-requests) states in full.
## Webhook object
@@ -98,7 +98,7 @@ Every operation returning a [webhook object](#webhook-object) or a token webhook
## Webhook message body
[Execute webhook](#execute-webhook) accepts this JSON body. Every field may be omitted, and `{}` is a valid body that fails later as an empty message.
[Execute webhook](#execute-webhook) accepts this JSON body. Every field may be omitted, and `{}` passes schema validation and then returns 400 `CANNOT_SEND_EMPTY_MESSAGE`.
### Structure
@@ -117,11 +117,11 @@ Every operation returning a [webhook object](#webhook-object) or a token webhook
| username?<sup>8</sup> | ?string | Per-message webhook name override (1-80 characters), or null |
| avatar_url?<sup>8</sup> | ?string | Absolute `http` or `https` per-message webhook avatar URL override of at most 2,048 characters, or null |
<sup>1</sup> A webhook author always resolves an effective [max_message_length](/http-api/instance/#limit-keys) of at least 4000 characters, and a higher configured value for the guild raises it further. Exceeding it returns 400 `INVALID_FORM_BODY` with the validation code `CONTENT_EXCEEDS_MAX_LENGTH`
<sup>1</sup> Webhook message content is limited to the resolved [max_message_length](/http-api/instance/#limit-keys) value for the guild or 4000 characters, whichever is larger. Exceeding it returns 400 `INVALID_FORM_BODY` with the validation code `CONTENT_EXCEEDS_MAX_LENGTH`
<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 uses the ordinary message attachment contract
<sup>3</sup> A JSON body attaches no file. A multipart body attaches files as described in [Execute webhook](#execute-webhook)
<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)
@@ -295,7 +295,7 @@ These objects define the body [Execute GitHub webhook](#execute-github-webhook)
| review? | ?[GitHub review](#github-review-object) object | Pull request review data, or null |
| sender | [GitHub user](#github-user-object) object | Event sender |
<sup>1</sup> The value gates rendering for most event types, and the exact accepted action for each type is stated in [GitHub event types](#github-event-types)
<sup>1</sup> An event type that lists one or more required actions in [GitHub event types](#github-event-types) renders a message only when `action` is one of those values
<sup>2</sup> A pull request uses the same representation as an issue, so only the fields listed in the [GitHub issue structure](#github-issue-object) are consumed
@@ -522,7 +522,7 @@ The value of the `X-GitHub-Event` request header selects the rendering. An event
<sup>4</sup> A forced push requires `head_commit` and compare, and renders a compare link as its whole description
<sup>5</sup> A conclusion of `skipped` on the relevant suite suppresses the message
<sup>5</sup> A `check_suite` event whose check suite has the conclusion `skipped` produces no message. A `check_run` event produces no message when its parent check suite has the conclusion `skipped`
An ordinary push requires compare and at least one commit, and renders one line for each commit. The line set is not capped by count, and the description built from them is truncated to the ordinary 350-character ceiling.
@@ -642,7 +642,7 @@ A backfilled incident or maintenance item appends `Backfilled` to the footer tex
<sup>3</sup> A true value appends `Backfilled` to the embed footer, separated from the page footer text by a vertical bar. When the page supplies no footer text, `Backfilled` is the whole footer
<sup>4</sup> The embed timestamp is the latest update's `created_at`, then `updated_at`, then `created_at`, and the first value the runtime can parse as a date wins
<sup>4</sup> The embed timestamp is the first of these values that parses as a date: the `created_at` of the latest update, then this object's `updated_at`, then this object's `created_at`
### Instatus maintenance update object
@@ -840,7 +840,7 @@ The decoded bytes must be at most the resolved [avatar_max_size](/http-api/insta
<sup>1</sup> An avatar failure names the `avatar` path with `BASE64_LENGTH_INVALID`, `INVALID_BASE64_FORMAT`, `IMAGE_SIZE_EXCEEDS_LIMIT`, or `INVALID_IMAGE_FORMAT`
<sup>2</sup> The reached allowance is in a top-level member named after its limit key
<sup>2</sup> The error response has a top-level `max_webhooks_per_guild` or `max_webhooks_per_channel` field whose value is the allowance that was reached
| Condition | Error |
| --- | --- |
@@ -1021,7 +1021,7 @@ The operation removes the webhook, frees its guild and channel webhook slot, and
## Token routes
The routes below take the matching webhook ID and token in their paths as the complete credential. The `Authorization` header is neither required nor read on any of them.
The routes below take the matching webhook ID and token in their paths as the complete credential. None of them requires the `Authorization` header, and a header sent to one grants no access. Fluxer still resolves it, as [Rate limit keying](#rate-limit-keying) describes.
## Get webhook with token
@@ -1302,7 +1302,7 @@ Any other type returns 400 `CANNOT_MODIFY_SYSTEM_WEBHOOK`, and a forward, which
### Side effects
The operation replaces the supplied fields, and a content change marks the message edited. Existing mentions stay unchanged and `allowed_mentions` is ignored. Supplying embeds replaces the complete collection and validates its attachment references. Sessions that can read the channel receive [Message Update](/gateway/events/#message-update).
The operation replaces the supplied fields, and a content change marks the message edited. Existing mentions stay unchanged and `allowed_mentions` is ignored. Supplying embeds replaces the complete collection. An embed image or thumbnail URL of the form `attachment://{filename}` returns 400 `INVALID_FORM_BODY` unless `{filename}` names an attachment already on the message with a supported image file extension. Sessions that can read the channel receive [Message Update](/gateway/events/#message-update).
### Rate limit
@@ -1403,7 +1403,7 @@ An unrecognised event type, a recognised type whose required fields or action ar
<RouteHeader method="POST" path="/v1/webhooks/{webhook_id}/{token}/slack" />
Accepts a Slack-compatible callback, converts it to the Fluxer message contract, and creates one webhook-authored message.
Accepts a Slack-compatible callback, converts it to Fluxer message content and embeds, and creates one webhook-authored message.
A successful callback emits a [Message Create](/gateway/events/#message-create).
@@ -1441,9 +1441,9 @@ The success body is the literal string `ok` under the `text/html` content type,
Conversion creates one webhook-authored message with the converted content and embeds, and emits [Message Create](/gateway/events/#message-create) to sessions that can read the channel.
A supplied username replaces the author name on that message. A supplied `icon_url` that parses as an absolute URL is fetched through the media boundary and stored as the message avatar. The stored webhook name and avatar do not change. When the URL cannot be fetched, the callback still succeeds and the message has no avatar override.
A supplied username replaces the author name on that message. A supplied `icon_url` that parses as an absolute URL is fetched through the Media Proxy and stored as the message avatar. The stored webhook name and avatar do not change. When the URL cannot be fetched, the callback still succeeds and the message has no avatar override.
The webhook execution default applies, and every mention in the converted content is suppressed. The callback has no nonce, so a repeated callback creates a second message.
Fluxer suppresses every mention in the converted content, as [Execute webhook](#execute-webhook) does when `allowed_mentions` is omitted. The callback has no nonce, so a repeated callback creates a second message.
### Rate limit
@@ -1469,7 +1469,7 @@ A rendered callback emits a [Message Create](/gateway/events/#message-create).
The body is an [Instatus callback](#instatus-callback-object) object. A body the schema rejects returns 400 `INVALID_FORM_BODY`.
:::note[Deduplication is per webhook and callback]
Only the first callback for a webhook and a given incident update, maintenance update, or component update is processed within a 24-hour window. A repeated callback is acknowledged and creates no second message. Fluxer processes every callback that has no such identifier.
Only the first callback for a webhook and a given incident update, maintenance update, or component update is processed within a 24-hour window. A repeated callback is acknowledged and creates no second message. Fluxer processes every callback whose incident or maintenance item has no `id`, whose component update has no `component_id` or `created_at`, or that has no incident, maintenance item, or component update.
:::
A callback that renders nothing, and one whose message creation fails, both leave the identifier free for a later retry.
+1 -1
View File
@@ -16,7 +16,7 @@ Use the HTTP API to read and change resources, and Gateway [events](/gateway/eve
## Shared contracts
See [Authentication](/authentication/), [Errors](/http-api/errors/), [Rate limits](/topics/rate-limits/) and [Locales](/topics/locales/) for shared behaviour. Most resource identifiers are [snowflakes](/snowflakes/). Each resource documents exceptions.
See [Authentication](/authentication/), [Errors](/http-api/errors/), [Rate limits](/topics/rate-limits/) and [Locales](/topics/locales/) for shared behaviour. Most resource identifiers are [snowflakes](/snowflakes/). Each resource page states where it differs from this shared behaviour.
## Field notation
@@ -23,7 +23,7 @@ The Media Proxy serves Fluxer attachments, image assets, themes, entrance sound
<sup>2</sup> Served by a `static` endpoint alone
[Transformations](/media-proxy/transformations/) defines representation selection for every family that has one. [Responses and limits](/media-proxy/responses-and-limits/) lists statuses, size bounds, deadlines, and cache policies.
[Transformations](/media-proxy/transformations/) defines the query parameters that select a representation for the families that accept them. [Responses and limits](/media-proxy/responses-and-limits/) lists statuses, size bounds, deadlines, and cache policies.
## Base URLs
@@ -55,7 +55,7 @@ One Media Proxy process serves exactly one mode. The mode is fixed at startup an
The relay `PUT` is the only route with a mode gate, and it returns 404 outside `upload` mode. On a read that requests no transformation, only an `mp` endpoint rasterises SVG, so an `upload` endpoint returns the original SVG bytes. The [operator and internal endpoints](/media-proxy/routes/#operator-and-internal-endpoints) behave the same in every mode.
Which published base URL serves which mode is a deployment choice. The reference self-hosted deployment serves `endpoints.media` from an `upload` mode process.
The operator chooses which mode serves each published base URL. The reference self-hosted deployment serves `endpoints.media` from an `upload` mode process.
## Methods
@@ -63,7 +63,7 @@ Every read route accepts `GET` and `HEAD`. HEAD returns the same status and repr
The relay path accepts `PUT`. Any other method there returns 405 with an `Allow` header. An unknown path returns 404.
On the [signed external route](/media-proxy/routes/#get-signed-external-media), origin metadata can cause HEAD representation headers to differ from GET.
On the [signed external route](/media-proxy/routes/#get-signed-external-media), a HEAD request can be answered from the headers of an origin HEAD response, so its representation headers can differ from GET.
## Request headers
@@ -120,7 +120,7 @@ A range on a transformed response selects bytes from the result and returns 206
An origin that answers a forwarded range with SVG bytes under another media type can produce a 206 containing raw SVG.
:::
Disposition follows that declared type, so SVG mislabelled as an image or video media type is served inline.
Content disposition follows the media type the origin declared, so SVG mislabelled as an image or video media type is served inline.
## Representation headers
@@ -156,7 +156,7 @@ Every successful media representation uses `Cache-Control: public, max-age=31536
The upload relay is the only route that returns an `ETag`, and it relays the object storage value for the stored object. No read route sends an `ETag` or a `Last-Modified`, so a cache revalidates a representation by fetching it again.
:::note[External media is cached for a year too]
The signed path is derived from the target URL, so the bytes behind one unchanged target are cached for a year at both layers.
The signed path is derived from the target URL, so the bytes behind one unchanged target are cached for a year by browsers under `Cache-Control` and by shared caches under `CDN-Cache-Control`.
:::
A 416 response has `Accept-Ranges`, `Content-Range`, `Access-Control-Allow-Origin`, `Vary`, and `X-Robots-Tag`, with no `Content-Type` and no cache policy. Its body is empty.
@@ -175,4 +175,4 @@ Use the filename from `Content-Disposition` when saving a response. Transformati
The attachment, image asset, and signed external routes rasterise SVG to WebP, so a browser does not execute the document in the Media Proxy origin.
:::
On a non-transforming attachment read, an `mp` endpoint rasterises SVG to lossless WebP. A transforming request uses the `format` and `quality` it was given, and an image asset path defaults to `high`. A `static` mode endpoint serves the original bytes.
On a non-transforming attachment read, an `mp` endpoint rasterises SVG to lossless WebP. A transforming request uses the `format` and `quality` it was given, and an image asset path uses `high` when the request gives no `quality`, except for animated WebP output, which uses `auto`. A `static` mode endpoint serves the original bytes.
@@ -12,7 +12,7 @@ An unsuccessful response body is an English reason phrase under the content type
| Status | Body | Condition |
| --- | --- | --- |
| 400 | Bad request | An invalid storage key, dimension, format, or external path, an unparsable relay `partNumber`, a failed attachment or external transformation, or a relay upload the endpoint could not take<sup>1</sup> |
| 400 | Bad request | An invalid storage key, dimension, format, or external path, an unparsable relay `partNumber`, a failed attachment or external transformation, or a relay upload body the endpoint failed to read from the client<sup>1</sup> |
| 401 | Unauthorized | An invalid signed external path signature, a missing, malformed, or expired relay capability, or a missing or invalid internal bearer token |
| 403 | Media access denied | The media access allowlist rejected the client address |
| 403 | Forbidden | A relay capability presented for another bucket, key, method, `uploadId`, or `partNumber`, or `/_metrics` requested from a non-loopback address |
@@ -21,7 +21,7 @@ An unsuccessful response body is an English reason phrase under the content type
| 413 | Payload too large | A stored object, external body, upload body, or internal request body beyond its bound |
| 500 | Transcode failed | An image asset transcode failed and the source is not directly displayable |
| 500 | Internal server error | A relay spool write to the endpoint's own disk failed |
| 502 | Bad gateway | An object store read or write failed, an external origin could not be used, or an outbound socket deadline or a relay object storage write deadline expired |
| 502 | Bad gateway | An object store read or write failed, an external origin fetch failed, an external origin redirected in a loop or more than five times, or an external origin returned an unsuccessful status that the Media Proxy does not pass through, or an outbound socket deadline or a relay object storage write deadline expired |
| 503 | Service unavailable | The upload relay spool budget is exhausted, an external buffer reservation or allocation failed, or an external origin answered `/_metadata` with 429<sup>3</sup> |
| 504 | Gateway timeout | A transformation admission slot was unavailable or the transformation deadline expired |
@@ -31,9 +31,9 @@ An unsuccessful response body is an English reason phrase under the content type
<sup>3</sup> `/_metadata` is the only endpoint that remaps an origin 429. The signed external read route retains 429 as 429
A retained external origin status reaches the client as an upstream fetch failure on the signed external read route and as the canonical reason phrase of its status on `/_metadata`. An object store error that maps to no case above uses the canonical reason phrase of its status.
When the Media Proxy passes an external origin status through to the client, the body is `Upstream fetch failed` on the signed external read route. On `/_metadata`, the body is the canonical reason phrase of that status. An object store error that maps to no case above uses the canonical reason phrase of its status.
Every error a route produces uses `Cache-Control: no-store` and the standard [security headers](/media-proxy/overview/#representation-headers). The exceptions have the security headers and set no cache policy, and [Cache policies](#cache-policies) names them. No plain-text error has CORS headers unless it came from the [upload relay](/media-proxy/upload-relay/), and no error has `Retry-After` or a request identifier.
Every error a route produces uses `Cache-Control: no-store` and the standard [security headers](/media-proxy/overview/#representation-headers). A 416 response, a media access allowlist rejection, and the empty-body method rejection of a registered path have the security headers and set no cache policy, and [Cache policies](#cache-policies) names them. No plain-text error has CORS headers unless it came from the [upload relay](/media-proxy/upload-relay/), and no error has `Retry-After` or a request identifier.
### Handling contract
@@ -62,9 +62,9 @@ Every `HEAD` response has an empty body, so the Body column describes `GET`, `PU
<sup>1</sup> The [internal endpoints](/media-proxy/routes/#operator-and-internal-endpoints) `/_metadata` and `/_frames` answer 200 with JSON, `/_health` with plain text, and `/_metrics` with the Prometheus text exposition
<sup>2</sup> A path served by the read fallback answers with the [media error response](#media-error-response). Each registered path answers with an empty body, no `Content-Type`, and an `Allow` header
<sup>2</sup> A read route path, or any other path outside the registered paths, answers with the [media error response](#media-error-response). Each registered path answers with an empty body, no `Content-Type`, and an `Allow` header
<sup>3</sup> A retained external origin status is the only source of these statuses
<sup>3</sup> These statuses occur only when an external origin returned that status and the Media Proxy passed it through
<sup>4</sup> A locally unsatisfiable range answers with an empty body, `Content-Range: bytes */{size}`, and `Accept-Ranges: bytes`, and has no `Content-Type` and no cache policy
@@ -82,7 +82,7 @@ The third-party origin chose the status, and Fluxer passed it through.
Proxied or stored media is limited to 500 MiB, and exceeding that returns 413. The bound applies to a streamed object, a buffered object, an external response body, and any input selected for transformation. When a streamed external body passes the bound only after the response head is committed, the Media Proxy truncates it.
A decoded signed external target URL is limited to 8,192 bytes, and a longer URL returns 400. The route follows at most five redirects. A sixth redirect or a redirect loop returns 502. Every redirect target is subject to the same URL limit and address policy.
A decoded signed external target URL is limited to 8,192 bytes, and a longer URL returns 400. The route follows at most five redirects. A sixth redirect or a redirect loop returns 502. Every redirect target is subject to the same URL limit and the same public address check, which blocks non-public IP addresses and every port other than 80 and 443.
Decoded images are limited to 16,384 pixels on either edge and 268,435,456 pixels in total. Animated input is limited to 20,000 frames and 1,073,741,824 decoded pixels across all frames. No configuration changes these bounds. [Transformations](/media-proxy/transformations/#transformation-limits) defines the resulting failure statuses.
@@ -62,9 +62,9 @@ The path has no signature and no expiry, so anyone holding the URL reads the obj
When a transformation is requested, `Range` selects bytes from the transformed result.
An SVG attachment with a transformation parameter is rasterised, and `format` and `quality` select the output as they do for any other source, defaulting to WebP. Without a transformation parameter, only an `mp` endpoint rasterises it, always as lossless WebP. An `upload` endpoint returns the stored SVG bytes.
An SVG attachment with a transformation parameter is rasterised, and `format` and `quality` select the output as they do for any other source, defaulting to WebP. Without a transformation parameter, only a Media Proxy in [`mp` deployment mode](/media-proxy/overview/#deployment-modes) rasterises it, always as lossless WebP. A Media Proxy in `upload` mode returns the stored SVG bytes.
A video source requires an explicit image `format`. Another transformation parameter without `format` returns 400. A source that is neither an image nor a video returns 400 when `format` is present. Without `format`, Fluxer returns it unchanged.
A video source requires an explicit image `format`. A video request with `width`, `height`, `quality`, or `animated=true` and no `format` returns 400. A source that is neither an image nor a video returns 400 when `format` is present. Without `format`, Fluxer returns it unchanged.
### Response
@@ -131,11 +131,11 @@ A redirect beyond the [five-redirect bound](/media-proxy/responses-and-limits/#r
`effort` is read only on [Get attachment](#get-attachment) and is ignored here.
A transformation begins when `width`, `height`, `format`, or `quality` is present, when `animated` resolves to true, when the target filename ends in `.svg`, or when the origin body is SVG by media type or by its first bytes. A transforming request forwards no `Range` to the origin and applies the client range to the transformed bytes, so it still selects 206 or 416.
A transformation begins when `width`, `height`, `format`, or `quality` is present, when `animated` resolves to true, when the target filename ends in `.svg`, or when the origin body is SVG by media type or by its first bytes. A transforming request forwards no `Range` to the origin and applies the client range to the transformed bytes, so a satisfiable range returns 206 and an unsatisfiable range returns 416.
A non-transforming request can forward the range to the origin. Handle both 200 and 206 responses and use their range headers, as described in [Byte ranges](/media-proxy/overview/#byte-ranges).
Insufficient capacity to process the body returns 503. Origin metadata can make `HEAD` representation headers differ from `GET`.
Fluxer returns 503 when it cannot reserve or allocate a buffer for the origin body. A `HEAD` with no transformation and no forwarded range can build `Content-Type` and `Content-Length` from the origin's own `HEAD` response, so those headers can differ from a `GET` response.
A video target requires an explicit image `format` to produce a thumbnail. Without one, Fluxer returns the original bytes unchanged, and it does the same for a target that is neither an image nor a video.
@@ -149,13 +149,13 @@ A video target requires an explicit image `format` to produce a thumbnail. Witho
| 401 | Unauthorized | The path signature is invalid |
| 413 | Payload too large | The origin declared or delivered more than 500 MiB |
| 416 | empty | A local range is unsatisfiable |
| 502 | Bad gateway | The origin could not be reached, exceeded the redirect bound, or returned a status outside the retained set |
| 503 | Service unavailable | Insufficient capacity to process the body |
| 502 | Bad gateway | The origin could not be reached, exceeded the redirect bound, or returned an unsuccessful status that the paragraph below this table does not list |
| 503 | Service unavailable | Fluxer could not reserve or allocate a buffer for the origin body |
| 504 | Gateway timeout | Transformation capacity was unavailable or the transformation exceeded its deadline |
<sup>1</sup> A forwarded range relays the origin `Content-Range` and `Content-Length` unchanged and omits either header the origin did not send, so a chunked origin 206 produces a 206 with no `Content-Length`. A local range over buffered or transformed bytes always has both
An origin status of 400, 401, 403, 404, 405, 406, 408, 409, 410, 411, 412, 413, 414, 415, 416, 428, or 429 reaches the client unchanged as an upstream fetch failure, and the origin body and headers are not returned. Any other unsuccessful origin status becomes 502. [Common responses](#common-responses) also apply.
An origin status of 400, 401, 403, 404, 405, 406, 408, 409, 410, 411, 412, 413, 414, 415, 416, 428, or 429 reaches the client unchanged with the plain-text body `Upstream fetch failed`, and the origin body and headers are not returned. Any other unsuccessful origin status becomes 502. [Common responses](#common-responses) also apply.
### Side effects
@@ -231,7 +231,7 @@ Fluxer matches an asset path by its shape alone. A path with the wrong number of
Use the hash returned by the API. Do not calculate one from the image yourself.
An owner segment of the literal `.` or `..` parses as an asset path but produces an unsafe storage key and returns 400. So does a hash of the literal `a_` on a path that strips the prefix.
An owner segment of the literal `.` or `..` parses as an asset path but produces an unsafe storage key and returns 400. So does a hash of the literal `a_` on an avatar, icon, branding, banner, splash, embed splash, or guild member asset path.
### Query parameters
@@ -259,7 +259,7 @@ An owner segment of the literal `.` or `..` parses as an asset path but produces
| --- | --- | --- |
| 200 | Complete asset representation | The original or transformed asset is available |
| 206 | Selected asset bytes | One range is satisfiable |
| 400 | Bad request | An owner segment of `.` or `..`, or a hash of `a_` on a prefix-stripping path, makes the storage key unsafe |
| 400 | Bad request | An owner segment of `.` or `..`, or a hash of `a_` on a path other than an emoji or sticker path, makes the storage key unsafe |
| 404 | Not found | The path is not a valid asset path or the asset does not exist |
| 416 | empty | The range is unsatisfiable |
| 500<sup>1</sup> | Transcode failed | A required transcode failed and the source is not directly displayable |
@@ -493,7 +493,7 @@ These paths are not part of the public API surface. They are exempt from the med
| Path | Method | Description |
| --- | --- | --- |
| `/_health` | GET | Returns 200 while the process is running |
| `/_metrics` | GET | Returns the Prometheus text exposition of the process. Only a loopback address is served, and any other address returns 403 |
| `/_metrics` | GET | Returns the Prometheus text exposition of the process. A request from a non-loopback address returns 403 |
| `/_metadata` | POST | Extracts media metadata, a placeholder, and an optional NSFW verdict for the HTTP API |
| `/_thumbnail` | POST | Produces a WebP thumbnail of a staged upload for the HTTP API |
| `/_frames` | POST | Extracts one JPEG video frame for the HTTP API |
@@ -12,7 +12,7 @@ The theme, entrance sound, and static object routes read no parameter and never
An attachment or signed external request transforms when `width`, `height`, `format`, or `quality` is present, or when `animated` resolves to true. `download` and `effort` select no transformation.
A signed external request also transforms when the target filename ends in `.svg`, and when the origin body is SVG by media type or by its first bytes, whatever the filename. An SVG origin body is transformed unless the origin answered a forwarded range with a 206 under a non-SVG media type, which the proxy relays as raw bytes.
A signed external request also transforms when the target filename ends in `.svg`, and when the origin body is SVG by media type or by its first bytes, whatever the filename. An SVG origin body is transformed. One case skips the transformation. When the origin answers a forwarded range with a 206 and a non-SVG media type, the proxy relays those raw bytes.
The Media Proxy still rasterises an SVG attachment that selects no transformation. That path always uses lossless WebP and runs only in [`mp` mode](/media-proxy/overview/#deployment-modes).
@@ -27,7 +27,7 @@ The Media Proxy canonicalises nothing and issues no redirect, so two spellings o
| width?<sup>1</sup> | integer | The output width for an attachment or signed external request |
| height?<sup>1</sup> | integer | The output height for an attachment or signed external request |
| size?<sup>2</sup> | integer | The requested square edge for an image asset request |
| format?<sup>3</sup> | string | The requested output format under the rules for the route |
| format?<sup>3</sup> | string | The requested output format under [Attachment and external formats](#attachment-and-external-formats) or [Image asset formats](#image-asset-formats) |
| quality? | string | The requested [quality profile](#quality) |
| animated?<sup>4</sup> | boolean | Whether animated output is requested |
| effort?<sup>5</sup> | integer | The WebP encoder effort for an attachment request |
@@ -39,7 +39,7 @@ The Media Proxy canonicalises nothing and issues no redirect, so two spellings o
<sup>3</sup> An image asset request accepts the same value under the name `fmt`, and reads `fmt` only when `format` is absent
<sup>4</sup> Overrides the route default in both directions
<sup>4</sup> A true value requests animated output and a false value requests static output, whatever the route default
<sup>5</sup> An empty or unparsable value is ignored. A value from 10 through 255 is clamped to 9, and a value above 255 does not parse, so it too is ignored
@@ -120,13 +120,13 @@ Quality names are matched exactly and are case-sensitive. An unrecognised value
<sup>1</sup> PNG, APNG, and GIF have fixed encoder settings and ignore `quality`, so the quality number reaches WebP and JPEG output only
<sup>2</sup> Selects quality automatically based on the source
<sup>2</sup> Selects `lossless` for animated WebP output from a GIF or APNG source of at most 4 MiB and at most 16,777,216 pixels across all frames. Every other request selects `high`
<sup>3</sup> Lossless applies to WebP alone, and JPEG at quality 100 is still a lossy encode
An image asset defaults to `high`. An attachment or signed external image defaults to `lossless`, except that a JPEG, HEIC, or HEIF source defaults to `high`. Animated WebP output defaults to `auto` on every route that reads `quality`. Fluxer extracts a video thumbnail at `high`, and `quality` then applies only to the resize step that `width` or `height` requests. A non-transforming SVG rasterisation always uses `lossless`.
The attachment-only `effort` parameter controls WebP encoding effort. Values above the selected encoder's maximum are clamped. Other output formats ignore it.
The attachment-only `effort` parameter controls WebP encoding effort. Fluxer clamps a value above 9 to 9. Only lossless animated WebP output uses a value above 6. Other output formats ignore it.
## Animation
@@ -154,9 +154,9 @@ An attachment source that is neither an image nor a video returns 400 when `form
## Original representations
The Media Proxy returns the original bytes when the source already has the selected format, no resize or crop is required, and no encoder option requires a new representation. A source whose bytes sniff as animated also requires a request that resolves to animated, and a static request against it is encoded.
The Media Proxy returns the original bytes when the source already has the selected format, no resize or crop is required, and no `effort` or `quality` value forces encoding. A source whose bytes sniff as animated also requires a request that resolves to animated, and a static request against it is encoded.
An `effort` value forces encoding, and a `quality` value forces encoding for every source except GIF. With neither `width` nor `height`, an animated attachment or signed external request for the source's own GIF, WebP, or APNG format bypasses both tests and can still reuse the original animation.
An `effort` value forces encoding, and a `quality` value forces encoding for every source except GIF. With neither `width` nor `height`, an animated attachment or signed external request for the source's own GIF, WebP, or APNG format ignores both the `effort` test and the `quality` test and can still reuse the original animation.
Use the response `Content-Type`, which can differ from the filename extension or stored metadata.
@@ -166,10 +166,10 @@ A stored object above the [500 MiB media bound](/media-proxy/responses-and-limit
Decoded images are limited to 16,384 pixels on either edge and 268,435,456 pixels in total. Animated input is also limited to 20,000 decoded frames and 1,073,741,824 decoded pixels across all frames. These bounds are fixed. Exceeding a decoded image or animation limit fails the transformation.
Animated WebP and animated APNG output is bounded again at encode time, and exceeding one of those bounds truncates the output. The encoder stops adding frames after 20,000 frames, or once the accumulated frame delays reach 30,000 ms of playback, and emits the frames it already has. An operator can configure the frame cap from 1 through 100,000 and the playback cap from 100 through 600,000 ms. Animated GIF output has neither cap on either of its paths, so the decode limits above are its only bound.
Animated WebP and animated APNG output is bounded again at encode time, and exceeding one of those bounds truncates the output. The encoder stops adding frames after 20,000 frames, or once the accumulated frame delays reach 30,000 ms of playback, and emits the frames it already has. An operator can configure the frame cap from 1 through 100,000 and the playback cap from 100 through 600,000 ms. Animated GIF output has neither cap on any encoding path, so the decode limits above are its only bound.
The transformation deadline defaults to 15,000 ms and can be configured from 1,000 through 120,000 ms. WebP or APNG animation can be shortened to meet it. A GIF resize that reaches the deadline fails.
An attachment or signed external transformation failure returns 400. An image asset transformation failure returns 500 when the source is not directly displayable, and otherwise returns 200 with the original stored bytes and no `Content-Disposition`.
An attachment or signed external transformation failure returns 400. An image asset transformation failure returns 500 when the stored media type does not begin with `image/` or is `image/avif`, `image/heic`, `image/heif`, or SVG, and otherwise returns 200 with the original stored bytes and no `Content-Disposition`.
A transformation refused admission returns 504, and so does one that exceeds its deadline. [Responses and limits](/media-proxy/responses-and-limits/) defines the admission capacity and the deadlines.
@@ -15,7 +15,7 @@ Paths here are relative to the relay base in the issued upload URL. That base is
The capability is the complete authorisation. The relay reads no HTTP API `Authorization` header and has no request-count rate limit. Every path below `/v1/relay/` is exempt from the [media access policy](/media-proxy/overview/#access-restrictions). The `PUT` has a [deployment mode](/media-proxy/overview/#deployment-modes) gate and returns 404 outside `upload` mode.
:::note[Some upload URLs address object storage directly]
An upload URL may point directly to object storage. Use the URL returned by the HTTP API. This page describes relay URLs only.
An upload URL points directly to object storage when the caller's IP address geolocates to a country the deployment lists in `keep_direct_countries`. Use the URL returned by the HTTP API. This page describes relay URLs only.
:::
:::caution[The complete URL is authenticated]
@@ -71,7 +71,7 @@ The relay ignores every other request header.
The body is arbitrary bytes.
When `Content-Length` is supplied, send exactly that many bytes. A longer body returns 413 and a shorter body returns 400. The authorised upload size and endpoint body limit apply even without this header. Interrupted uploads return 400, and insufficient relay capacity returns 503.
When `Content-Length` is supplied, send exactly that many bytes. A longer body returns 413 and a shorter body returns 400. Without this header, a body above the authorised upload size or the endpoint body limit still returns 413. Interrupted uploads return 400, and insufficient relay capacity returns 503.
### Response
@@ -93,7 +93,7 @@ The relay forwards no object storage response body and no arbitrary response hea
<sup>1</sup> It has no relay CORS headers and no cache policy
:::caution[A message naming the key creates the attachment]
A successful relay response stores bytes only. [Create message](/http-api/messages/#create-message), [Modify message](/http-api/messages/#modify-message), and [Execute webhook](/http-api/webhooks/#execute-webhook) are the requests that can name the key.
A successful relay request stores the bytes and creates no attachment. [Create message](/http-api/messages/#create-message), [Modify message](/http-api/messages/#modify-message), and [Execute webhook](/http-api/webhooks/#execute-webhook) are the requests that can name the key.
:::
[Complete attachment upload](/http-api/messages/#complete-attachment-upload) assembles a multipart upload once every part is stored, and a singlepart upload needs no completion step. A stream preview becomes readable as soon as the stored object is in place, and the [Streams resource](/http-api/streams/) defines that flow.
@@ -117,6 +117,6 @@ A successful request writes the object the capability selects to the uploads buc
| Endpoint body limit | 500 MiB | 1 byte through 5 GiB |
| Object storage write deadline | 900,000 ms | 1,000 through 3,600,000 ms |
The effective limit is the smaller of the endpoint body limit and the authorised upload size.
The largest body the relay accepts is the smaller of the endpoint body limit and the authorised upload size.
For a streamed body the relay extends the object storage write deadline by one second for every 16 KiB of declared length.
When the request declares `Content-Length`, the relay extends the object storage write deadline by one second for every 16 KiB of declared length.
@@ -74,11 +74,11 @@ The object-storage secret, reused as `FLUXER_S3_SECRET_ACCESS_KEY`. `seaweedfs-i
#### `FLUXER_SUDO_MODE_SECRET`
Sudo mode JWTs, as a raw HS256 key. Invalidates every elevated session.
Sudo mode JWTs, as a raw HS256 key. Changing it ends every active sudo mode session.
#### `FLUXER_CONNECTION_INITIATION_SECRET`
Connection initiation tokens and harvest download links. Invalidates in-flight authorisations and issued download links.
Connection initiation tokens and harvest download links. Changing it invalidates connection initiation tokens not yet verified and harvest download links already issued.
#### `FLUXER_GATEWAY_RPC_AUTH_TOKEN`
@@ -94,7 +94,7 @@ Generate with `openssl rand -base64 32`. [Upload relay](/media-proxy/upload-rela
#### `FLUXER_ADMIN_SECRET_KEY_BASE`
Admin sessions, CSRF tokens, and OAuth state. Signs every admin out. The admin service refuses to start when it is empty.
Admin sessions, CSRF tokens, and OAuth state. Changing it signs every admin out. The admin service refuses to start when it is empty.
#### `FLUXER_ADMIN_OAUTH_CLIENT_SECRET`
@@ -213,11 +213,11 @@ Defaults to the derived endpoint. The marketing origin. The second allowed CORS
#### `FLUXER_INVITE_ENDPOINT`
Defaults to the derived endpoint. The invite base. Hostname is extracted and matched.
Defaults to the derived endpoint. The invite base. The API reads links on its hostname as invite links and does not unfurl them.
#### `FLUXER_GIFT_ENDPOINT`
Defaults to the derived endpoint. The gift base. Hostname is extracted and matched.
Defaults to the derived endpoint. The gift base. The API does not unfurl links on its hostname.
Internal endpoints address one container from another and never appear in a browser. `FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT` is required by `gifs`. The rest are optional.
@@ -247,25 +247,25 @@ Falls back to `FLUXER_STATIC_CDN_ENDPOINT`. The static origin used by `unfurl`.
## The edge
The `edge` container is the only HTTP entry point. Both layouts below work with the edge left alone.
The `edge` container is the only HTTP entry point. Neither layout below needs a change to the edge settings.
Bundled TLS is the default. `docker compose up -d` binds `80/tcp`, `443/tcp`, and `443/udp` and obtains its own certificate for `FLUXER_DOMAIN`. Point DNS at the host and set nothing else. The `443` publishes follow `FLUXER_PUBLIC_PORT`, so a non-default public port moves them with it.
Put your own reverse proxy in front by adding `docker-compose.proxy.yml`, a second Compose file that overrides parts of the first. Load it with `-f` twice, or set `COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml` in `.env` once. The edge then serves plain HTTP on one port, and the proxy in front terminates TLS. [Behind your own reverse proxy](/operator/reverse-proxy/) owns the switch, the requirements, and the per-proxy configuration.
Put your own reverse proxy in front by adding `docker-compose.proxy.yml`, a second Compose file that overrides parts of the first. Load it with `-f` twice, or set `COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml` in `.env` once. The edge then serves plain HTTP on one port, and the proxy in front terminates TLS. [Behind your own reverse proxy](/operator/reverse-proxy/) has the steps to enable the overlay, the requirements, and the per-proxy configuration.
All are optional.
#### `FLUXER_EDGE_SITE_ADDRESS`
Defaults to the address Compose builds from `FLUXER_PUBLIC_SCHEME`, `FLUXER_DOMAIN` and `FLUXER_PUBLIC_PORT`. What the edge listens on, and the default keeps the listener on the address the instance advertises. Honoured in the bundled layout only, because `docker-compose.proxy.yml` sets the literal `:8080` and `tunnel.compose.yml` the literal `:80`, and either discards any `.env` value. Several hostnames therefore need the bundled layout. Write the scheme into any value you set here, because a bare hostname means automatic HTTPS on `443` whatever `FLUXER_PUBLIC_SCHEME` says.
Defaults to the address Compose builds from `FLUXER_PUBLIC_SCHEME`, `FLUXER_DOMAIN` and `FLUXER_PUBLIC_PORT`. What the edge listens on, and the default keeps the listener on the address the instance advertises. Honoured in the bundled layout only, because `docker-compose.proxy.yml` sets the literal `:8080` and `tunnel.compose.yml` the literal `:80`, and either discards any `.env` value. A site address that lists more than one hostname therefore needs the bundled layout. Write the scheme into any value you set here, because a bare hostname means automatic HTTPS on `443` whatever `FLUXER_PUBLIC_SCHEME` says.
#### `FLUXER_EDGE_TRUSTED_PROXIES`
Default `private_ranges`. Which upstream hops the edge believes about `X-Forwarded-For`. The default is `192.168.0.0/16`, `172.16.0.0/12`, `10.0.0.0/8`, `127.0.0.1/8`, `fd00::/8` and `::1`, which covers every same-host proxy. Narrow it to the proxy's own address when the proxy reaches the instance from a public address, or when clients reach the proxy from a range the default already covers.
Default `private_ranges`. The peer addresses whose `X-Forwarded-For` header the edge trusts. The default is `192.168.0.0/16`, `172.16.0.0/12`, `10.0.0.0/8`, `127.0.0.1/8`, `fd00::/8` and `::1`, which covers every same-host proxy. Narrow it to the proxy's own address when the proxy reaches the instance from a public address, or when clients reach the proxy from a range the default already covers.
#### `FLUXER_EDGE_BIND`
Default `127.0.0.1:8080`. Where the plain-HTTP port binds. Read only under the overlay. `0.0.0.0:8080` must be firewalled to the proxy host, because a trusted peer's `X-Forwarded-For` is honoured.
Default `127.0.0.1:8080`. Where the plain-HTTP port binds. Read only under the overlay. `0.0.0.0:8080` must be firewalled to the proxy host, because the edge takes the client address from the `X-Forwarded-For` header of any peer in `FLUXER_EDGE_TRUSTED_PROXIES`.
#### `FLUXER_HTTP_PORT`
@@ -273,17 +273,17 @@ Default `80`. The host side of the edge's HTTP publish, which serves the redirec
#### `FLUXER_HTTPS_PORT`
Defaults to `FLUXER_PUBLIC_PORT`. The host side of the edge's HTTPS publish, and the only thing it moves is that host side. Set it when the host already has something on the port the instance advertises. It moves the TCP and the UDP publish together, because HTTP/3 needs both on the same port. Same optional bind address, and it is not read under the overlay either.
Defaults to `FLUXER_PUBLIC_PORT`. The host side of the edge's HTTPS publish, and the only thing it moves is that host side. Set it when the host already has something on the port the instance advertises. It moves the TCP and the UDP publish together, because HTTP/3 needs both on the same port. It takes the same optional bind address in front of the port. The overlay does not read it.
#### `COMPOSE_FILE`
No default. Which Compose files are loaded. Read by Docker Compose itself. Set it to select the overlay for every command.
`FLUXER_CADDY_SITE_ADDRESS` is the old name for `FLUXER_EDGE_SITE_ADDRESS`. Compose reads it only when `FLUXER_EDGE_SITE_ADDRESS` is unset, so an older `.env` keeps the listener it already had and can be renamed at any time.
`FLUXER_CADDY_SITE_ADDRESS` is the old name for `FLUXER_EDGE_SITE_ADDRESS`. Compose reads it only when `FLUXER_EDGE_SITE_ADDRESS` is unset, so an older `.env` keeps the listener it already had. Rename `FLUXER_CADDY_SITE_ADDRESS` to `FLUXER_EDGE_SITE_ADDRESS` at any time.
## Client IP
The edge resolves one client address per request and rewrites `X-Forwarded-For` to it on every upstream hop, so no service reads what a visitor sent. When the peer is outside `FLUXER_EDGE_TRUSTED_PROXIES`, the edge uses the peer address and discards the header. When the peer is inside the list, the edge takes the rightmost header entry that is not itself trusted, so a proxy that appends still delivers the real caller. A client whose own address is inside the list resolves to the proxy, which is why [Trusted proxies](/operator/reverse-proxy/#trusted-proxies) tells a LAN or VPN deployment to narrow it.
The edge resolves one client address per request and rewrites `X-Forwarded-For` to it on every upstream hop, so no service reads what a visitor sent. When the peer is outside `FLUXER_EDGE_TRUSTED_PROXIES`, the edge uses the peer address and discards the header. When the peer is inside the list, the edge takes the rightmost header entry that is not itself trusted. A proxy that appends to the header therefore still passes on the real client address. A client whose own address is inside the list resolves to the proxy, which is why [Trusted proxies](/operator/reverse-proxy/#trusted-proxies) tells a LAN or VPN deployment to narrow it.
All are optional.
@@ -307,7 +307,7 @@ No default. Legacy unprefixed aliases. Accepted only by `admin` and `app-proxy`,
Default empty. Addresses exempt from IP bans. Comma separated. Every entry must parse as an IP or the API fails at boot.
The API rejects any request whose client-IP header is missing, empty, not a parsable address, or not trusted under `FLUXER_TRUST_CLIENT_IP_HEADER` with 403, except on `/_health`, `/webhooks/livekit`, `/test`, and the Bluesky client metadata and JWKS routes. The edge sets the header on every upstream hop, so this applies only to a layout that puts something directly in front of `api`. A proxy in front of the edge that never sets the header passes the check, and every request then looks as though it came from the proxy.
The API returns 403 for any request whose client-IP header is missing, empty, or not a parsable address, and for every request while `FLUXER_TRUST_CLIENT_IP_HEADER` is `false`. The exceptions are `/_health`, `/webhooks/livekit`, `/test`, and the Bluesky client metadata and JWKS routes. The edge sets the header on every upstream hop, so a missing or unparsable header happens only in a layout that puts something other than the edge directly in front of `api`. A proxy in front of the edge that never sets the header passes the check, and every request then looks as though it came from the proxy.
## Images
@@ -577,7 +577,7 @@ Compose configures the internal services. Change the following settings only whe
#### `FLUXER_SVC_NAME`
Default `default`. The metrics prefix and the concurrency default. Changing it does not rename NATS subjects.
Default `default`. The metrics prefix. It also selects the built-in default of `FLUXER_SVC_MAX_CONCURRENT_REQUESTS`. Changing it does not rename NATS subjects.
#### `FLUXER_SVC_MODE`
@@ -605,7 +605,7 @@ Defaults to 192 for messages, 320 for snowflakes, 64 otherwise. In-flight reques
#### `POD_NAME`
No default. The shard ordinal source and node identity. Also read by the Gateway and by API RPC timing.
No default. The shard ordinal source and node identity. Also read by the Gateway, and by the API as the `pod_name` in its RPC timing records.
For custom service routing, add these settings to the API container environment. The bundled Compose file does not forward them:
@@ -668,7 +668,7 @@ Default empty. The server-side LiveKit control API. Compose sets `http://livekit
#### `FLUXER_LIVEKIT_WEBHOOK_URL`
Default empty. Where LiveKit posts webhooks. The route is exempt from user auth and from both client-IP middlewares.
Default empty. Where LiveKit posts webhooks. The API accepts requests on this route without user authentication and without a client-IP header.
#### `FLUXER_LIVEKIT_DEFAULT_REGION`
@@ -694,9 +694,9 @@ Voice reconciliation has moved from `worker` to the separate recon service. The
## Email
Email is off by default, and the conditions below turn it on. The switch must be on, and the provider must be `smtp` with a complete SMTP configuration, meaning `FLUXER_EMAIL_FROM_EMAIL`, `FLUXER_EMAIL_SMTP_HOST`, `FLUXER_EMAIL_SMTP_PORT`, `FLUXER_EMAIL_SMTP_USERNAME`, and `FLUXER_EMAIL_SMTP_PASSWORD` are all non-empty. All are optional.
Email is off by default, and the conditions below turn it on. The email switch must be on. It is the value saved in the admin dashboard, or `FLUXER_EMAIL_ENABLED` when the dashboard has no saved value. Also, the provider must be `smtp` with a complete SMTP configuration, meaning `FLUXER_EMAIL_FROM_EMAIL`, `FLUXER_EMAIL_SMTP_HOST`, `FLUXER_EMAIL_SMTP_PORT`, `FLUXER_EMAIL_SMTP_USERNAME`, and `FLUXER_EMAIL_SMTP_PASSWORD` are all non-empty. All are optional.
The same conditions turn on a DNS check at registration. The check runs when email is on by the rule above. The dashboard switch and the SMTP values decide it along with the variable. The address domain has to publish an `MX` record, or an `A` or `AAAA` record as a fallback, and an address at a domain that publishes neither is answered `That email domain cannot receive mail.` however well formed it is. The first admin account is no exception, so an owner address at a `.lan`, `.internal` or `home.arpa` name needs email left off.
The same conditions turn on a DNS check at registration. The check runs when email is on by the rule above. A switch or SMTP value saved in the admin dashboard wins over the matching variable. The address domain has to publish an `MX` record, or an `A` or `AAAA` record as a fallback, and an address at a domain that publishes neither is answered `That email domain cannot receive mail.` however well formed it is. The first admin account is no exception, so an owner address at a `.lan`, `.internal` or `home.arpa` name needs email left off.
#### `FLUXER_EMAIL_ENABLED`
@@ -736,7 +736,7 @@ The same conditions turn on a DNS check at registration. The check runs when ema
#### `FLUXER_EMAIL_SMTP_SECURE`
`.env.example` `true`. Implicit TLS. Defaults to `true` whenever the SMTP block exists.
`.env.example` `true`. Implicit TLS. Defaults to `true` whenever the provider is `smtp`.
The API reads `FLUXER_EMAIL_WEBHOOK_SECRET` for inbound delivery webhooks. Neither `.env.example` nor `docker-compose.yml` has it.
@@ -799,7 +799,7 @@ For environment-based configuration, use `FLUXER_AUTH_BLUESKY_ENABLED`, `FLUXER_
The Gateway reads the same names. A malformed pair, or a private key that does not derive the public point, does not stop it. It records the fault in its log at startup and then drops every web push notification.
`FLUXER_GATEWAY_PUSH_ENABLED` is read by the Gateway alone, defaults to `true`, and turns that check off when it is `false`.
`FLUXER_GATEWAY_PUSH_ENABLED` is read by the Gateway alone, defaults to `true`, and skips that startup check on the VAPID pair when it is `false`.
## Mobile push
@@ -847,7 +847,7 @@ No default. The service account address. Paired with the project.
#### `FLUXER_PUSH_FCM_PRIVATE_KEY`
No default. The service account key. Use one of the key sources.
No default. The service account key. Set one of this, `FLUXER_PUSH_FCM_PRIVATE_KEY_PATH`, or `FLUXER_PUSH_FCM_SERVICE_ACCOUNT_JSON_PATH`.
#### `FLUXER_PUSH_FCM_PRIVATE_KEY_PATH`
@@ -883,7 +883,7 @@ Default empty. The webhook signing secret. Not forwarded by the shipped Compose
#### `FLUXER_STRIPE_PRICES`
Default `{}`. Every price ID at once. JSON object. The individual price variables are declared after it and win.
Default `{}`. Every price ID at once. JSON object. An individual price variable overrides the matching price in this object.
Individual price variables also exist, one per product and currency: `FLUXER_STRIPE_PRICE_MONTHLY_`, `FLUXER_STRIPE_PRICE_YEARLY_`, `FLUXER_STRIPE_PRICE_GIFT_1_MONTH_` and `FLUXER_STRIPE_PRICE_GIFT_1_YEAR_` in USD, EUR, BRL, DKK, INR, NOK, PLN, SEK, and TRY, plus `FLUXER_STRIPE_PRICE_VISIONARY_` and `FLUXER_STRIPE_PRICE_GIFT_VISIONARY_` in USD and EUR.
@@ -891,7 +891,7 @@ Individual price variables also exist, one per product and currency: `FLUXER_STR
Default `{}`. Retired price IDs, keyed by the same slot names `FLUXER_STRIPE_PRICES` uses, each mapped to a list: `{"monthly_brl": ["price_..."]}`. JSON object. Existing subscriptions on these prices keep renewing. The localised checkout catalogue uses `FLUXER_STRIPE_PRICES` alone.
Repricing a slot is ordered, and the order is not reversible without failed invoices. Create the new price in Stripe, move the ID it replaces into this variable, roll the API, and only then point `FLUXER_STRIPE_PRICES` at the new price. A price ID that neither variable names is unknown to the API: a renewal invoice on it fails the webhook with `Unknown product for invoice renewal`, a checkout completing on it fails with `Unknown price ID for checkout session`, and both keep failing until the ID is registered. The API answers Stripe as soon as the signature verifies and hands the event to `worker`, so the retries are the `processStripeWebhook` job's own: the Stripe dashboard shows the delivery as succeeded and the error is in the `worker` logs. Keep a retired ID listed for as long as any subscription still bills on it, which for a yearly price is at least a year after the switch.
Repricing a slot has a fixed order. Doing the steps in another order leaves a price ID unknown to the API, and invoices on that price fail. Create the new price in Stripe, move the ID it replaces into this variable, roll the API, and only then point `FLUXER_STRIPE_PRICES` at the new price. A price ID that neither variable names is unknown to the API: a renewal invoice on it fails the webhook with `Unknown product for invoice renewal`, a checkout completing on it fails with `Unknown price ID for checkout session`, and both keep failing until the ID is registered. The API answers Stripe as soon as the signature verifies and hands the event to `worker`, so any retry comes from `worker` rerunning the `processStripeWebhook` job: the Stripe dashboard shows the delivery as succeeded and the error is in the `worker` logs. Keep a retired ID listed for as long as any subscription still bills on it, which for a yearly price is at least a year after the switch.
## Moderation and abuse
@@ -1021,11 +1021,11 @@ Use the [admin dashboard](#runtime-settings-in-the-admin-dashboard) or [Admin in
Invalid saved configuration prevents the API and worker from starting. If a running process cannot apply an update, it logs the error and keeps its previous valid settings. Check the reported section and field paths, repair the saved configuration, then retry. It is not repaired automatically.
Older configurations retain these migration rules:
The API and worker read configuration saved by older releases under these rules:
- Gateway rollout accepts `nats_request_timeout_ms` only when `rpc_request_timeout_ms` is absent. Both require an integer from 1000 to 60000. Use the current name in Admin requests.
- Registration accepts `adminRegistrationUrlsEnabled` only when `admin_registration_urls_enabled` is absent. Missing registration settings default to `open` with admin registration URLs enabled.
- Missing SSO settings default to disabled, with enforcement following enablement and automatic provisioning enabled. Stored flags require `true` or `false`.
- Missing SSO settings default to disabled. A missing enforcement flag takes the value of the enabled flag, and automatic provisioning defaults to on. Stored flags require `true` or `false`.
- SSO allowed domains accept a JSON string array or a legacy comma-separated list of at most 100 entries. Domains are trimmed, lowercased, IDNA encoded and deduplicated. An empty list leaves domains unrestricted. Invalid lists must be repaired even while SSO is disabled.
- Missing registration URL and pending-registration lists mean empty lists. Invalid records are rejected rather than discarded. Timestamps require an explicit UTC marker or offset, and null is accepted only for nullable fields.
@@ -1033,7 +1033,7 @@ Explicit `false` values and supported null clears remain valid. Branding and int
## Limits
No environment variable changes an instance limit. Use the admin dashboard's Limit Config page or the [Admin API](/admin-api/). Effective limits combine saved settings, deployment defaults and premium policy. Changes take effect after a short propagation delay. Clients receive the [limit configuration object](/http-api/instance/#limit-configuration-object).
No environment variable changes an instance limit. Use the admin dashboard's Limit Config page or the [Admin API](/admin-api/). Effective limits combine saved settings, deployment defaults and premium policy. After a change is saved, every running `api` and `worker` process reloads the limits with no restart. Clients receive the [limit configuration object](/http-api/instance/#limit-configuration-object).
Request concurrency is separate from instance limits, and each process sets its own. All are optional.
@@ -1051,7 +1051,7 @@ Default `512`. Gateway NATS handler count.
#### `FLUXER_DISABLE_RATE_LIMITS`
Default `false`. Turns rate limits off. Read by the API and, as a raw presence check, by the Gateway.
Default `false`. Turns rate limits off. Read by the API and by the Gateway. The Gateway turns rate limits off only for the values `1`, `true` and `TRUE`.
#### `FLUXER_RELAX_REGISTRATION_RATE_LIMITS`
@@ -1079,7 +1079,7 @@ Falls back to `FLUXER_GATEWAY_LOGGER_LEVEL`. The Gateway log level at runtime. O
#### `BUILD_VERSION`
Default `dev`. The reported build. Baked into the images. A fallback outside development prints a warning.
Default `dev`. The reported build. Baked into the images. When `BUILD_VERSION` is unset or empty outside development, the process prints a warning.
#### `RELEASE_CHANNEL`
@@ -1193,7 +1193,7 @@ Default `none`. How cached copies of deleted or replaced media are purged. `none
#### `FLUXER_CACHE_PURGE_HTTP_ENDPOINT`
Default empty. Required when the adapter is `http`. Must be an absolute `http` or `https` URL without credentials, or startup fails. While purges are queued, the worker posts JSON `{"exact": [...], "prefix": [...]}` here every 10 seconds. Redirects are not followed. Any 2xx marks the batch done. The endpoint purges every proxy that caches media. A `400` or `422` makes the worker resend each URL alone, and a URL refused alone moves to the `cache_purge:rejected` key. Any other failure is retried on a later run.
Default empty. Required when the adapter is `http`. Must be an absolute `http` or `https` URL without credentials, or startup fails. While purges are queued, the worker posts JSON `{"exact": [...], "prefix": [...]}` here every 10 seconds. Redirects are not followed. Any 2xx marks the batch done. Your endpoint then purges those URLs from every proxy that caches media. A `400` or `422` makes the worker resend each URL alone, and a URL refused alone moves to the `cache_purge:rejected` key. Any other failure is retried on a later run.
#### `FLUXER_CACHE_PURGE_HTTP_TOKEN`
@@ -1237,7 +1237,7 @@ Default empty. The favicon. Its origin is added to the CSP by `app-proxy`.
#### `FLUXER_APP_THEME_COLOR`
Default empty. The theme colour. Its origin is added to the CSP by `app-proxy`.
Default empty. The theme colour.
#### `FLUXER_INSTANCE_SETUP_CONFIGURED`
@@ -1387,7 +1387,7 @@ Default `15000`. Per-transform timeout. Accepts 1000 to 120000.
#### `FLUXER_MEDIA_PROXY_MAX_ENCODE_FRAMES`
Defaults to the animated frame default. Animation frame ceiling. Accepts 1 to 100000.
Default `20000`. Animation frame ceiling. Accepts 1 to 100000.
#### `FLUXER_MEDIA_PROXY_MAX_ENCODE_DURATION_MS`
@@ -1433,7 +1433,7 @@ Default `1048576`. Presence push buffer size.
#### `FLUXER_GATEWAY_SHUTDOWN_DRAIN_WAIT_MS`
Default `5000`. How long a drain waits. Milliseconds.
Default `5000`. Milliseconds. The Gateway parses it into its configuration at startup, and nothing reads it after that, so it controls no drain.
#### `FLUXER_GATEWAY_HTTP_FAILURE_THRESHOLD`
@@ -1497,7 +1497,7 @@ The clamp bounds reach the Gateway from `.env`. To pin the count, set `FLUXER_ER
The Gateway protocol version is `1`. The Gateway answers any other value in `?v=` with 101, then a close frame reading `Invalid API version`. [Gateway overview](/gateway/overview/) has the close code.
The Gateway reads every `FLUXER_GATEWAY_` name straight from the environment, so all of them work.
The Gateway reads every `FLUXER_GATEWAY_` name straight from the environment, so any of them works once it is in the `gateway` container environment. The bundled Compose file passes only the names it lists, so add any other name through a Compose override file.
## App proxy settings
@@ -1744,7 +1744,7 @@ The internal services each run a router, which takes requests and holds no state
Every service has a memory limit and four also have a memory reservation, all under `deploy.resources`. Compose reads `gb` as 1024 MiB and `mb` as 1 MiB, so `5gb` is 5368709120 bytes. Plain `docker compose up` applies both keys on a single host, with no Swarm and no `--compatibility` flag. The engine rejects any limit below `6mb`, and rejects a limit lower than the same service's reservation with `Minimum memory limit can not be less than memory reservation limit`.
A limit is a ceiling. The limits below sum to 16.75 GiB and the stack does not need a host that large, because a container costs what it touches.
A limit is a ceiling. The limits below sum to 16.75 GiB and the stack does not need a host that large, because each container uses only the memory it allocates, up to its limit.
`deploy.resources.reservations.memory` becomes the container's cgroup v2 `memory.low`, which biases kernel reclaim towards other containers under host pressure. It reserves nothing on its own.
@@ -1804,7 +1804,7 @@ Default `1gb`. The ceiling for `gateway`. The BEAM has no heap ceiling of its ow
#### `FLUXER_MEDIA_PROXY_MEMORY_LIMIT`
Default `512mb`. The ceiling for `media-proxy`. Image and video transforms decode into this ceiling, so an oversize upload is where it binds.
Default `512mb`. The ceiling for `media-proxy`. Image and video transforms decode into this ceiling, so a large upload is what reaches this limit.
#### `FLUXER_STATIC_PROXY_MEMORY_LIMIT`
@@ -1946,7 +1946,7 @@ FLUXER_SEAWEEDFS_GOMEMLIMIT=768MiB
FLUXER_GIFS_SHARD_CACHE_MAX_BYTES=134217728
```
At 8 GB the api and worker heap ceilings fall to 664 MB each and Postgres caches less of the working set, which shows up as slower search and slower history scrolling under load. Nothing is turned off, though the largest attachments need the 16 GB SeaweedFS ceiling to upload every part at once.
At 8 GB the api and worker heap ceilings fall to 664 MB each and Postgres caches less of the working set, which shows up as slower search and slower history scrolling under load. Nothing is turned off, though the largest attachments need the default `2gb` SeaweedFS ceiling to upload every part at once.
The 4 GB profile trades more.
@@ -1974,7 +1974,7 @@ FLUXER_VALKEY_MAXMEMORY=128mb
FLUXER_GIFS_SHARD_CACHE_MAX_BYTES=67108864
```
At 4 GB the api heap ceiling is 396 MB and the worker heap ceiling is 332 MB. That covers chat. A large attachment and a search reindex at the same time exceed it. Media transforms above roughly 20 MB start failing in `media-proxy`, Meilisearch indexes a backlog more slowly, and a busy voice room is the first thing to drop. Keep `FLUXER_POSTGRES_SERVER_MAX_CONNECTIONS` at or above 110.
At 4 GB the api heap ceiling is 396 MB and the worker heap ceiling is 332 MB. That covers chat. A large attachment and a search reindex at the same time exceed those heap ceilings. Media transforms above roughly 20 MB start failing in `media-proxy`, Meilisearch indexes a backlog more slowly, and a busy voice room is the first thing to drop. Keep `FLUXER_POSTGRES_SERVER_MAX_CONNECTIONS` at or above 110.
## Routing
@@ -2000,7 +2000,7 @@ Losing `valkey-data` can delay scheduled account and bulk-message deletions whil
`nats-data` retains pending jobs in `JOBS` for up to 7 days and failed jobs in `JOBS_DLQ` for up to 30 days. A full jobs stream rejects new work. A full dead-letter stream drops its oldest entries, so investigate failures promptly. If dead-letter storage is unavailable, failed jobs remain in `JOBS` only until they expire. Losing this volume loses queued work, which is not automatically recovered from the database.
Startup refuses incompatible queue limits and never rewrites an existing stream. Stop publishers and workers before migrating an incompatible stream.
Startup refuses an existing `JOBS` or `JOBS_DLQ` stream with the wrong name, subjects, retention or storage type, or with sealing, no-ack, mirroring, sources, republishing or a subject transform set. Startup never recreates a stream. When the `JOBS` limits differ, startup updates them in place, unless tightening them would drop queued jobs. Stop publishers and workers before migrating an incompatible stream.
Volume names are prefixed with the Compose project name, so `postgres-data` is `fluxer_postgres-data` on the host.
@@ -140,7 +140,7 @@ The run prints one line per phase and ends with the URL to open.
### Serving on another port
The installer writes an `.env` for `https` on `443` and has no flag for another port. The lines below change it, the port of the public address and the publish that answers on it:
The installer writes an `.env` for `https` on `443` and has no flag for another port. The two lines below in `.env` move the instance to another port. `FLUXER_PUBLIC_PORT` sets the port of the public address, and `FLUXER_HTTPS_PORT` sets the host port the edge publishes HTTPS on:
```ini
FLUXER_PUBLIC_PORT=8443
@@ -172,7 +172,7 @@ Leave `FLUXER_PUBLIC_ORIGIN` commented out, or keep it consistent with the publi
| `--dry-run` | Print the plan. Write nothing and start nothing |
| `--no-start` | Write everything and skip `docker compose up -d` |
| `--update` | Upgrade: record, back up, refresh, pull, recreate, verify |
| `--rollback` | Put back the images and stack files of the newest record |
| `--rollback` | Restore the images and stack files that the newest `--update` run recorded |
| `--allow-root` | Permit running as root |
| `--engine <command>` | Container engine to drive. Default `docker`, or `podman` when `docker` is absent |
@@ -222,7 +222,7 @@ Open the hostname in a browser. A fresh instance is unconfigured, so it serves t
Create an account, then choose branding, registration mode, community policy, media expiry, integrations and premium settings. Completing the wizard closes setup access.
Create the owner account with an email address at a domain you control. The first registration that supplies one receives the wildcard admin ACL, which is full access to the admin dashboard, unless registration is set to hold new accounts for approval. Finishing the wizard grants that same ACL to whichever account completes it, when that account holds none.
Create the owner account with an email address at a domain you control. The first registration that includes an email address receives the wildcard admin ACL, which is full access to the admin dashboard, unless registration is set to hold new accounts for approval. Finishing the wizard grants that same ACL to whichever account completes it, when that account holds none.
Once email is on, the address goes through a DNS check before the account exists. Email counts as on when the switch is set and the provider is `smtp` with a complete SMTP configuration, so `FLUXER_EMAIL_ENABLED=true` on its own does not turn the check on. The address domain has to publish an `MX` record, or an `A` or `AAAA` record as a fallback. A name that resolves on your own network alone, such as a `.lan`, `.internal` or `home.arpa` name, publishes neither, and the account form answers `That email domain cannot receive mail.` whatever the address looks like. Use a domain with public records, or leave email off.
@@ -93,7 +93,7 @@ Browsers send it, and the admin dashboard refuses a mutating request with a cros
The instance sends its own policy, and its nonce is what lets the web app boot. Browsers enforce every policy they receive.
Every worked configuration below answers them all.
Every worked configuration below meets all of these requirements.
## The public address
@@ -408,7 +408,7 @@ Apple and Google require the association files at those fixed paths for saved-pa
`FLUXER_EDGE_TRUSTED_PROXIES` names the peer addresses whose `X-Forwarded-For` the edge believes. It defaults to `private_ranges`, which Caddy expands to `192.168.0.0/16`, `172.16.0.0/12`, `10.0.0.0/8`, `127.0.0.1/8`, `fd00::/8`, and `::1`, so a proxy on the same host or the same Docker network needs no change.
The edge resolves one client address per request and rewrites `X-Forwarded-For` to it on every upstream hop, so no service ever reads what a visitor sent. A peer outside the list becomes the client address, and the edge discards the header it sent. A peer inside the list contributes the rightmost header entry that is not itself trusted, and a proxy that appends still delivers the real caller.
The edge resolves one client address per request and rewrites `X-Forwarded-For` to it on every upstream hop, so no service ever reads what a visitor sent. A peer outside the list becomes the client address, and the edge discards the header it sent. For a peer inside the list, the edge uses the rightmost header entry that is not itself trusted. A proxy that appends to the header therefore still passes on the real client address.
A proxy that reaches the instance from a public address is not trusted, so the edge discards its header and attributes every request to the proxy itself. That puts all your users in one rate limit bucket and one geolocation. Add the address, and list `private_ranges` too if you still need it:
@@ -47,9 +47,9 @@ A failed run leaves the instance running on the images it already had.
`docker compose up -d` does not notice a changed `Caddyfile`. The script restarts `edge` by name to pick it up.
An instance older than the `/livekit` routing needs one edit no upgrade can make for it. [Voice signalling moved to /livekit](#voice-signalling-moved-to-livekit) has it, and voice stays silent until it is made.
An instance set up before the edge served LiveKit at `/livekit` needs one edit that the upgrade does not make. Until you make it, voice does not connect. [Voice signalling moved to /livekit](#voice-signalling-moved-to-livekit) has the edit.
The refreshed `docker-compose.yml` renames the `caddy` service to `edge`, and the two publish the same host ports. `--remove-orphans` takes the old container down in the same call, so the new one can bind them. Without it the step stops with `Bind for 0.0.0.0:443 failed`. It also removes any container in the project whose service the loaded Compose files no longer define, so keep `COMPOSE_FILE` the same across an upgrade. [Keep a local compose change](#keep-a-local-compose-change) has the file layout that survives one. [When the compose file list names a missing file](#when-the-compose-file-list-names-a-missing-file) has the failure a line naming an absent file produces.
The refreshed `docker-compose.yml` renames the `caddy` service to `edge`, and the two publish the same host ports. `--remove-orphans` takes the old container down in the same call, so the new one can bind them. Without it the step stops with `Bind for 0.0.0.0:443 failed`. It also removes any container in the project whose service the loaded Compose files no longer define, so keep `COMPOSE_FILE` the same across an upgrade. [Keep a local compose change](#keep-a-local-compose-change) has the file layout that survives one. [When the compose file list names a missing file](#when-the-compose-file-list-names-a-missing-file) has the error Compose prints when `COMPOSE_FILE` names a file that does not exist.
The rename also moves the `caddy-data` and `caddy-config` volumes to `edge-data` and `edge-config`, so the old two are left unused and the edge requests its certificate again on the first start.
@@ -112,7 +112,7 @@ curl -fsSL --proto '=https' --tlsv1.2 -o docker-compose.proxy.yml \
`main` is the ref for `FLUXER_IMAGE_TAG=v1` and for `latest`. [Match the images to the stack files](#match-the-images-to-the-stack-files) has the ref a pinned tag needs.
Removing the line from `.env` also gets the run moving and is the wrong fix. Without the overlay the edge publishes `80` and `443` and takes them from the reverse proxy already on the host.
Removing the line from `.env` also lets the upgrade run, and it is the wrong fix. Without the overlay, the edge publishes host ports `80` and `443`, which the reverse proxy already on the host uses.
## Run the upgrade
@@ -178,7 +178,7 @@ wss://chat.example.com/livekit
Both `https` and `wss` work. Update `FLUXER_LIVEKIT_URL` in `.env` to the new path, or remove it to use the derived address, then run `docker compose up -d api`.
This updates the existing `default-server-1` server in the `default` region. For any other server, change **Endpoint** under **Voice Servers** in the admin dashboard. Correct `.env` first, or a later restart can restore the old address on the default server.
When `api` starts, it writes the new address to the existing `default-server-1` server in the `default` region. For any other server, change **Endpoint** under **Voice Servers** in the admin dashboard. Correct `.env` first, or a later restart can restore the old address on the default server.
Voice media is unaffected. It never went through the edge, and `7881/tcp` and `7882/udp` still reach the host directly.
@@ -292,7 +292,7 @@ docker compose down
docker volume rm fluxer_postgres-data
```
On Windows, take that dump the way [Restore a backup](#restore-a-backup) moves one, with `-f /tmp/pre-major.dump` and `docker compose cp`, and restore it the same way.
On Windows, run `pg_dump` inside the container with `-f /tmp/pre-major.dump` and copy the file out with `docker compose cp`. Restore it with `docker compose cp` and `pg_restore` as [Restore a backup](#restore-a-backup) shows.
Once that volume is removed, the dump is the only copy of the database.
+1 -1
View File
@@ -24,7 +24,7 @@ A snowflake packs a timestamp, a worker ID, and a sequence into 64 bits. The wor
<sup>1</sup> The timestamp records the instant the identifier was issued, which can fall shortly before the resource exists
<sup>2</sup> Sequence order applies only within one worker and one millisecond
<sup>2</sup> A higher sequence value means a later identifier only when both identifiers come from one worker in one millisecond
The epoch is 1420070400000 milliseconds after the Unix epoch. Every Fluxer instance uses the same value. The lower 22 bits expose allocator details, so a client MUST NOT use them for routing or resource semantics.
@@ -14,7 +14,7 @@ The value `none` means the instance challenges no operation. A gated operation t
## Gated operations
The following operations verify a CAPTCHA while the instance enforces verification.
The following operations verify a CAPTCHA while discovery reports a `provider` other than `none`.
| Method | Route | Operation |
| --- | --- | --- |
@@ -30,7 +30,7 @@ Create private channel is gated only on the group direct message path, where the
## Exemption
Instance policy can exempt a request. Exemptions are not advertised, so clients must handle a challenge on every gated operation.
Fluxer skips the check in three cases, and the operation then proceeds with no CAPTCHA header. The instance account policy grants the `captcha_exempt` capability to the authenticated account's email address. The authenticated account holds the [`APP_STORE_REVIEWER`](/admin-api/users/#account-flags) flag. The request body has an `email` that belongs to an account holding that flag. Discovery does not report exemptions, so clients must handle a challenge on every gated operation.
## Request headers
@@ -64,6 +64,6 @@ A rejected solution or unavailable provider returns 400 `INVALID_CAPTCHA`. The r
| CAPTCHA_REQUIRED<sup>1</sup> | 400 | The operation is gated and the request has no solution |
| INVALID_CAPTCHA | 400 | The provider rejected the solution, or verification could not be completed |
<sup>1</sup> [Send phone verification](/http-api/users/phone-verification/#send-phone-verification) also answers this code when its phone attempt risk controls return a captcha decision. That operation is not gated and accepts no solution, so retrying it with `X-Captcha-Token` never helps
<sup>1</sup> [Send phone verification](/http-api/users/phone-verification/#send-phone-verification) also answers this code when Fluxer's risk check on the phone attempt decides that the request needs a CAPTCHA. That operation is not gated and accepts no solution, so retrying it with `X-Captcha-Token` never helps
Both codes are defined in the [API error code registry](/http-api/errors/#api-error-code-registry), and the body of each is the ordinary [error response](/http-api/#error-response) envelope.
@@ -57,9 +57,9 @@ An authenticated user's saved locale takes precedence over `Accept-Language`. Ot
Set the account locale through [user settings](/http-api/users/settings/) to make the choice persistent.
Matching ignores case and accepts underscores in place of hyphens. Exact supported tags and the aliases `en` and `sv` take precedence over regional fallbacks. Header quality weights order candidates within each group, with header order breaking ties.
Matching ignores case and accepts underscores in place of hyphens. Fluxer first looks for an exact supported tag, or the alias `en` or `sv`, anywhere in the header. It uses a regional fallback only when the header has none. In each of those two passes, Fluxer takes the tag with the highest quality weight, and header order breaks ties.
Unsupported regional tags can fall back to these defaults:
An unsupported tag such as `fr-CA` or `de-AT` matches no locale. An unsupported tag whose language subtag is in this table selects the locale beside it:
| Language subtag | Selected locale |
| --- | --- |
@@ -14,9 +14,9 @@ Every bucket is also keyed by the caller's identity. An authenticated request is
A request that resolves no account is keyed by the client IP address, exactly for IPv4 and by the `/64` for IPv6, so clients in the same `/64` share an allowance. Where the deployment is configured to read the address from a header the request does not have, Fluxer refuses the request with 403 `FORBIDDEN` before evaluating any bucket.
Fluxer also evaluates a route bucket against the global bucket unless the route declares that bucket exempt. The global bucket is keyed by the same identity, so a request that resolves no account consumes the global allowance of its client IP address.
A request also counts against the global bucket, unless its route bucket is declared exempt. The global bucket is keyed by the same identity, so a request that resolves no account consumes the global allowance of its client IP address.
These buckets are exempt, and each is the only bucket its route declares: `webhook:execute::webhook_id`, `webhook:message_get::webhook_id`, `webhook:message_edit::webhook_id`, `webhook:message_delete::webhook_id`, `webhook:github::webhook_id`, `webhook:instatus::webhook_id`, and `stripe:webhook`. Those routes draw on no global allowance. The `user:group_dm:create` and `user:group_dm:recipient:add` buckets are exempt as well. Each sits on a route that already consumed a non-exempt bucket, so both routes still draw on the global allowance.
These buckets are exempt, and each is the only bucket its route declares: `webhook:execute::webhook_id`, `webhook:message_get::webhook_id`, `webhook:message_edit::webhook_id`, `webhook:message_delete::webhook_id`, `webhook:github::webhook_id`, `webhook:instatus::webhook_id`, and `stripe:webhook`. Those routes draw on no global allowance. The `user:group_dm:create` and `user:group_dm:recipient:add` buckets are exempt as well. Each is a second bucket on a route whose first bucket is not exempt, so both routes still draw on the global allowance.
Every HTTP API and Admin API operation declares a bucket, apart from the [desktop download](/http-api/downloads/) routes, which declare none. A caller that sends no credential on a [Bluesky client document](/http-api/connections/#get-bluesky-client-metadata) is keyed by the client IP address.
@@ -36,7 +36,7 @@ Every bucket is a leaky bucket. It admits at most the declared limit at once and
A rate-limited request can return 429 even if its credentials would otherwise be rejected with 401 or 403.
:::
Some operations have additional allowances, including group direct message creation, adding group recipients, and deleting guild emoji or stickers. Each operation lists its limits. Rejected requests can still consume an allowance.
Some routes declare a second route bucket, including group direct message creation, adding group recipients, and deleting guild emoji or stickers. Each operation lists its limits. Fluxer counts a request against its route buckets before the handler runs, so a request the handler rejects still uses up allowance.
:::caution[A global denial revokes a user session]
When the global bucket denies a request authenticated by a non-bot account's user session token, Fluxer revokes that token before writing the 429. The client must authenticate again. A bot token, an OAuth2 access token, and an Admin API key are never revoked this way, and a route bucket denial never revokes a credential.
@@ -131,7 +131,7 @@ An allowance enforced inside a handler is keyed independently of the route bucke
The `disable_rate_limits` deployment switch turns off the login allowances along with both buckets. `relax_registration_rate_limits` turns off the registration allowances. Every other allowance below is enforced on every deployment.
A denial takes one of the shapes below. A send or submission allowance answers 429 with the [rate limit response object](#rate-limit-response-object) and the [rate limit headers](#rate-limit-headers) minus `X-RateLimit-Bucket`. A change allowance answers 400 `INVALID_FORM_BODY` with one [validation error](/http-api/#validation-error-object) entry whose `code` names the exhausted allowance.
A denial takes one of the shapes below. An allowance in [Allowances answering 429](#allowances-answering-429) answers 429 with the [rate limit response object](#rate-limit-response-object) and the [rate limit headers](#rate-limit-headers) minus `X-RateLimit-Bucket`. An allowance in [Allowances answering 400](#allowances-answering-400) answers 400 `INVALID_FORM_BODY` with one [validation error](/http-api/#validation-error-object) entry whose `code` names the exhausted allowance.
The 400 shape has no `retry_after` member, no `X-RateLimit-*` header, and no `Retry-After` header. The remaining delay appears only in the entry's localised `message`.
@@ -148,10 +148,10 @@ The 400 shape has no `retry_after` member, no `X-RateLimit-*` header, and no `Re
| [Request password recovery](/http-api/authentication/#request-password-recovery) | 20 per 30 minutes, keyed by the client IP address | `RATE_LIMITED` |
| [Request password recovery](/http-api/authentication/#request-password-recovery) | 5 per 30 minutes, keyed by the submitted email address | `RATE_LIMITED` |
| [Start email change](/http-api/users/email-and-password/#start-email-change) and [Resend original email code](/http-api/users/email-and-password/#resend-original-email-code) | 3 sends per 15 minutes, keyed by the authenticated account | `RATE_LIMITED` |
| [Request new email](/http-api/users/email-and-password/#request-new-email), [Resend new email code](/http-api/users/email-and-password/#resend-new-email-code), and both bounced recovery sends | 5 sends per 15 minutes, keyed by the authenticated account | `RATE_LIMITED` |
| [Request new email](/http-api/users/email-and-password/#request-new-email), [Resend new email code](/http-api/users/email-and-password/#resend-new-email-code), and both [bounced email recovery](/http-api/users/email-and-password/#bounced-email-recovery) sends | 5 sends per 15 minutes, keyed by the authenticated account | `RATE_LIMITED` |
| [Start password change](/http-api/users/email-and-password/#start-password-change) | 3 sends per 15 minutes, keyed by the authenticated account | `RATE_LIMITED` |
| [Resend password change code](/http-api/users/email-and-password/#resend-password-change-code) | 3 sends per 15 minutes, keyed by the authenticated account | `RATE_LIMITED` |
| Every code resend and every new-address request on an email or password change ticket | 1 send per 30 seconds, keyed by the previous send recorded on that ticket | `RATE_LIMITED` |
| Every code resend and every new-address request on an email or password change ticket | 1 send per 30 seconds, keyed by the ticket and counted from its previous send | `RATE_LIMITED` |
| [Report message](/http-api/reports/#report-message), [Report user](/http-api/reports/#report-user), [Report guild](/http-api/reports/#report-guild), and [Create DSA report](/http-api/reports/#create-dsa-report) | 5 per hour, keyed by the reporter, an account or a verified email address | `RATE_LIMITED` |
| [Report message](/http-api/reports/#report-message) | 3 per hour, keyed by the reporter and the channel together | `RATE_LIMITED` |
| [Report message](/http-api/reports/#report-message) | 20 per hour, keyed by the reported message, across all reporters | `RATE_LIMITED` |
@@ -181,9 +181,9 @@ The Resend IP authorisation cooldown has no `X-RateLimit-*` header. It has a `Re
| [Modify current guild member](/http-api/guild-members/#modify-current-guild-member) | 25 per 30 minutes on the guild pronouns, when the submitted value differs | `PRONOUNS_CHANGED_TOO_MANY_TIMES` |
| [Modify current guild member](/http-api/guild-members/#modify-current-guild-member) | 25 per 30 minutes on the guild accent colour, when the submitted value differs | `ACCENT_COLOR_CHANGED_TOO_MANY_TIMES` |
| [Modify voice activity sharing](/http-api/users/settings/#modify-voice-activity-sharing) | 1 per 24 hours on the sharing default | `VOICE_ACTIVITY_SHARING_ON_COOLDOWN` |
| [Complete login with TOTP](/http-api/authentication/#complete-login-with-totp) and [Complete login with WebAuthn MFA](/http-api/authentication/#complete-login-with-webauthn-mfa) | 10 per 15 minutes on one multi-factor attempt | `INVALID_CODE` |
| [Complete login with TOTP](/http-api/authentication/#complete-login-with-totp) and [Complete login with WebAuthn MFA](/http-api/authentication/#complete-login-with-webauthn-mfa) | 5 per 5 minutes on one multi-factor attempt against one MFA ticket | `INVALID_CODE` |
| [Sudo mode](/http-api/users/mfa/#sudo-mode) with the `totp` method | 10 per 15 minutes on one multi-factor attempt | `INVALID_MFA_CODE` |
| [Complete login with TOTP](/http-api/authentication/#complete-login-with-totp) and [Complete login with WebAuthn MFA](/http-api/authentication/#complete-login-with-webauthn-mfa) | 10 multi-factor attempts per 15 minutes | `INVALID_CODE` |
| [Complete login with TOTP](/http-api/authentication/#complete-login-with-totp) and [Complete login with WebAuthn MFA](/http-api/authentication/#complete-login-with-webauthn-mfa) | 5 multi-factor attempts per 5 minutes on one MFA ticket | `INVALID_CODE` |
| [Sudo mode](/http-api/users/mfa/#sudo-mode) with the `totp` method | 10 multi-factor attempts per 15 minutes | `INVALID_MFA_CODE` |
Every Modify current user allowance is keyed by the authenticated account, and the bot tag allowance by the bot account, so an owner changing a bot's tag draws on the bot's allowance. The guild member allowances are keyed by the guild and the member together, and one account holds a separate allowance in each guild. The login allowances are keyed by the account and by the MFA ticket respectively, and the sudo allowance by the account.
@@ -205,4 +205,4 @@ The allowance is one message for each interval the channel configures in `rate_l
## Other surfaces
Each protocol surface documents its own rate limit contract. The [main Gateway](/gateway/overview/) states its session, command, replay, backpressure, and admission limits in [Gateway limits and rate limits](/gateway/limits-and-rate-limits/). The [Media Proxy API](/media-proxy/overview/) has no request-count rate limit and bounds work through concurrency, payload, and deadline limits. The [upload relay](/media-proxy/upload-relay/) authorises each transfer with a bounded capability.
Each protocol surface documents its own rate limit contract. The [main Gateway](/gateway/overview/) states its session, command, replay, backpressure, and admission limits in [Gateway limits and rate limits](/gateway/limits-and-rate-limits/). The [Media Proxy API](/media-proxy/overview/) has no request-count rate limit and bounds work through concurrency, payload, and deadline limits. The [upload relay](/media-proxy/upload-relay/) authorises each transfer with an upload URL that expires and limits the body size.
@@ -19,7 +19,7 @@ When [instance features](/http-api/instance/#instance-features-object) reports `
[Request attachment upload URLs](/http-api/messages/#request-attachment-upload-urls) accepts up to 10 files. Declare each file's ID, name, exact byte count and content type. The endpoint reference defines the fields, permissions and file size limits.
Keep the returned plan, including its `upload_filename`. Use the returned `content_type`, which may differ from the declared value. The `upload_mode` determines the next steps.
Keep the returned plan, including its `upload_filename`. Use the returned `content_type`, which Fluxer derives from the file name and which can therefore differ from the declared value. The `upload_mode` determines the next steps.
### Upload modes
@@ -40,7 +40,7 @@ Use the `part_size` and `parts` returned in the upload plan. Every part must con
Send `PUT` requests to the returned `upload_url` values without an `Authorization` header. The URLs can target storage or the [upload relay](/media-proxy/upload-relay/). Do not rewrite their paths or query strings.
Send exactly the declared byte count. The relay returns 413 for an oversized body and 401 for a missing, invalid, or expired capability. Its own body limit also applies, with a default of 500 MiB.
Send exactly the declared byte count. The relay returns 413 for an oversized body and 401 for a missing, invalid, or expired capability. It also returns 413 for a body above its own limit, which is 500 MiB by default.
A singlepart transfer sends the whole file with the entry's `content_type` as its `Content-Type` header. A multipart transfer sends each part separately.
@@ -84,7 +84,7 @@ A preview does not prove that its connection is currently publishing a stream.
Use [Upload stream preview](/http-api/streams/#upload-stream-preview) to send the image as base64 in JSON. Canonical base64 uses the standard alphabet, a length divisible by four and at most two trailing `=` characters. Decoding and re-encoding must produce the same string. Invalid encoding returns 400 `INVALID_STREAM_THUMBNAIL_PAYLOAD`.
Alternatively, [request an upload URL](/http-api/streams/#create-stream-preview-upload-url) and send the JPEG with `PUT`. Use the returned `content_type`, respect `max_bytes` and upload before `expires_at`. The URL can be reused until it expires.
Alternatively, [request an upload URL](/http-api/streams/#create-stream-preview-upload-url) and send the JPEG with `PUT`. Use the returned `content_type`, send at most `max_bytes` bytes and upload before `expires_at`. The URL can be reused until it expires.
Send a valid JPEG of at most 1000000 bytes. [Get stream preview](/http-api/streams/#get-stream-preview) reads it, and [Delete stream preview](/http-api/streams/#delete-stream-preview) removes it.
+6 -6
View File
@@ -95,13 +95,13 @@ A new voice channel stores a `bitrate` of 64000. The ceiling is 96000, and the `
<sup>4</sup> MUTE_MEMBERS covers the `mute` field and DEAFEN_MEMBERS the `deaf` field of the [guild member update object](/http-api/guild-members/#guild-member-update-object). `PRIORITY_SPEAKER` and `USE_VAD` are defined and assignable [permission bits](/http-api/permissions/#permissions) that no HTTP route and no Gateway command evaluates
[ADMINISTRATOR](/http-api/permissions/) resolves to the complete mask before any channel overwrite is applied, so it satisfies every row of that table. The states below skip the VIEW_CHANNEL and CONNECT check. A member the guild is already moving is admitted. So is a member holding virtual access to the channel. The guild grants virtual access to a connected member that loses VIEW_CHANNEL or that a moderator moves into a channel it cannot see. Virtual access also grants SPEAK and STREAM in that channel on its own.
[ADMINISTRATOR](/http-api/permissions/) resolves to the complete mask before any channel overwrite is applied, so it satisfies every row of that table. Two cases skip the VIEW_CHANNEL and CONNECT check. A member the guild is already moving is admitted. So is a member holding virtual access to the channel. The guild grants virtual access to a connected member that loses VIEW_CHANNEL or that a moderator moves into a channel it cannot see. Virtual access also grants SPEAK and STREAM in that channel on its own.
A grant is evaluated when it is issued, and the guild re-evaluates a connection that is already open. Joining, moving, a region change, a role edit, an overwrite edit, and a member role change each recompute SPEAK and STREAM.
Fluxer checks the permissions in the table above when it issues a grant, and the guild checks them again for a connection that is already open. Joining, moving, a region change, a role edit, an overwrite edit, and a member role change each recompute SPEAK and STREAM.
The guild applies the new result to a live connection and issues no new [Voice Server Update](/gateway/events/#voice-server-update). The media server mutes a published microphone, camera, or screen share track the member may no longer publish, and drops a connection that fails the VIEW_CHANNEL and CONNECT check.
Moderation is an HTTP operation on the guild membership. [Modify guild member](/http-api/guild-members/#modify-guild-member) and [Modify current guild member](/http-api/guild-members/#modify-current-guild-member) share one request body. Each applies a moderator mute and a moderator deafen, moves a member between guild voice channels, and forces a disconnect. Both hold the caller to MUTE_MEMBERS, DEAFEN_MEMBERS, and MOVE_MEMBERS, including when the target is the caller itself. No other HTTP route and no Gateway command does any of it.
Moderation is an HTTP operation on the guild membership. [Modify guild member](/http-api/guild-members/#modify-guild-member) and [Modify current guild member](/http-api/guild-members/#modify-current-guild-member) share one request body. Each applies a moderator mute and a moderator deafen, moves a member between guild voice channels, and forces a disconnect. Both require the caller to hold MUTE_MEMBERS, DEAFEN_MEMBERS, and MOVE_MEMBERS for those changes, including when the target is the caller itself. No other HTTP route and no Gateway command does any of it.
That mute and that deafen reach the media server without a new credential. The change applies to every connection the account holds in that channel, and no [Voice Server Update](/gateway/events/#voice-server-update) follows.
@@ -121,7 +121,7 @@ That mute and that deafen reach the media server without a new credential. The c
A private call reads no `voice_connection_limit` and applies a fixed ceiling of 5 connections for each member.
A member whose `communication_disabled_until` is still in the future is refused with `VOICE_MEMBER_TIMED_OUT` before any permission or capacity check runs. An account that has never claimed its credentials is refused with `VOICE_UNCLAIMED_ACCOUNT` for a one-on-one direct message call and for any guild voice channel whose guild it does not own. A group direct message call is not refused. A session that did not identify with `e2ee_capable` is refused with `VOICE_E2EE_REQUIRED` while the guild has voice encryption enabled and every connection already in the channel is capable. A bot is exempt from that one.
A member whose `communication_disabled_until` is still in the future is refused with `VOICE_MEMBER_TIMED_OUT` before any permission or capacity check runs. An [unclaimed account](/http-api/authentication/#register-an-account) is refused with `VOICE_UNCLAIMED_ACCOUNT` for a one-on-one direct message call and for any guild voice channel whose guild it does not own. A group direct message call is not refused. A session that did not identify with `e2ee_capable` is refused with `VOICE_E2EE_REQUIRED` while the guild has voice encryption enabled and every connection already in the channel is capable. A bot is exempt from that one.
### Regions
@@ -143,7 +143,7 @@ The [Calls resource](/http-api/calls/) owns its HTTP surface, which reads whethe
A recipient's [incoming call flags](/http-api/users/#incoming-call-flags) decide whether it is rung. The flags can admit nobody, friends only, friends of friends, guild members, or everyone, and they can admit everyone silently.
[Get call eligibility](/http-api/calls/#get-call-eligibility) applies conditions of the caller's own before it reads that policy. A caller already connected to the channel's call is reported as not ringable. So is an unclaimed account in a direct message. The operation applies no recipient policy to a group direct message, and reports one as ringable unless the caller is already connected to its call.
[Get call eligibility](/http-api/calls/#get-call-eligibility) checks two conditions on the caller before it reads that policy. A caller already connected to the channel's call is reported as not ringable. So is an unclaimed account in a direct message. The operation applies no recipient policy to a group direct message, and reports one as ringable unless the caller is already connected to its call.
[Ring call recipients](/http-api/calls/#ring-call-recipients) applies neither of those conditions and evaluates the policy once per targeted recipient, in a group direct message as well as in a direct message. The result selects who is rung, so a recipient the policy excludes and a recipient it admits silently are both left out of the ringing set while the request still answers 204.
@@ -181,7 +181,7 @@ Every session the account holds receives the Dispatch, including sessions that a
An account chooses whether a friend is told which voice channel it is in. [Modify voice activity sharing](/http-api/users/settings/#modify-voice-activity-sharing) writes the account's default and rewrites the caller's side of every existing friendship to the same value in one operation. It then holds a 24 hour cooldown, and a second attempt inside that window is refused at the `share_voice_activity` path with the validation code `VOICE_ACTIVITY_SHARING_ON_COOLDOWN` and a `retry_after` in seconds.
The stored result is `share_voice_activity` on the caller's own [relationship object](/http-api/users/relationships/#relationship-object), and `friend_shares_voice_activity` reports the reciprocal record. That reciprocal is resolved by [List relationships](/http-api/users/relationships/#list-relationships) and by the [Relationship Update](/gateway/events/#relationship-update) Dispatches this operation emits for each rewritten friendship, one to the caller and one to the friend. Every other operation that returns a relationship object reports it as true.
The stored result is `share_voice_activity` on the caller's own [relationship object](/http-api/users/relationships/#relationship-object), and `friend_shares_voice_activity` reports the reciprocal record. [List relationships](/http-api/users/relationships/#list-relationships) reads `friend_shares_voice_activity` from that reciprocal record. So do the [Relationship Update](/gateway/events/#relationship-update) Dispatches this operation emits for each rewritten friendship, one to the caller and one to the friend. Every other operation that returns a relationship object reports `friend_shares_voice_activity` as true.
:::caution[The Gateway does not enforce voice activity sharing]
A [voice state](/gateway/events/#voice-state-object) reaches every session that can view the channel whatever `share_voice_activity` holds, so an account that shares nothing is still visible there. A client MUST NOT present the flag as concealment.