Skip to content

Search

Message search matches indexed messages against a text query and a set of filters. TEBHOS indexes messages in Elasticsearch. Guild member search covers member search, and Discovery covers guild search.

There are two routes. Search messages (POST /search/messages) accepts user sessions and bot tokens and can search one channel, one guild, or every guild and direct message of the caller. Search messages (query string) (GET /messages/search) is an older, user-only TEBHOS form.

Every result is limited to channels the caller can read: in a guild channel the caller needs VIEW_CHANNEL and READ_MESSAGE_HISTORY, and in a private channel the caller must be a recipient. Messages in hidden channels never appear in results.

Search messages

POST /search/messages

Searches indexed messages and returns a global search result object. Accepts user sessions and bot tokens; a bot may only use the current scope.

Every hit is re-read from the message store before it is returned. A hit whose message no longer exists is dropped from the page and removed from the index, so a page can hold fewer entries than hits_per_page.

JSON body

Field Type Description
scope? string Search scope, default current
context_channel_id? snowflake With scope current: the channel to search. For DM scopes: a DM of the caller that is always included
context_guild_id? snowflake With scope current: the guild to search (takes precedence over context_channel_id)
channel_ids? array[snowflake] At most 500. Narrow the search to these channels; each must be readable by the caller (403 MISSING_PERMISSIONS) and, with context_guild_id, belong to that guild
content? string Free-text query (1-1,024 characters), matched against message content and embed text
contents? array[string] At most 100 queries; a message matching any of them is returned (replaces content)
exact_phrases? array[string] At most 10 phrases that must all appear contiguously
hits_per_page? integer 1-25, default 25
page? integer 1-400, default 1. Ignored when cursor is given
cursor? array[string] The cursor of the previous page, to page past page 400
max_id? / min_id? snowflake Only messages with an ID below / above this value
exclude_channel_id? array[snowflake] Channels to leave out
author_id? / exclude_author_id? array[snowflake] Authors to include / exclude (at most 100)
author_type? / exclude_author_type? array[string] user, bot or webhook
mentions? array[snowflake] Users who must all be mentioned
exclude_mentions? array[snowflake] Users who must not be mentioned
mention_everyone? boolean Whether the message mentions @everyone
pinned? boolean Whether the message is pinned
has? / exclude_has? array[string] Content types that must all be present / must be absent
embed_type? / exclude_embed_type? array[string] Embed types: image, video, sound, article
embed_provider? / exclude_embed_provider? array[string] Embed provider names
link_hostname? / exclude_link_hostname? array[string] Hostnames of links in the text (compared in lowercase)
attachment_filename? / exclude_attachment_filename? array[string] Exact attachment file names
attachment_extension? / exclude_attachment_extension? array[string] Attachment extensions, without the dot
sort_by? string timestamp (default) or relevance
sort_order? string desc (default) or asc; applies to timestamp
include_nsfw? boolean Accepted for compatibility. TEBHOS has no age-restricted channels, so it changes nothing

Snowflakes may be sent as strings or numbers. Filters on the same list must all match (mentions, has, embed_type, link_hostname, attachment_filename, attachment_extension), except author_id and author_type, which match any listed value.

Filters added to the index

author_type, mention_everyone, has: snapshot, the embed filters and embed text matching read fields that TEBHOS added to its search index. Messages indexed before these fields existed match them only after a re-index. has: poll matches nothing, because polls are not indexed.

Response

Status Body Condition
200 global search result object Search completed, possibly with no match
400 error response INVALID_FORM_BODY A field is out of range or malformed; errors[].code is CONTEXT_CHANNEL_OR_GUILD_ID_REQUIRED (scope current without a context), BOT_SEARCH_SCOPE_UNAVAILABLE (a bot used another scope) or ALL_CHANNELS_MUST_BELONG_TO_GUILD
403 error response MISSING_PERMISSIONS: a listed channel is not readable. FORBIDDEN: the context channel cannot be viewed
404 error response The context channel does not exist or is not visible (NOT_FOUND), or the caller is not a member of the context guild (UNKNOWN_GUILD)
503 error response FEATURE_TEMPORARILY_DISABLED No search backend is configured for the instance

Rate limit

20 requests per 10 seconds per account, on the search:messages bucket (shared with the query-string route).

Example

curl -X POST https://tebhos.com/api/v1/search/messages \
  -H "Authorization: Bot $TEBHOS_BOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"context_guild_id": "1489002177550843904", "content": "release notes", "has": ["link"], "hits_per_page": 10}'

Global search scopes

Value What is searched
current With context_guild_id: every readable channel of that guild (the caller must be a member). Otherwise the context_channel_id channel; without READ_MESSAGE_HISTORY only messages after the guild's history cutoff are returned, and nothing when there is no cutoff
open_dms Direct and group direct messages the caller has open
all_dms Every direct and group direct message the caller has, including closed ones
all_guilds Every readable channel of every guild the caller is a member of
all all_dms and all_guilds together
open_dms_and_all_guilds open_dms and all_guilds together

Global search result object

Field Type Description
messages array[message object] The matching messages on this page, without referenced_message
channels array[channel object] The channels the returned messages belong to
total integer The number of matching messages, reduced by stale hits dropped on this page. For a channel searched without READ_MESSAGE_HISTORY it is the number of messages returned
hits_per_page integer The page size used
page integer The page that was returned
cursor? array[string] Pass it back as cursor to get the next page; omitted when the page is empty

Global search content types

Value Description
image Has an image attachment
video Has a video attachment
sound Has an audio attachment
file Has at least one attachment
sticker Has at least one sticker
embed Has at least one embed
link Has a link in its text
snapshot Is a forwarded message
poll Has a poll (never matches on TEBHOS)

Search messages (query string)

GET /messages/search

The older TEBHOS search route. User-only: a bot token is rejected with 403 ACCESS_DENIED.

Searches indexed messages and returns a message search result object.

Every hit is re-read from the message store before it is returned. A hit whose message no longer exists is dropped from the page and removed from the index, so a page can hold fewer entries than limit.

Query parameters

Field Type Description
q string The free-text query, matched against message content and the author's display name. Required, but may be empty to match every message
channel_id? snowflake The channel to search. Required unless server_id is given without it (see search scopes)
server_id? snowflake The guild to search
scope? string channel (default) or server; see search scopes
limit? integer Results per page (at most 50, default 25)
page? integer The page number, starting at 1 (default 1)
author_id? snowflake Only messages by this user
exclude_author_id? snowflake Exclude messages by this user
mentions? snowflake Only messages that mention this user
pinned? boolean true returns only pinned messages
has? string Comma-separated content types that must all be present
exclude_has? string Comma-separated content types that must all be absent
link? string Only messages with a link to this exact hostname (compared in lowercase)
filename? string Only messages with an attachment of exactly this filename
ext? string Only messages with an attachment of this extension (without the dot, compared in lowercase)
before? string Only messages created before this local day (YYYY-MM-DD)
after? string Only messages created after this local day (YYYY-MM-DD), that is from the start of the following day
during? string Only messages created on this local day (YYYY-MM-DD)
tz_offset? integer The client's time-zone offset in minutes, as returned by JavaScript getTimezoneOffset() (for example -180 for UTC+3), used to turn the day filters into UTC bounds. Clamped to -840..840, default 0
sort? string Sort mode (default newest)

An invalid date in before, after or during returns 400 BAD_REQUEST.

Response

Status Body Condition
200 message search result object Search completed, possibly with no match
400 error response Neither channel_id nor server_id was supplied, q is missing, or an ID or date is malformed (BAD_REQUEST)
403 error response The caller is a bot (ACCESS_DENIED), is not a member of the guild, or cannot view the channel (FORBIDDEN)
404 error response The channel does not exist or the caller cannot see it (NOT_FOUND)
500 error response No search backend is configured for the instance

Rate limit

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

Example

curl -G "https://tebhos.com/api/v1/messages/search" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "q=release notes" \
  --data-urlencode "channel_id=1489002177550843905" \
  --data-urlencode "has=link,image" \
  --data-urlencode "sort=newest"

Search scopes

Parameters What is searched
channel_id (scope omitted or channel) That single channel. The caller needs VIEW_CHANNEL. Without READ_MESSAGE_HISTORY, only messages after the guild's message-history cutoff are returned, and nothing is returned when the guild has no cutoff
channel_id + scope=server + server_id Every channel of the guild in which the caller has both VIEW_CHANNEL and READ_MESSAGE_HISTORY. The caller must be a member of the guild. All filters apply
server_id only The whole guild, limited to channels the caller can read. Only q and limit apply; the other filters and page are ignored, and the response has only items

Direct message and group direct message channels are searched with channel_id; the caller must be a recipient.

Message search result object

Structure

Field Type Description
items array[message object] The matching messages on this page
total integer The approximate number of matching messages
page integer The page that was returned
total_pages integer ceil(total / limit), or 0 when nothing matched
next_cursor ?string The next page number as a string, or null on the last page

total comes from the search index and is reduced by the number of stale hits dropped on the current page, so it is an estimate. Page through results by incrementing page until next_cursor is null.

The server_id-only form returns {"items": [...]} without the paging fields.

Example

{
  "items": [],
  "total": 0,
  "page": 1,
  "total_pages": 0,
  "next_cursor": null
}

Search content types

Values accepted by has and exclude_has. Unknown values are ignored.

Value Description
image Has an image attachment
video Has a video attachment
sound Has an audio attachment
file Has at least one attachment
link Has a link in its text
embed Has at least one embed
sticker Has at least one sticker

Search sort modes

Value Description
newest Newest first (by creation time, then message ID)
oldest Oldest first
relevance Uses the backend's default ordering when q is not empty. The index always appends a descending message ID sort, so in practice this is also newest first. With an empty q, it falls back to newest