Skip to content

Latest commit

 

History

History
210 lines (163 loc) · 7.27 KB

DISCORD-SEARCH-API.md

File metadata and controls

210 lines (163 loc) · 7.27 KB

Discord message search API

Reference for the endpoint MediaTab is built on. This is an internal client API — not part of Discord's public bot documentation — so everything below was verified against Discord's own shipping client code rather than taken on trust. See RESEARCH.md for how.

Bots cannot use this. It is a user-client endpoint, which is exactly what a Vencord plugin is.


Endpoints

Lifted verbatim from Discord's endpoint table in web.*.js:

SEARCH_GUILD:        e => `/guilds/${e}/messages/search`
SEARCH_CHANNEL:      e => `/channels/${e}/messages/search`
SEARCH_TABS_GUILD:   e => `/guilds/${e}/messages/search/tabs`
SEARCH_TABS_CHANNEL: e => `/channels/${e}/messages/search/tabs`
SEARCH_TABS_DMS:          "/users/@me/messages/search/tabs"
Use Endpoint
A DM or group DM GET /channels/{channel_id}/messages/search
One channel in a server GET /guilds/{guild_id}/messages/search?channel_id={channel_id}
A whole server GET /guilds/{guild_id}/messages/search

MediaTab uses the first three. The /tabs variants power Discord's own tabbed search UI and return per-tab counts; they aren't needed here.

Call these through Vencord's RestAPI from @webpack/common, never with bare fetch. RestAPI supplies the auth token, the correct API version, the super-properties header and Discord's own rate-limit queue.

import { RestAPI } from "@webpack/common";

const { body } = await RestAPI.get({
    url: `/channels/${channelId}/messages/search?${params}`
});

Query parameters

Names confirmed from the queryKey fields in Discord's search-token module.

Parameter Type Notes
content string Free text. Max 1024 characters.
author_id snowflake, repeatable Filter by sender. Up to 1521 values.
author_type string user / bot / webhook, optionally - prefixed to negate.
mentions snowflake, repeatable Messages mentioning these users.
has enum, repeatable link, embed, file, image, video, sound, sticker, poll. See the warning below.
link_hostname string e.g. youtube.com.
attachment_extension string e.g. png. No leading dot.
attachment_filename string
channel_id snowflake, repeatable Guild search only. Up to 500 values.
pinned boolean
include_nsfw boolean Discord sets this from the user's nsfwAllowed flag.
min_id snowflake Results newer than this id.
max_id snowflake Results older than this id.
sort_by timestamp | relevance Default timestamp.
sort_order asc | desc Default desc.
offset integer 0–9975.
limit integer 1–25.

has semantics — the one real trap

Discord's own media tab sends both values:

// from Discord's SearchStore module, tab → filter mapping
{
  1: { has: ["image", "video"] },   // MEDIA
  3: { has: ["file"] },             // FILES
  0: {},                            // MESSAGES
  2: { has: ["link"] },             // LINKS
  4: { pinned: true }               // PINS
}
// default sort
{ sort_by: "timestamp", sort_order: "desc" }

Whether repeated has values are OR-ed or AND-ed is undocumented. The AND reading would make has=image&has=video return only messages containing an image and a video — nearly nothing.

MediaTab does not gamble on it: one request per has value, merged client-side. Costs more requests on the "All" tab, cannot be silently wrong.

Pagination and the 10,000 ceiling

offset maxes out at 9975, so offsets alone reach 10,000 results and stop. Past that, page with max_id (descending) or min_id (ascending) set to the last id you saw. Keyset paging has no ceiling.

min_id and max_id also implement date filters. Convert a timestamp with Discord's own helper:

import { SnowflakeUtils } from "@webpack/common";
const minId = SnowflakeUtils.fromTimestamp(Date.parse("2026-01-01"));

Do not paginate by the length of messages: Discord notes that search may return slightly fewer results than limit for messages that haven't been accessed in a long time. Stop when a page comes back short and treat a zero-length page as the end — MediaTab does both.


Response

{
  "total_results": 1483,        // approximate
  "messages": [                 // array of CONTEXT GROUPS, not messages
    [
      { "id": "…", "hit": false, /* message before */ },
      { "id": "…", "hit": true,  /* the actual result */ },
      { "id": "…", "hit": false, /* message after */ }
    ]
  ],
  "threads": [ /* … */ ],
  "members": [ /* … */ ],
  "analytics_id": "…",
  "documents_indexed": 12345    // present while a guild index is still building
}

Two things to get right:

  1. messages is an array of arrays. Each group carries surrounding messages for context in Discord's UI. Take the entry with hit: true, falling back to the first entry.
  2. total_results is an estimate. Fine for "~1,483 matches", not for arithmetic. When merging several has buckets, take the maximum rather than the sum, since buckets overlap.

Fields MediaTab reads

Attachments:

{
  "filename": "photo.png",
  "size": 204800,
  "url": "https://cdn.discordapp.com/attachments/…?ex=…&is=…&hm=…",
  "proxy_url": "https://media.discordapp.net/attachments/…?ex=…&is=…&hm=…",
  "content_type": "image/png",
  "width": 1920, "height": 1080,
  "spoiler": false
}

Embeds — type is image, video, gifv, article, link or rich, with url, title, provider and any of image / thumbnail / video, each carrying url, proxy_url, width, height.

Stickers — sticker_items[] with id, name and format_type (1 png, 2 apng, 3 lottie, 4 gif). Lottie cannot be shown as an image.


Status codes

Code Meaning What to do
200 Results —
202 Guild index still building Wait body.retry_after seconds, retry
403 No Read Message History Surface to the user; retrying won't help
429 Rate limited Wait body.retry_after seconds, retry

Search is rate limited noticeably more tightly than most endpoints. Keep limit=25 (the maximum) so you make the fewest requests for a given number of results, and prefer narrow filters.


Thumbnails via media.discordapp.net

proxy_url hosts support on-the-fly resizing:

https://media.discordapp.net/attachments/…/photo.png
    ?ex=…&is=…&hm=…          ← MUST be preserved, or you get 403
    &width=280&height=210
    &format=webp             ← or jpeg, useful for a video still frame

Always build these with URL + searchParams.set so the signature parameters survive. Never upscale past the original dimensions — Discord will happily serve a blurry enlargement.


Sources

  • Discord's shipping client bundle (https://discord.com/assets/web.*.js) — endpoint table, search tab definitions, queryKey mappings. Primary source for everything above.
  • Discord Userdoccers — community documentation of internal endpoints.
  • Discord Search API — current state, Jan 2026 — parameter limits and the offset ceiling.