Skip to content
Fluxer API

Search

Message search matches indexed messages against a text query and a filter set. One request covers a single channel, one guild, the caller’s private channels, or several of those at once. The Guild member search resource defines guild member search, and the Discovery resource defines discoverable guild search.

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.

An instance backs message search with either Elasticsearch or Meilisearch. Term matching and typo tolerance belong to whichever it runs, so an instance can differ in how forgiving a match is. Filter matching is exact in every case.

The body a completed search returns, holding one page of matching messages.

FieldTypeDescription
messages1array[message object]The matching messages on the requested page
channels2array[channel object]The channels the returned messages belong to
total3integerThe number of messages matching the query
hits_per_pageintegerThe page size that was applied, echoed from the request
pageintegerThe page number that was applied, echoed from the request
cursor?4array[string]The opaque cursor the backend produced, which no backend accepts back

1 Each entry is an ordinary message object with the referenced_message field removed, so a reply is returned without its resolved parent even though the reply’s own message_reference is preserved

2 Contains one entry for each distinct channel represented in messages, and never contains a channel with no returned message

3 Counted over the whole resolved scope. A single channel context without READ_MESSAGE_HISTORY reports the number actually returned

4 Present only when the backend produced a cursor. A single channel context, a reconciled offset page, and a Meilisearch instance each return none

Search messages never honours a supplied cursor, so page through a result set with page alone.

The operation drops a hit whose message no longer exists. On an offset page the operation re-walks the result set so that total and the returned page both count only live messages. A request that supplied a cursor skips that reconciliation and only removes the stale hits, so the page can be shorter than hits_per_page while further pages exist.

{
"messages": [],
"channels": [],
"total": 0,
"hits_per_page": 25,
"page": 1
}

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.

FieldTypeDescription
indexingbooleanWhether an index the search needed is not queryable yet, always true on this body

Tell the two bodies apart by the presence of indexing, which the result object never has. The request enqueues the missing indexing work, and a later identical request returns ordinary results once that work completes.

{
"indexing": true
}
ValueDescription
currentThe single guild named by context_guild_id, or the single channel named by context_channel_id when no guild context is supplied
open_dms1Every direct message and group direct message channel the caller currently has open
all_dmsEvery direct message and group direct message channel the caller has ever been a recipient of
all_guildsEvery guild the caller is currently a member of
allEvery guild the caller is currently a member of together with the all_dms channel set
open_dms_and_all_guildsEvery guild the caller is currently a member of together with the open_dms channel set

1 A private channel is open when the caller has not closed it, and a channel named by context_channel_id is added to the open set for this request even when it is closed

A bot uses current. Supplying any other value with a bot token returns 400 INVALID_FORM_BODY on the path scope with the field code BOT_SEARCH_SCOPE_UNAVAILABLE.

ValueDescription
userAuthored by an ordinary user account
botAuthored by a bot account
webhook1Authored by a webhook

1 A webhook message is indexed as webhook even when the executing application is a bot

ValueDescription
imageHas an uploaded attachment whose media type begins with image/
soundHas an uploaded attachment whose media type begins with audio/
videoHas an uploaded attachment whose media type begins with video/
fileHas at least one uploaded attachment of any media type
stickerHas at least one sticker
embed1Has at least one generated preview or supplied rich embed
link2Has an HTTP or HTTPS URL in its own text
poll3Has a poll
snapshot4Is a forward and therefore has at least one message snapshot

1 Derived from the message’s own embed list, so an uploaded attachment never satisfies it

2 Derived from the message text alone, so a message that only has a link inside an embed does not match

3 has never matches on this value, and exclude_has with this value excludes nothing

4 A forward also merges the content, embeds, attachments, stickers, and mentions of its snapshots into its own indexed document, so the other attributes can be satisfied by forwarded content

ValueDescription
image1An image preview embed
video1A video preview embed
sound2A value the filter accepts that matches no document
article1An article or webpage preview embed

1 Compared against the embed’s own type string, which is one of rich, link, article, image, video, audio, gifv, or bluesky

2 An audio preview is indexed as audio. Use the sound content type to find uploaded audio attachments

The four values above are the only ones this filter accepts, so a rich, link, gifv, or bluesky embed cannot be selected or excluded by embed type.

ValueDescription
timestampOrder by creation time, tie-broken by descending message ID
relevance1Order by whatever the configured search backend produces when no explicit sort field is submitted

1 sort_order is not applied to this value

A Meilisearch instance orders relevance by its own ranking rules. An Elasticsearch instance applies its descending message ID tiebreak as the only sort key, so relevance there is equivalent to timestamp with a sort_order of desc.

ValueDescription
ascOldest first
descNewest first
POST/v1/search/messagesBot

Searches indexed messages in the resolved scope. Returns a message search result object on success, or a search indexing object when an index the search needs is not queryable yet.

  • A bot is restricted to the current scope.
  • The caller must be a recipient of every private channel that contributes to the search.
  • A guild scope includes a guild channel only when the caller holds both VIEW_CHANNEL and READ_MESSAGE_HISTORY in it.
  • A single channel context requires guild membership and VIEW_CHANNEL alone. Without READ_MESSAGE_HISTORY the guild’s message history cutoff applies to each returned message.
  • An age-restricted guild or single channel context requires an account old enough for age-restricted content, and otherwise returns 403 NSFW_CONTENT_AGE_RESTRICTED.
  • An instance with no search backend configured returns 403 FEATURE_TEMPORARILY_DISABLED.

Every scope that touches a guild channel calls the main Gateway. A Gateway call that times out returns 504 GATEWAY_TIMEOUT, a reply that cannot be interpreted returns 502 BAD_GATEWAY, and a Gateway that is overloaded or unreachable returns 503 SERVICE_UNAVAILABLE.

Every field is optional, though the default current scope requires a context. An omitted filter is not applied.

FieldTypeDescription
scope?1stringSearch scope to resolve (default current)
context_channel_id?2snowflakeThe ID of the channel that supplies the channel context
context_guild_id?2snowflakeThe ID of the guild that supplies the guild context
channel_ids?3array[snowflake]The channel IDs to narrow the resolved scope to (at most 500)
channel_id?4array[snowflake]An alias of channel_ids (at most 500)
hits_per_page?integerThe number of messages on each page (1-25, default 25)
page?5integerThe page number (1-9007199254740991, default 1)
cursor?6array[string]The opaque cursor returned by a previous page, accepted and then discarded
min_id?7snowflakeThe lowest message ID to match, applied exclusively
max_id?7snowflakeThe highest message ID to match, applied exclusively
content?8stringThe free-text query (1-1024 characters)
contents?9array[string]The alternative free-text queries (at most 100, 1-1024 characters each)
exact_phrases?10array[string]The phrases that must all match (at most 10, 1-1024 characters each)
exclude_channel_id?array[snowflake]The channel IDs, any of which excludes a message (at most 500)
author_id?11array[snowflake]The author user IDs, any of which can match (at most 100)
exclude_author_id?array[snowflake]The author user IDs, any of which excludes a message (at most 100)
author_type?11array[string]Author types, any of which can match (at most 20)
exclude_author_type?array[string]Author types, any of which excludes a message (at most 20)
mentions?12array[snowflake]The user IDs, all of which must be mentioned (at most 100)
exclude_mentions?array[snowflake]The user IDs, any of which excludes a message (at most 100)
mention_everyone?booleanWhether a matching message has an everyone mention
pinned?booleanWhether a matching message is pinned
has?12array[string]Content types, all of which must be present (at most 20)
exclude_has?array[string]Content types, none of which can be present (at most 20)
embed_type?12array[string]Embed types, all of which must be present (at most 20)
exclude_embed_type?array[string]Embed types, any of which excludes a message (at most 20)
embed_provider?12 13array[string]The embed provider names, all of which must be present (at most 50, 1-256 characters each)
exclude_embed_provider?13array[string]The embed provider names, any of which excludes a message (at most 50, 1-256 characters each)
link_hostname?12 13array[string]The hostnames, all of which must be present (at most 100, 1-255 characters each)
exclude_link_hostname?13array[string]The hostnames, any of which excludes a message (at most 100, 1-255 characters each)
attachment_filename?12 13array[string]The filenames, all of which must be present (at most 100, 1-1024 characters each)
exclude_attachment_filename?13array[string]The filenames, any of which excludes a message (at most 100, 1-1024 characters each)
attachment_extension?12 14array[string]The extensions, all of which must be present (at most 50, 1-32 characters each)
exclude_attachment_extension?14array[string]The extensions, any of which excludes a message (at most 50, 1-32 characters each)
sort_by?stringSearch sort field (default timestamp)
sort_order?stringSearch sort order (default desc)
include_nsfw?15booleanWhether age-restricted channels can contribute results (default false)

1 A bot that supplies any value other than current is rejected with 400 INVALID_FORM_BODY on the path scope

2 Consulted only by the current scope, where a guild context wins over a channel context, and by the private channel scopes, where it adds one closed channel back

3 Every listed channel must already be in the resolved scope. A current channel context ignores it entirely

4 Read only when channel_ids is absent, and then it narrows the resolved scope exactly as channel_ids does

5 Only page reaches a page beyond the first

6 The route never honours it. Supplying one serves the first page again while the response still echoes the requested page

7 Both bounds are exclusive, so min_id matches messages strictly newer than it and max_id matches messages strictly older than it

8 Matched against the message text and the collected embed text. The value is trimmed and stripped of control and formatting characters before its length is checked

9 Supply the text query in exactly one of content and contents

10 Every phrase must match, and a phrase matches only when its terms appear adjacently and in order in the message text or the collected embed text

11 A message matches when it satisfies any one of the listed values

12 A message matches only when it satisfies every listed value, so supplying two values requires both

13 Compared exactly and case-sensitively, so a partial value never matches. A link hostname uses the URL host of a link in the message text or of an embed URL

14 Indexed in lowercase, without a leading dot, and only for the final dot-separated segment of a filename when that segment is 1 through 10 characters

15 Honoured only for an account whose recorded date of birth places it at 18 years or older, and otherwise treated as false. A bot always passes

A current guild context rejects a requested channel outside that guild with 400 and the field code ALL_CHANNELS_MUST_BELONG_TO_GUILD. It rejects a channel inside the guild that the caller cannot read with 403 MISSING_PERMISSIONS. Every other scope rejects a requested channel its resolved set does not already contain with 403 MISSING_PERMISSIONS. Use exclude_channel_id to remove a channel.

Fluxer compares attachment_extension without lowercasing it, so an uppercase value never matches. include_nsfw gates which channels a resolved scope keeps, and a single channel context ignores it.

The two backends read min_id and max_id differently. An Elasticsearch instance compares the message ID, while a Meilisearch instance compares the whole creation second the bound encodes, so a message created in the same second as the bound is excluded there. An Elasticsearch instance requires at least one contents entry to match and replaces content with that group. A Meilisearch instance joins the group, content, and the quoted phrases into one query string, and requires no entry in particular. Supplying exact_phrases without contents narrows content to the message text alone on Elasticsearch.

The current scope requires one of context_guild_id and context_channel_id. Supplying neither returns 400 with the field code CONTEXT_CHANNEL_OR_GUILD_ID_REQUIRED on the path context.

Fluxer resolves a supplied context_guild_id before anything else. A guild that cannot be resolved returns 404 UNKNOWN_GUILD, and a guild the caller is not a member of returns 403 MISSING_PERMISSIONS. A guild context searches that guild, restricted to channel_ids when supplied and to every channel of the guild otherwise, and every channel in the resulting set must belong to that guild. A channel the caller cannot both view and read history in is dropped when the set came from the guild, and rejected with 403 MISSING_PERMISSIONS when the caller named it in channel_ids. An age-gated channel that include_nsfw does not admit is removed from the set before the permission check runs, so it never causes that refusal.

A guild whose NSFW level is age restricted refuses a caller who is not old enough with 403 NSFW_CONTENT_AGE_RESTRICTED. An old-enough caller who did not set include_nsfw receives an empty result from such a guild. The request still succeeds.

A channel context searches only that channel and ignores channel_ids, cursor, and include_nsfw. When the caller cannot read the channel’s history and the guild defines no message history cutoff, the search returns an empty result.

The all_guilds, all, and open_dms_and_all_guilds scopes include only guild channels the caller can currently view and read history in. The open_dms and open_dms_and_all_guilds scopes include the caller’s open private channels. The all_dms and all scopes include every private channel available to the caller unless channel_ids narrows the set. A scope that resolves to no channel returns an empty result with a total of 0.

Searchable text is the message content plus the collected embed text, which covers embed titles, descriptions, URLs, author names, provider names, footer text, and field names and values, including nested embeds.

Content moderation scans every string in the body whose field name does not end in _id or _ids, so a value of at least three characters that matches the instance phrase blocklist or has a blocked URL returns 403 CONTENT_BLOCKED.

StatusBodyCondition
200message search result object | search indexing objectSearch completed, or an index it needed is not queryable yet
400error responseA bot supplied a scope other than current, the current scope has no context, or a requested channel does not belong to the context guild, each returning INVALID_FORM_BODY
403error responseCaller presents an OAuth2 bearer credential, or the guild of the context channel cannot be resolved while a stored guild record still exists, each returning ACCESS_DENIED
403error responseThe account has an outstanding required action and the request returns ACCOUNT_SUSPICIOUS_ACTIVITY
403error responseA body string is blocked by content moderation and the request returns CONTENT_BLOCKED
403error responseNo search backend is configured for the instance and the request returns FEATURE_TEMPORARILY_DISABLED
403error responseA requested channel is outside the resolved scope, the caller is not a member of the context guild, or the caller cannot view the context channel, each returning MISSING_PERMISSIONS
403error responseThe context guild or the context channel is age restricted and the caller is not old enough and the request returns NSFW_CONTENT_AGE_RESTRICTED
404error responseContext guild cannot be resolved, or the guild of the context channel cannot be resolved and no stored guild record exists, each returning UNKNOWN_GUILD
404error responseThe context channel does not exist or is a private channel the caller is not a recipient of and the request returns UNKNOWN_CHANNEL
404error responseThe caller’s own account record is missing and the request returns UNKNOWN_USER

When a channel in the resolved scope has never been indexed or its index is stale, the operation enqueues an indexing job for it and returns the search indexing object.

20 requests per 10 seconds for each authenticated user, on the search:messages bucket.