diff --git a/fluxer_docs/src/content/docs/http-api/discovery.mdx b/fluxer_docs/src/content/docs/http-api/discovery.mdx
index 3923c1ba9..f128b549a 100644
--- a/fluxer_docs/src/content/docs/http-api/discovery.mdx
+++ b/fluxer_docs/src/content/docs/http-api/discovery.mdx
@@ -8,14 +8,14 @@ import RouteHeader from '@/components/RouteHeader.astro';
Discovery is the public directory of guilds any account can browse and join. A guild manager applies to have a guild listed, and an operator reviews that application through the [Admin Discovery API](/admin-api/discovery/).
-[Join discovery guild](#join-discovery-guild) is user-only and rejects a bot token with 403 `ACCESS_DENIED`. Every other route accepts a user session token or a bot token. Besides that rejection, a bearer credential is the only credential that produces `ACCESS_DENIED` on any route here. Where a response table below pairs a revoked account with `ACCESS_DENIED`, a session, bot, or bearer token that no longer resolves to an account returns 401 `UNAUTHORIZED` instead.
+[Preview discovery channel](#preview-discovery-channel) and [Join discovery guild](#join-discovery-guild) are user-only and reject a bot token with 403 `ACCESS_DENIED`. Every other route accepts a user session token or a bot token. Besides that rejection, a bearer credential is the only credential that produces `ACCESS_DENIED` on any route here. Where a response table below pairs a revoked account with `ACCESS_DENIED`, a session, bot, or bearer token that no longer resolves to an account returns 401 `UNAUTHORIZED` instead.
-On a guild an operator has marked unavailable, Fluxer refuses [Apply for discovery](#apply-for-discovery), [Edit discovery application](#edit-discovery-application), [Withdraw discovery application](#withdraw-discovery-application), and [Get discovery status](#get-discovery-status) with 403 `MISSING_ACCESS` before the route runs. The gate does not cover [Join discovery guild](#join-discovery-guild).
+On a guild an operator has marked unavailable, Fluxer refuses [Apply for discovery](#apply-for-discovery), [Edit discovery application](#edit-discovery-application), [Withdraw discovery application](#withdraw-discovery-application), and [Get discovery status](#get-discovery-status) with 403 `MISSING_ACCESS` before the route runs. The gate does not cover [Preview discovery channel](#preview-discovery-channel) or [Join discovery guild](#join-discovery-guild).
The routes under `/v1/guilds/{guild_id}/discovery` require [MANAGE_GUILD](/http-api/permissions/), which is an [elevated permission](/http-api/permissions/#elevated-permissions). While the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated, a caller other than the owner also needs an enrolled authenticator, and receives 400 `TWO_FACTOR_REQUIRED` without one. A guild that does not exist returns 404 `UNKNOWN_GUILD`. Both a non-member and a member without the permission return 403 `MISSING_PERMISSIONS`. The routes answer 503 `SERVICE_UNAVAILABLE` when the check cannot be admitted, and 504 `GATEWAY_TIMEOUT` when it does not answer in time.
:::note[Discovery is an optional instance capability]
-Searching, applying, editing, withdrawing, and joining fail with 400 `DISCOVERY_DISABLED` while an operator has it disabled. [List discovery categories](#list-discovery-categories) and [Get discovery status](#get-discovery-status) keep answering.
+Searching, previewing a channel, applying, editing, withdrawing, and joining fail with 400 `DISCOVERY_DISABLED` while an operator has it disabled. [List discovery categories](#list-discovery-categories) and [Get discovery status](#get-discovery-status) keep answering.
:::
## Discovery guild object
@@ -183,6 +183,54 @@ One entry of the fixed category registry, as [List discovery categories](#list-d
| id | integer | The [discovery category](#discovery-categories) value |
| name | string | The display name of the category |
+## Discovery channel preview object
+
+The guild and channel behind a channel or message link, as [Preview discovery channel](#preview-discovery-channel) returns them.
+
+### Structure
+
+| Field | Type | Description |
+| --- | --- | --- |
+| guild | [discovery preview guild](#discovery-preview-guild-object) object | The discoverable guild the channel belongs to |
+| channel | [discovery preview channel](#discovery-preview-channel-object) object | The channel new members can read |
+
+### Example
+
+```json
+{
+ "guild": {
+ "id": "1489002177550843904",
+ "name": "Example guild",
+ "icon": "a_9f1c2d3e4b5a60718293a4b5c6d7e8f9"
+ },
+ "channel": {
+ "id": "1489002177550843907",
+ "name": "general",
+ "type": 0
+ }
+}
+```
+
+## Discovery preview guild object
+
+### Structure
+
+| Field | Type | Description |
+| --- | --- | --- |
+| id | snowflake | The ID of the guild |
+| name | string | The current name of the guild |
+| icon | ?string | The icon hash of the guild, or null when it stores none |
+
+## Discovery preview channel object
+
+### Structure
+
+| Field | Type | Description |
+| --- | --- | --- |
+| id | snowflake | The ID of the channel |
+| name | ?string | The name of the channel, or null when it stores none |
+| type | integer | The [channel type](/http-api/channels/#channel-types) of the channel |
+
## Discovery categories
The release fixes the set, so an operator cannot add, rename, or remove a category.
@@ -324,6 +372,43 @@ A client that wants a translated label supplies its own translation keyed on `id
60 requests per 10 seconds for each authenticated user, on the `discovery:categories` bucket.
+## Preview discovery channel
+
+
+
+Returns the [discovery channel preview object](#discovery-channel-preview-object) for a channel or message link that points into a discoverable guild, so a client can show the guild and channel before the account joins. The caller needs no permission and no membership.
+
+The route answers only when the guild holds an approved discovery application, the guild does not have the invites-disabled feature, the channel belongs to that guild, and the `@everyone` role can read the channel. Reading means `@everyone` holds both [VIEW_CHANNEL](/http-api/permissions/) and [READ_MESSAGE_HISTORY](/http-api/permissions/) after the channel's `@everyone` overwrite is applied. A guild whose `@everyone` role has [ADMINISTRATOR](/http-api/permissions/) passes regardless of overwrites. Role and member overwrites are not considered, and any channel type can pass.
+
+:::note[Every refusal returns the same error]
+A guild that does not exist, a guild without an approved listing, a channel that does not exist, a channel in another guild, and a channel `@everyone` cannot read all return 400 `DISCOVERY_NOT_DISCOVERABLE`. The response does not reveal which condition failed.
+:::
+
+### Path parameters
+
+| Field | Type | Description |
+| --- | --- | --- |
+| guild_id | snowflake | The ID of the discoverable guild |
+| channel_id | snowflake | The ID of the channel to preview |
+
+### Response
+
+| Status | Body | Condition |
+| --- | --- | --- |
+| 200 | [discovery channel preview](#discovery-channel-preview-object) object | Guild is listed in discovery and new members can read the channel |
+| 400 | [error response](/http-api/#error-response) | A path parameter is not a valid snowflake and the request returns `INVALID_FORM_BODY` |
+| 400 | [error response](/http-api/#error-response) | Discovery is disabled for the instance and the request returns `DISCOVERY_DISABLED` |
+| 400 | [error response](/http-api/#error-response) | The guild is not discoverable or `@everyone` cannot read the channel, and the request returns `DISCOVERY_NOT_DISCOVERABLE` |
+| 403 | [error response](/http-api/#error-response) | Caller is a bot, presents a bearer credential, or holds a revoked account and the request returns `ACCESS_DENIED` |
+
+### Side effects
+
+None. The route reads stored records only and emits no Gateway Dispatch.
+
+### Rate limit
+
+60 requests per 10 seconds for each authenticated user, on the `discovery:channel_preview` bucket, which is not partitioned by guild or channel.
+
## Join discovery guild