HTTP API
Use the HTTP API to read and change resources. Discover the base URL through /.well-known/fluxer. Third-party clients use endpoints.api_public, and the first-party web application uses endpoints.api_client.
Use /v1, the only API version.
Read resource limits from instance discovery. Attachment counts, expression counts and profile field lengths can differ between deployments.
Field notation explains optional, nullable, and nullish fields.
Request format
Section titled “Request format”Send JSON bodies with Content-Type: application/json. Responses are UTF-8 JSON unless an operation states otherwise. Message operations and webhook execution also accept multipart bodies, and OAuth2 token operations accept form bodies.
An overloaded instance returns 503 SERVICE_UNAVAILABLE with Retry-After: 1. Wait before retrying.
Unknown paths and unsupported methods return 404 NOT_FOUND without an Allow header. Trailing slashes are significant.
Every GET route supports HEAD, returning the same status and headers without a body. Routes without GET do not support HEAD. The same-host origin requirement also applies to HEAD.
Request body formats
Section titled “Request body formats”Each operation documents its accepted body format and fields.
Use application/json for JSON and application/x-www-form-urlencoded for OAuth2 form bodies. OAuth2 token operations also accept multipart/form-data.
Create message, Modify message and Execute webhook accept these multipart fields:
| Field | Type | Description |
|---|---|---|
| payload_json?1 | string | JSON object with the operation’s complete message request payload |
| files[n]?2 | file | Direct attachment file at zero-based index n |
1 An absent payload_json is read as an empty object. A value that is not a string, or does not parse as JSON, is rejected with INVALID_JSON_IN_PAYLOAD_JSON
2 n is a run of decimal digits, at most 10000 and below the deployment’s max_attachments_per_message limit, which defaults to 10
The legacy names file and file followed by an index are accepted as file fields as well, and a bare file takes the next free legacy index.
Field-name failures are rejected with their own code:
- An index outside either bound returns
FILE_INDEX_EXCEEDS_MAXIMUM. - Any other name beginning with
files[returnsINVALID_FILE_FIELD_NAME. - Two file fields resolving to the same index return
DUPLICATE_FILE_INDEX. - More than one file supplied for one index returns
MULTIPLE_FILES_FOR_INDEX_NOT_ALLOWED. - Where the resolved limit is 0, any file field at all returns
ATTACHMENTS_NOT_ALLOWED_FOR_MESSAGE.
Include the multipart boundary in Content-Type and each field name in Content-Disposition. Malformed multipart bodies return FAILED_TO_PARSE_MULTIPART_FORM_DATA. Unknown fields and non-file values for valid files[n] indices are ignored.
A multipart message body MAY also have content, nonce, tts, flags, favorite_meme_id, and sticker_ids as plain form fields. Each overrides the member of the same name in the parsed payload_json, and sticker_ids collects every value it is given.
The attachments array inside payload_json maps attachment metadata to files by matching each attachment id to n in files[n]. A metadata entry that has a filename but matches no supplied file is rejected with NO_FILE_FOR_ATTACHMENT_METADATA, and two metadata entries claiming the same file index are rejected with DUPLICATE_ATTACHMENT_IDS_NOT_ALLOWED. A supplied file that no metadata entry claims is still attached, taking its index as the attachment ID and its uploaded filename as the attachment filename. A metadata entry that has no filename and matches no supplied file is read as a pre-uploaded attachment reference.
Messages defines the attachment metadata and pre-uploaded attachment form, and Attachment uploads defines the separate relay upload flow.
Input normalisation
Section titled “Input normalisation”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 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.
Those operations document their own JSON validation errors.
Authentication
Section titled “Authentication”Authentication defines the accepted Authorization schemes and links to the OAuth2 scope registry. Each operation states which credentials it accepts. The sudo verification object defines the proof required for sensitive account operations.
An OAuth2 bearer access token is accepted only where a route opts in, and the resource page says so. Everywhere else a bearer credential is refused with 403 ACCESS_DENIED. An account with a suspicious activity flag is refused with 403 ACCOUNT_SUSPICIOUS_ACTIVITY.
Standard request headers
Section titled “Standard request headers”These headers are accepted across resources. An operation-specific header is documented in that operation’s request header table.
| Field | Type | Description |
|---|---|---|
| Authorization? | string | The single credential for an authenticated request, in one of the accepted schemes |
| Content-Type?1 | string | Request body media type and, for multipart bodies, the boundary |
| Accept-Language?2 | string | Selects the locale used for an error message |
| X-Audit-Log-Reason?3 | string | Free-text reason recorded on the resulting audit log entry |
| X-Fluxer-Client-Properties?4 | string | Base64-encoded JSON with the native client’s os, read when an authentication session is created |
| X-Fluxer-Sudo-Mode-JWT?5 | string | A sudo mode proof previously issued to the authenticated user |
| X-Captcha-Token?6 | string | The CAPTCHA solution issued by the selected provider |
| X-Captcha-Type?6 | string | Either hcaptcha or turnstile, selecting the provider that issued the token |
| X-Request-ID?7 | string | A correlation identifier the client chooses, echoed unchanged in the response |
| User-Agent? | string | The originating client description recorded on a new authentication session and on an Admin audit entry |
| Origin?8 | string | The browser origin used for cross-origin negotiation and for the mutating same-host origin check |
1 Use the media type specified in request body formats
2 The configured locale of the authenticated account takes precedence, so this header selects the locale only for an unauthenticated request or an account with no configured locale
3 The value is read verbatim with no percent-decoding, then stripped of form feed and right-to-left override characters and trimmed. A blank or too-long value is treated as absent
4 Read only for a native Fluxer User-Agent, at most 4096 characters, and only the os member is used
5 A valid token replaces the sudo proof fields in the operation body and is echoed in the response header without extending its lifetime.
6 Read only while the instance has a provider configured and the operation is gated. The handshake is defined in CAPTCHA handling
7 A supplied value is echoed back unchanged and unvalidated
8 The exact use is defined in cross-origin requests
A client MUST NOT send an X-Request-ID it is not willing to see in logs, because Fluxer echoes and records the value without validating it.
An X-Audit-Log-Reason normalised to more than 512 characters is discarded, and the request still succeeds. Every request is normalised this way, but only an operation that supports an audit reason records the result.
Standard response headers
Section titled “Standard response headers”| Field | Type | Description |
|---|---|---|
| X-Fluxer-Version?1 | string | The build version of the instance that served the request |
| X-Request-ID?2 | string | The identifier assigned to the request |
| X-Fluxer-Sudo-Mode-JWT?3 | string | The sudo mode token for the caller, present only on an operation that completes a sudo proof |
| Content-Type? | string | The media type of the representation, absent from a response with no body |
| Cache-Control?4 | string | The literal value no-cache unless the operation sets its own directive |
| Access-Control-Allow-Origin?5 | string | The request Origin when it is a configured application origin, and the literal * on routes that set their own wildcard |
| Access-Control-Expose-Headers?5 | string | The literal value X-Fluxer-Version, ETag |
| Vary?5 | string | The literal value Origin, sent whenever the allowed origin was echoed |
| Retry-After?6 | string | Whole seconds to wait, sent on a rate limit denial, a slowmode denial, a resource lock, and the in-flight ceiling 503 |
| X-RateLimit-Limit?7 | string | Present on a route denial and on a successful bot or webhook request |
| X-RateLimit-Remaining?7 | string | Present on a route denial and on a successful bot or webhook request |
| X-RateLimit-Reset?7 | string | Unix timestamp in seconds |
| X-RateLimit-Reset-After?7 | string | Seconds until the bucket resets |
| X-RateLimit-Bucket?7 | string | A stable 16-character hash of the bucket name |
| X-RateLimit-Scope?7 | string | Present on a rate limit denial, taking the value user, shared, or global |
| X-RateLimit-Global?7 | string | Present on a global rate limit denial, taking the literal value true |
1 The value is the literal dev when the build has no version stamp
2 A generated UUID unless the request supplied its own, in which case that value is echoed back unchanged and unvalidated
3 A token newly issued where the caller proved sudo mode again, and otherwise the incoming proof echoed back with no extension of its lifetime
4 Absent from a response with no body
5 Access-Control-Expose-Headers is sent on every response the instance CORS policy handles. Access-Control-Allow-Origin and Vary are sent only for an allowed origin
6 The literal 1 on the in-flight ceiling 503, and otherwise the rate limit header contract
7 The complete contract is defined in Rate limits
An operation that sets its own Cache-Control keeps that value. A response whose Content-Type names a stylesheet, script, font, image, video, or audio representation receives public, max-age=31536000.
Rate limits
Section titled “Rate limits”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 defines the bucket scoping rules, the global allowance, the 429 body, the scope registry, and the complete X-RateLimit-* header contract.
A global denial has Retry-After, X-RateLimit-Scope, and X-RateLimit-Global alone.
Hosted-only routes
Section titled “Hosted-only routes”A small set of routes exists only on the hosted Fluxer deployment. A self-hosted deployment answers one of them with 404 NOT_FOUND. Deployment availability lists every hosted-only route and states how a client resolves the deployment kind before authenticating.
Cross-origin requests
Section titled “Cross-origin requests”The CORS response policy is an allow-list of exactly two origins, the deployment’s configured web application endpoint and its marketing endpoint. A request whose Origin matches one of them receives Access-Control-Allow-Origin set to that origin and Vary: Origin. Every other request, including one that sends no Origin, receives no Access-Control-Allow-Origin from that policy. Credentialed cross-origin requests are not enabled, so Access-Control-Allow-Credentials is never sent.
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.
Get instance discovery on /.well-known/fluxer, Get OpenAPI document on /v1/openapi.json, and 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 it was served.
Error response object
Section titled “Error response object”A JSON error response identifies the stable error code and a human-readable message. An operation can add further top-level members with structured detail. Errors documents the code registries, the HTTP status fallback mapping, and the localisation behaviour.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| code1 | string | Stable machine-readable API error code |
| message2 | string | Human-readable description of this failure |
| errors?3 | array[validation error object] | Request fields that failed validation |
1 Every code is an uppercase symbolic name matching [A-Z][A-Z0-9_]*
2 The wording varies by locale and defaults to the code itself when the failure supplies no message
3 Present on an INVALID_FORM_BODY response, and on any other failure that has field detail
Example
Section titled “Example”{ "code": "MISSING_PERMISSIONS", "message": "Missing permissions"}The body has no request identifier. A client correlates a failure through the X-Request-ID response header.
MISSING_OAUTH_SCOPE has required_scope, and RATE_LIMITED has retry_after and global, where global is true only on a global bucket denial. Each operation that produces one of those members documents it. A client MUST treat any member it does not recognise as absent. Two operations returning the same code can have different extra members, so a client MUST read the members its own operation documents.
Validation error object
Section titled “Validation error object”Each entry identifies one failed input field. A 400 response whose top-level code is INVALID_FORM_BODY has the array in errors. Errors states which failures produce it.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| path1 | string | The location of the request field that failed validation |
| code?2 | string | Stable machine-readable validation code |
| message3 | string | The human-readable description of this validation failure |
1 A nested field is a dot-joined path such as embeds.0.title, and a failure with no field position uses the literal path root
2 Present whenever the failure has an enumerated code from the validation error code registry
3 The localised message registered for the code, falling back to the code itself, and a fixed English string on a failure that has no code
Example
Section titled “Example”{ "code": "INVALID_FORM_BODY", "message": "Invalid form body", "errors": [ {"path": "name", "code": "BASE_TYPE_REQUIRED", "message": "This field is required"} ]}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
Section titled “Resource pages”| Page | Covers |
|---|---|
| Errors | The error envelope, status fallback mapping, and the API and validation code registries |
| Deployment availability | Routes that exist only on the hosted deployment |
| Authentication operations | Registration, login, MFA, WebAuthn, SSO, email verification, recovery, sessions, IP authorisation, desktop handoff |
| Gateway | Bot discovery of the main Gateway endpoint and session start budget |
| Instance | Instance discovery, client geolocation, the limit key registry, the served OpenAPI document |
| OAuth2 | Authorisation, consent, token exchange, introspection, revocation, granted authorisations |
| Applications | Application, bot account, client secret, and bot token management, plus public lookup |
| Connections | External account connection initiation, verification, visibility, and deletion |
| Users | The user object and the shared user enumerations |
| Current user | Current account retrieval, profile mutation, lifecycle, policy acceptance, authorised IP state |
| User settings | Account, notification, and privacy settings, guild folders, voice activity sharing |
| User settings Protobuf | Every structured client preference message and enumeration |
| Email and password changes | The ticketed credential replacement flows |
| Multi-factor authentication | TOTP, backup codes, WebAuthn credentials, sudo verification |
| Phone verification | Outbound and inbound phone verification |
| Relationships | Friend requests, friendships, blocks, relationship nicknames |
| User notes | Private notes attached to user IDs |
| Private channels | Direct message and group DM discovery, creation, preload, pin state |
| User content collections | Recent mentions, saved messages, asynchronous message deletion |
| Gift inventory | Premium gift codes created by the current account |
| Data harvests | Data harvest creation, status, and download |
| Read states | Message watermark and mention count acknowledgements |
| Memes | The saved image, video, and audio collection and batch GIF URL resolution |
| Themes | Shareable custom CSS theme creation |
| Channels | Channel objects, private recipients, permission overwrites, slowmode, RTC regions |
| Calls | Call eligibility, region selection, ringing, and termination |
| Streams | Go Live stream keys, stream regions, preview image lifecycle |
| Entrance sounds | The entrance sound collection, its per-scope selections, and playback |
| Messages | Messages, attachments, embeds, history, acknowledgements, pins, reactions, typing |
| Guilds | Guild objects and lifecycle operations |
| Guild channels | Listing, creation, hierarchy, permission inheritance, bulk positioning |
| Guild members | Member objects, profiles, voice moderation, ownership, removal, role assignment |
| Guild member search | Indexed search filters, sorting, pagination, supplemental join metadata |
| Guild moderation | Bans, temporary bans, ban replacement, and the blocks a ban has |
| Guild emojis | Guild emoji objects, uploads, metadata changes, deletion |
| Guild stickers | Guild sticker objects, uploads, metadata changes, deletion |
| Expressions | Emoji and sticker metadata reads across both |
| Guild audit logs | Audit entries, typed targets, contexts, changes, filters, the audit reason contract |
| Roles and permissions | Permission flags, computation order, role objects and lifecycle |
| Discovery | Public guild listings, categories, the listing application lifecycle, joining without an invite |
| Invites | Invite lookup, creation, acceptance, deletion, code generation |
| Webhooks | Webhook management, message execution, GitHub, Slack, and Instatus callbacks |
| Search | Authenticated global message search |
| Unfurl | Authenticated external URL metadata resolution |
| Billing | Stripe checkout, card preapproval, gift purchase, age verification, refunds, the Stripe webhook |
| Premium | Premium pricing, entitlement state, subscription self-service, billing portal handoff |
| Gifts | Public gift code lookup and authenticated redemption |
| Donations | Donation currencies and intervals, checkout sessions, the donor management link |
| Reports | Authenticated safety reports and email-verified Digital Services Act notices |