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.
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}`
});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. |
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.
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.
Two things to get right:
messagesis an array of arrays. Each group carries surrounding messages for context in Discord's UI. Take the entry withhit: true, falling back to the first entry.total_resultsis an estimate. Fine for "~1,483 matches", not for arithmetic. When merging severalhasbuckets, take the maximum rather than the sum, since buckets overlap.
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.
| 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.
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.
- Discord's shipping client bundle (
https://discord.com/assets/web.*.js) — endpoint table, search tab definitions,queryKeymappings. 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.
{ "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 }