feat(threads): thread and forum API, admin, schemas and docs (#3251)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-07 15:25:12 +02:00
1 parent 5f4295e399
commit 3f4160b138
495 files changed
+39232 -1215

No files matched your search

@@ -0,0 +1,104 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Admin threads
description: The channel_threads rollout configuration, guild thread lookup, and thread deletion.
draft: true
---
import RouteHeader from '@/components/RouteHeader.astro';
The Admin API reads and deletes [threads](/http-api/threads/) whether or not the `channel_threads` experiment is active for their guild. The rollout itself is part of the [instance configuration](/admin-api/instance/#instance-configuration-object) under `channel_threads`.
## Channel threads configuration object
The instance rollout of threads, forum channels, and media channels. A guild is enabled by the guild fields, and a user within it by the user fields.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the rollout runs at all (default false) |
| config_version | integer | Revision counter, raised by Fluxer and never accepted from a request |
| ever_enabled<sup>1</sup> | boolean | Whether `enabled` was ever true |
| guild_basis_points | integer | Share of guilds the rollout selects, in basis points (0-10000, default 0) |
| guild_salt | string | Salt of the guild sampling hash (1-64 printable ASCII characters, default `channel-threads-guild-v1`) |
| enabled_guild_ids | array[snowflake] | Guilds the rollout always selects, up to 1000 entries (default empty) |
| disabled_guild_ids | array[snowflake] | Guilds the rollout never selects, up to 1000 entries (default empty) |
| user_basis_points | integer | Share of accounts the rollout selects, in basis points (0-10000, default 0) |
| user_salt | string | Salt of the account sampling hash (1-64 printable ASCII characters, default `channel-threads-user-v1`) |
| included_user_ids | array[snowflake] | Accounts the rollout always selects, up to 1000 entries (default empty) |
| excluded_user_ids<sup>2</sup> | array[snowflake] | Accounts and bots the rollout never selects, up to 1000 entries (default empty) |
<sup>1</sup> Set by Fluxer on the first write with `enabled` true, never cleared, and never accepted from a request
<sup>2</sup> An excluded moderation bot sees no thread and cannot act on one
Every field is present on read. An absent document or missing field uses the defaults above.
`disabled_guild_ids` wins over `enabled_guild_ids`, and `enabled_guild_ids` wins over `guild_basis_points`. The user fields follow the same order. A bot follows the guild fields and `excluded_user_ids` alone. A user also needs a client that declares `channel_threads`.
A user outside the rollout in an enabled guild sees no thread, forum, or media channel while other members do. Set `user_basis_points` to 10000 before enabling a guild outside staff.
:::caution[Disabling keeps the stored threads]
Setting `enabled` to false hides every thread, forum, and media channel and stops automatic archiving. Re-enabling shows the stored data again.
:::
## List guild threads
<RouteHeader method="GET" path="/v1/admin/guilds/{guild_id}/threads" auditReason />
Returns every thread of a guild, active and archived, newest first. Requires `guild:lookup`.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| guild_id | snowflake | The ID of the guild |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| threads | array[[thread](/http-api/threads/#thread-object) object] | The threads of the guild |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | Threads were listed |
### Side effects
One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded with the action `list_guild_threads`, the target type `guild`, and the guild ID as the target. Its metadata key `result_count` is the number of threads returned.
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.
## Delete thread
<RouteHeader method="DELETE" path="/v1/admin/channels/{channel_id}" auditReason />
Deletes a thread with its messages and memberships. Requires `message:delete_all`. Returns 204 with no body. Emits a [Thread Delete](/gateway/events/#thread-delete) Gateway event.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| channel_id | snowflake | The ID of the thread |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | None | Thread was deleted |
| 400 | [error response](/http-api/#error-response) | Channel is not a thread and the request returns `INVALID_CHANNEL_TYPE` |
| 404 | [error response](/http-api/#error-response) | Channel does not exist and the request returns `UNKNOWN_CHANNEL` |
### Side effects
One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded with the action `delete_thread`, the target type `channel`, and the thread ID as the target. Its metadata is `guild_id`, `parent_id`, and `type`. The guild audit log records nothing.
### Rate limit
100 requests per minute for each authenticated user, on the `admin:message:operation` bucket.
@@ -0,0 +1,170 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Gateway threads
description: Thread Dispatch events, thread subscriptions, forum unread counts, and the thread session flag.
draft: true
---
A session receives [threads](/http-api/threads/) through the Dispatch events below. A guild ready object in [Ready](/gateway/events/#ready), [Guild Create](/gateway/events/#guild-create), and [Guild Sync](/gateway/events/#guild-sync) has `threads` only when the session can view threads in that guild. A client treats the presence of that key as the signal that threads, forums, and media channels exist in the guild.
:::note[Threads need the `channel_threads` experiment]
A user session receives thread data only when it sets `CHANNEL_THREADS` in Identify and the experiment is active for the guild and the account. A bot session needs no flag.
:::
## Session flag
| Value | Name | Description |
| --- | --- | --- |
| 1 &lt;&lt; 2 | CHANNEL_THREADS | The client can parse thread, forum, and media channels |
The flag joins the [session flags](/gateway/commands/#session-flags) of Identify. A user session without it sees no channel of type 11, 12, 15, or 16, and no thread event.
## Guild threads
`threads` on a guild ready object is an array of [thread objects](/http-api/threads/#thread-object). A user session receives the active threads it joined, each with `member`. A bot session receives every active thread it can view.
The `member` of each entry has no `id`, `user_id`, or `member`.
## Dispatch events
| Event | Description | Scope |
| --- | --- | --- |
| [Thread Create](#thread-create) | A thread was created or became visible | Thread |
| [Thread Update](#thread-update) | A thread changed | Thread |
| [Thread Delete](#thread-delete) | A thread was deleted | Thread |
| [Thread List Sync](#thread-list-sync) | The session's active thread set was replaced | Guild |
| [Thread Member Update](#thread-member-update) | The session's own membership changed | Current user |
| [Thread Members Update](#thread-members-update) | Users joined or left a thread | Thread |
| [Thread Member List Update](#thread-member-list-update) | A subscribed thread member list resynced | Command response |
| [Forum Unreads](#forum-unreads) | Unread counts for forum posts | Command response |
A session receives a thread event only when it can view the thread. Messages, reactions, typing, and pins in a thread reach a session that is a member of the thread, a bot session, or an active session that subscribed through `threads`. A message in a thread has `channel_type` set to the thread type.
### <span id="thread-create"></span>THREAD_CREATE
A thread was created, or the session was added to one. The payload is the [thread object](/http-api/threads/#thread-object) with the fields below.
| Field | Type | Description |
| --- | --- | --- |
| newly_created? | boolean | Whether the thread was just created, present only then |
| member? | [thread member](/http-api/thread-members/#thread-member-object) object | The session's own membership, present only for a member |
A user session receives it for a thread it joined, a thread it was added to, or any thread while subscribed through `threads`. A passive session receives it only when it was added.
### <span id="thread-update"></span>THREAD_UPDATE
A thread changed. The payload is the [thread object](/http-api/threads/#thread-object).
A new message and a changed counter emit no Thread Update. A passive session receives it only for a thread it joined.
### <span id="thread-delete"></span>THREAD_DELETE
A thread was deleted.
| Field | Type | Description |
| --- | --- | --- |
| id | snowflake | The ID of the thread |
| guild_id | snowflake | The ID of the guild |
| parent_id | snowflake | The ID of the parent channel |
| type | integer | The [thread type](/http-api/threads/#thread-types) |
Recipients are the sessions that could view the thread before the deletion.
### <span id="thread-list-sync"></span>THREAD_LIST_SYNC
The session's active threads were replaced for a guild or for some of its channels.
| Field | Type | Description |
| --- | --- | --- |
| guild_id | snowflake | The ID of the guild |
| channel_ids?<sup>1</sup> | array[snowflake] | The parent channels whose threads are replaced |
| threads | array[[thread](/http-api/threads/#thread-object) object] | The active threads the session can view |
| members | array[[thread member](/http-api/thread-members/#thread-member-object) object] | The session's memberships in those threads |
<sup>1</sup> Absent when the event replaces every thread in the guild
Fluxer sends it after `threads` is set to true in [Lazy Request](#lazy-request-thread-options), within 5,000 ms, and when the session gains access to a channel that has threads.
### <span id="thread-member-update"></span>THREAD_MEMBER_UPDATE
The session's own membership in a thread changed. The payload is the [thread member object](/http-api/thread-members/#thread-member-object) with `guild_id`.
### <span id="thread-members-update"></span>THREAD_MEMBERS_UPDATE
Users joined or left a thread.
| Field | Type | Description |
| --- | --- | --- |
| id | snowflake | The ID of the thread |
| guild_id | snowflake | The ID of the guild |
| member_count | integer | The number of thread members, capped at 50 |
| added_members?<sup>1</sup> | array[[thread member](/http-api/thread-members/#thread-member-object) object] | The users who joined |
| removed_member_ids? | array[snowflake] | The IDs of the users who left |
<sup>1</sup> Each entry adds `member`, the guild member, and `presence`, the [presence object](/gateway/events/#presence-object). Either is null when Fluxer has none
A bot session receives every Thread Members Update it can view. A user session receives it when it was added or removed, is a member, or subscribed through `threads`. A passive session receives it only when it was added or removed.
### <span id="thread-member-list-update"></span>THREAD_MEMBER_LIST_UPDATE
A thread member list the session subscribed to through `thread_member_lists` resynced.
| Field | Type | Description |
| --- | --- | --- |
| guild_id | snowflake | The ID of the guild |
| thread_id | snowflake | The ID of the thread |
| members<sup>1</sup> | array[thread member list item object] | The members of the thread, at most 1,000 |
<sup>1</sup> Each item is `user_id`, `join_timestamp`, `flags`, `member`, and `presence`, where `member` is the guild member or null
The event is delivered live and never retained for Resume.
### <span id="forum-unreads"></span>FORUM_UNREADS
The answer to [Request Forum Unreads](#request-forum-unreads).
| Field | Type | Description |
| --- | --- | --- |
| guild_id | snowflake | The ID of the guild |
| channel_id | snowflake | The ID of the forum or media channel |
| threads | array[forum unread object] | One entry for each requested post the session can view |
Each forum unread object is `thread_id` with either `count` or `missing`. `count` is the number of unread messages after the sent `ack_message_id`, capped at 25. `missing` is true when the request sent no `ack_message_id` for that post.
## <span id="lazy-request-thread-options"></span>Lazy Request thread options
The [guild subscription object](/gateway/commands/#guild-subscription-object) of [Lazy Request](/gateway/commands/#lazy-request) takes two more options.
| Field | Type | Description |
| --- | --- | --- |
| threads? | boolean | Whether the session receives every thread event and thread message in the guild |
| thread_member_lists? | array[snowflake] | The threads whose member lists the session subscribes to, at most 10 |
Fluxer ignores both options for a user session that did not set `CHANNEL_THREADS`. A thread the session cannot view is dropped from `thread_member_lists`. Each newly listed thread produces a [Thread Member List Update](#thread-member-list-update), and a membership change in it produces another.
## Request Forum Unreads
Opcode `28` requests unread counts for posts in one forum or media channel.
| Field | Type | Description |
| --- | --- | --- |
| guild_id | snowflake | The ID of the guild |
| channel_id | snowflake | The ID of the forum or media channel |
| threads | array[forum unread request object] | The posts to count, deduplicated and truncated to 40 |
Each forum unread request object is `thread_id` and an optional `ack_message_id`, the newest message the client has read in that post.
```json
{
"op": 28,
"d": {
"guild_id": "1189375284394692608",
"channel_id": "1189375284394692700",
"threads": [{"thread_id": "1189375284394692800", "ack_message_id": "1189375284394692900"}]
}
}
```
Results arrive in one [Forum Unreads](#forum-unreads), where a post outside the channel has no entry. A channel the session cannot view, and a request with no valid `thread_id`, produce no Dispatch. A session that cannot view threads closes with `4001` and reason `Unknown opcode`.
A session sends at most five Request Forum Unreads commands in a rolling 5-second window. A further command is discarded without closing the connection.
@@ -0,0 +1,280 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Forums
description: Forum and media channels, forum tags, post data, and webhooks that post into a forum.
draft: true
---
import RouteHeader from '@/components/RouteHeader.astro';
A forum channel holds posts, and every post is a public [thread](/http-api/threads/) with its own first message. A media channel is a forum channel whose posts center on attachments. Neither accepts a message of its own.
:::note[Every route needs the `channel_threads` experiment]
A route on this page answers 404 `NOT_FOUND` unless the experiment is active for the guild and the caller. A user session also declares `channel_threads` in the `X-Fluxer-Features` header, and a bot needs no declaration.
:::
## Forum channel fields
A forum or media channel is a [channel object](/http-api/channels/#channel-object) of type `15` or `16` with the fields below.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| topic | ?string | The guidelines of the channel (1-4,096 characters) |
| last_message_id<sup>1</sup> | ?snowflake | The ID of the newest post |
| flags | integer | The [channel flags](#channel-flags) of the channel |
| available_tags | array[[forum tag](#forum-tag-object) object] | The tags a post can apply (max 20) |
| default_reaction_emoji | ?[default reaction](#default-reaction-object) object | The reaction a client shows on each post, or null when none is set |
| default_sort_order | ?integer | The [sort order](#forum-sort-orders) a client uses, or null when none is set |
| default_forum_layout?<sup>2</sup> | integer | The [layout](#forum-layouts) a client uses |
| default_tag_setting | string | The [tag matching](#forum-tag-settings) search uses by default |
| default_auto_archive_duration | ?integer | The [auto archive duration](/http-api/threads/#auto-archive-durations) new posts inherit, or null when none is set |
| default_thread_rate_limit_per_user | integer | The slowmode interval in seconds new posts inherit |
<sup>1</sup> [Channel Update](/gateway/events/#channel-update) is not emitted when this changes. A client advances it on [Thread Create](/gateway/events/#thread-create)
<sup>2</sup> Present only on a forum channel
`rate_limit_per_user` on the channel limits how often one member creates a post. Sending [Create message](/http-api/messages/#create-message) to the channel itself returns 400 `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL`, and so do reactions and pins.
### Example
```json
{
"id": "1501314428688998300",
"type": 15,
"guild_id": "1501314428688990000",
"name": "help",
"topic": "One question per post",
"position": 4,
"parent_id": null,
"permission_overwrites": [],
"last_message_id": "1501320000000000000",
"rate_limit_per_user": 0,
"flags": 16,
"available_tags": [
{"id": "1501314428688998301", "name": "Solved", "moderated": true, "emoji_id": null, "emoji_name": "✅"}
],
"default_reaction_emoji": null,
"default_sort_order": null,
"default_forum_layout": 0,
"default_tag_setting": "match_some",
"default_auto_archive_duration": null,
"default_thread_rate_limit_per_user": 0
}
```
## Forum post object
[Start thread](/http-api/threads/#start-thread) returns a forum post, which is a [thread object](/http-api/threads/#thread-object) with its first message.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| message | [message](/http-api/messages/#message-object) object | The first message of the post, which has the ID of the post |
## Channel types
| Value | Name | Description |
| --- | --- | --- |
| 15 | GUILD_FORUM | A guild channel whose messages live in posts |
| 16 | GUILD_MEDIA | A forum channel whose posts center on attachments |
## Channel flags
| Value | Name | Description |
| --- | --- | --- |
| 1 &lt;&lt; 1 | PINNED | The post is pinned to the top of its forum |
| 1 &lt;&lt; 4 | REQUIRE_TAG | A new post must apply at least one tag |
| 1 &lt;&lt; 15 | HIDE_MEDIA_DOWNLOAD_OPTIONS | Clients hide download, open, and copy actions on images and videos in a media channel |
`PINNED` is set on a post, and a forum holds at most one pinned post. `REQUIRE_TAG` and `HIDE_MEDIA_DOWNLOAD_OPTIONS` are set on the channel. `HIDE_MEDIA_DOWNLOAD_OPTIONS` on a forum channel returns 400 `HIDE_MEDIA_DOWNLOAD_OPTION_MEDIA_ONLY`, and any other bit returns 400 `INVALID_FORM_BODY`.
## Forum tag object
### Structure
| Field | Type | Description |
| --- | --- | --- |
| id | snowflake | The ID of the tag |
| name | string | The name of the tag (1-50 characters) |
| moderated | boolean | Whether only thread moderators can apply or remove the tag |
| emoji_id | ?snowflake | The ID of a custom emoji of this guild, or null |
| emoji_name | ?string | A single Unicode emoji, or null |
At most one of `emoji_id` and `emoji_name` is set. A tag name is unique within its channel, and the comparison is case-sensitive.
## Default reaction object
### Structure
| Field | Type | Description |
| --- | --- | --- |
| emoji_id | ?snowflake | The ID of a custom emoji of this guild, or null |
| emoji_name | ?string | A single Unicode emoji, or null |
## Forum sort orders
| Value | Name | Description |
| --- | --- | --- |
| 0 | LATEST_ACTIVITY | Sort posts by their newest message |
| 1 | CREATION_TIME | Sort posts by creation time |
## Forum layouts
| Value | Name | Description |
| --- | --- | --- |
| 0 | DEFAULT | No layout is set |
| 1 | LIST | Show posts as a list |
| 2 | GRID | Show posts as a grid of tiles |
## Forum tag settings
| Value | Description |
| --- | --- |
| match_some | A post matches when it applies any of the searched tags |
| match_all | A post matches when it applies every searched tag |
## Creating and modifying a forum
[Create guild channel](/http-api/guild-channels/#create-guild-channel) with `type` `15` or `16`, and [Modify channel](/http-api/channels/#modify-channel) on a forum or media channel, accept these fields beside the common guild channel fields.
| Field | Type | Description |
| --- | --- | --- |
| topic? | ?string | The guidelines of the channel (1-4,096 characters) |
| available_tags?<sup>1</sup> | array[[forum tag](#forum-tag-object) object] | The complete tag set (max 20) |
| default_reaction_emoji? | ?[default reaction](#default-reaction-object) object | The default reaction, or null to clear it |
| default_sort_order? | ?integer | The [sort order](#forum-sort-orders), or null to clear it |
| default_forum_layout?<sup>2</sup> | integer | The [layout](#forum-layouts) |
| default_tag_setting? | ?string | The [tag matching](#forum-tag-settings) default |
| default_auto_archive_duration? | ?integer | The [auto archive duration](/http-api/threads/#auto-archive-durations) new posts inherit, or null to clear it |
| default_thread_rate_limit_per_user? | integer | The slowmode interval in seconds new posts inherit (0-21,600) |
| flags? | integer | The [channel flags](#channel-flags) of the channel |
<sup>1</sup> An entry with `id` keeps that tag, and an entry without one creates a tag. A stored tag missing from the array is deleted
<sup>2</sup> Read only for a forum channel
A guild text channel accepts `default_auto_archive_duration` and `default_thread_rate_limit_per_user` as well.
A tag set over 20 returns 400 `MAX_FORUM_TAGS`, and a repeated name returns 400 `FORUM_TAG_NAMES_MUST_BE_UNIQUE`. An `id` the channel does not hold returns 404 `UNKNOWN_FORUM_TAG`. An `emoji_id` from another guild returns 404 `UNKNOWN_EMOJI`.
`REQUIRE_TAG` on a channel whose every tag is moderated returns 400 `NO_TAGS_AVAILABLE_TO_NON_MODERATORS`.
An invite to a forum or media channel returns 400 `INVALID_CHANNEL_TYPE`.
## Create forum tag
<RouteHeader method="POST" path="/v1/channels/{channel_id}/tags" bot auditReason />
Adds a tag to a forum or media channel and returns the updated [channel object](/http-api/channels/#channel-object). Requires `MANAGE_CHANNELS`. Emits a [Channel Update](/gateway/events/#channel-update) Gateway event.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| channel_id | snowflake | The ID of the forum or media channel |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| name | string | The name of the tag (1-50 characters) |
| moderated? | boolean | Whether only thread moderators can apply or remove the tag (default false) |
| emoji_id? | ?snowflake | The ID of a custom emoji of this guild |
| emoji_name? | ?string | A single Unicode emoji (max 64 characters) |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [channel](/http-api/channels/#channel-object) object | Tag was added |
| 400 | [error response](/http-api/#error-response) | Channel is not a forum or media channel and the request returns `INVALID_CHANNEL_TYPE` |
| 400 | [error response](/http-api/#error-response) | Channel has 20 tags and the request returns `MAX_FORUM_TAGS` |
| 400 | [error response](/http-api/#error-response) | Channel has a tag with the name and the request returns `FORUM_TAG_NAMES_MUST_BE_UNIQUE` |
| 403 | [error response](/http-api/#error-response) | Caller lacks `MANAGE_CHANNELS` and the request returns `MISSING_PERMISSIONS` |
### Rate limit
10 requests per 10 seconds for each authenticated user and channel ID, on the `channel:forum_tags::channel_id` bucket.
## Modify forum tag
<RouteHeader method="PUT" path="/v1/channels/{channel_id}/tags/{tag_id}" bot auditReason />
Replaces a tag of a forum or media channel and returns the updated [channel object](/http-api/channels/#channel-object). Requires `MANAGE_CHANNELS`. Emits a [Channel Update](/gateway/events/#channel-update) Gateway event.
The body matches [Create forum tag](#create-forum-tag). A `tag_id` the channel does not hold returns 404 `UNKNOWN_FORUM_TAG`.
### Rate limit
10 requests per 10 seconds for each authenticated user and channel ID, on the `channel:forum_tags::channel_id` bucket.
## Delete forum tag
<RouteHeader method="DELETE" path="/v1/channels/{channel_id}/tags/{tag_id}" bot auditReason />
Removes a tag from a forum or media channel and returns the updated [channel object](/http-api/channels/#channel-object). Requires `MANAGE_CHANNELS`. Emits a [Channel Update](/gateway/events/#channel-update) Gateway event.
A post that applied the tag keeps the ID until its tags next change, and a client ignores an ID absent from `available_tags`.
### Rate limit
10 requests per 10 seconds for each authenticated user and channel ID, on the `channel:forum_tags::channel_id` bucket.
## Get forum post data
<RouteHeader method="POST" path="/v1/channels/{channel_id}/post-data" bot />
Returns the owner and first message of each requested post in a forum or media channel. Requires `READ_MESSAGE_HISTORY`.
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| thread_ids | array[snowflake] | The IDs of the posts (1-100) |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| threads<sup>1</sup> | map[snowflake, [post data](#post-data-object) object] | The post data keyed by post ID |
<sup>1</sup> An ID that is not a post of this channel is left out
#### Post data object
| Field | Type | Description |
| --- | --- | --- |
| owner | ?[guild member](/http-api/guild-members/#guild-member-object) object | The guild member who created the post, or null when they left the guild |
| first_message | ?[message](/http-api/messages/#message-object) object | The first message of the post, or null when it was deleted |
### Rate limit
20 requests per 10 seconds for each authenticated user and channel ID, on the `channel:post_data::channel_id` bucket.
## Webhooks in forums
A webhook whose channel is a forum or media channel creates a post, or posts into an existing one. [Execute webhook](/http-api/webhooks/#execute-webhook) takes the fields below.
| Field | Type | Description |
| --- | --- | --- |
| thread_id?<sup>1</sup> | snowflake | The query parameter naming the thread to post into |
| thread_name? | string | The name of a new post (1-100 characters) |
| applied_tags?<sup>2</sup> | array[snowflake] | The IDs of the tags a new post applies (max 5) |
<sup>1</sup> The thread must belong to the channel of the webhook. Any other thread returns 404 `UNKNOWN_CHANNEL`
<sup>2</sup> A moderated tag returns 403 `MISSING_PERMISSIONS`
`thread_id` also works on a webhook whose channel is a guild text channel, and on the webhook message routes. A forum execution without `thread_id` or `thread_name` returns 400 `WEBHOOK_FORUM_TARGET_REQUIRED`, and one with both returns 400 `WEBHOOK_FORUM_TARGET_CONFLICT`.
`thread_name` or `applied_tags` on any other channel returns 400 `WEBHOOK_THREAD_NAME_REQUIRES_FORUM`. The Slack and GitHub formats need `thread_id` on a forum, and 400 `WEBHOOK_SERVICE_FORUM_UNSUPPORTED` answers without one.
A locked thread refuses a webhook message with 400 `THREAD_LOCKED`. An archived thread that is not locked is unarchived by it.
## New post notifications
A [channel notification override](/http-api/users/settings/#channel-notification-override-object) on a forum or media channel has `flags`. `NEW_FORUM_THREADS_OFF` (1 &lt;&lt; 13) and `NEW_FORUM_THREADS_ON` (1 &lt;&lt; 14) turn notifications for new posts off or on for the caller.
@@ -0,0 +1,270 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Thread members
description: Thread member objects, joining and leaving threads, member lists, and thread notification settings.
draft: true
---
import RouteHeader from '@/components/RouteHeader.astro';
A thread member records that a user joined a [thread](/http-api/threads/). A thread holds at most 1,000 members, and adding one more returns 400 `MAX_THREAD_MEMBERS`.
:::note[Every route needs the `channel_threads` experiment]
A route on this page answers 404 `NOT_FOUND` unless the experiment is active for the guild and the caller. A user session also declares `channel_threads` in the `X-Fluxer-Features` header, and a bot needs no declaration.
:::
## Thread member object
### Structure
| Field | Type | Description |
| --- | --- | --- |
| id?<sup>1</sup> | snowflake | The ID of the thread |
| user_id?<sup>1</sup> | snowflake | The ID of the user |
| join_timestamp | ISO8601 timestamp | The time the user last joined the thread |
| flags | integer | The [thread member flags](#thread-member-flags) |
| muted?<sup>2</sup> | boolean | Whether the user muted the thread |
| mute_config?<sup>2</sup> | ?[guild mute configuration](/http-api/users/settings/#guild-mute-configuration-object) object | The mute expiry of the thread, or null when none is stored |
| member?<sup>3</sup> | [guild member](/http-api/guild-members/#guild-member-object) object | The guild member of the user |
<sup>1</sup> Omitted on the `member` of a thread in [Ready](/gateway/events/#ready) and [Guild Create](/gateway/events/#guild-create)
<sup>2</sup> Present only on the caller's own membership
<sup>3</sup> Present only when the request sets `with_member`
### Example
```json
{
"id": "1501320000000000000",
"user_id": "1501314428688992222",
"join_timestamp": "2026-09-28T09:14:02.000Z",
"flags": 3,
"muted": false,
"mute_config": null
}
```
## Thread member flags
| Value | Name | Description |
| --- | --- | --- |
| 1 &lt;&lt; 0 | HAS_INTERACTED<sup>1</sup> | The user sent a message in the thread or created it |
| 1 &lt;&lt; 1 | ALL_MESSAGES | Notify the user of every message in the thread |
| 1 &lt;&lt; 2 | ONLY_MENTIONS | Notify the user only of mentions in the thread |
| 1 &lt;&lt; 3 | NO_MESSAGES | Notify the user of nothing in the thread |
<sup>1</sup> Set only by Fluxer
At most one of the three notification flags is set. With none set, the thread follows the notification level of its parent.
A member keeps the membership after losing access to the parent. Leaving the guild removes every membership in its threads.
## Join thread
<RouteHeader method="PUT" path="/v1/channels/{channel_id}/thread-members/@me" bot />
Adds the caller to a thread. Returns 204 with no body. Emits a [Thread Members Update](/gateway/events/#thread-members-update) Gateway event.
### Limitations
- The caller must be able to view the thread.
- The thread must not be archived.
- A locked thread requires a thread moderator.
Joining a thread the caller already belongs to returns 204 and changes nothing.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| channel_id | snowflake | The ID of the thread |
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| location? | string | A value Fluxer accepts and ignores (max 100 characters) |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | None | Caller is a member |
| 400 | [error response](/http-api/#error-response) | Channel is not a thread and the request returns `INVALID_CHANNEL_TYPE` |
| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED`, or locked and the request returns `THREAD_LOCKED` |
| 400 | [error response](/http-api/#error-response) | Thread has 1,000 members and the request returns `MAX_THREAD_MEMBERS` |
| 403 | [error response](/http-api/#error-response) | Caller cannot view the thread and the request returns `MISSING_ACCESS` |
### Rate limit
10 requests per 10 seconds for each authenticated user and channel ID, on the `channel:thread:member:put::channel_id` bucket.
## Add thread member
<RouteHeader method="PUT" path="/v1/channels/{channel_id}/thread-members/{user_id}" bot />
Adds another guild member to a thread. Requires `SEND_MESSAGES_IN_THREADS`. Returns 204 with no body. Emits a [Thread Members Update](/gateway/events/#thread-members-update) Gateway event.
### Limitations
- The thread must not be archived.
- A locked thread requires a thread moderator.
- A private thread with `invitable` false requires the caller or the target to be a thread moderator.
- The target must be a guild member who can view the parent.
`user_id` of `@me` behaves as [Join thread](#join-thread).
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| channel_id | snowflake | The ID of the thread |
| user_id | snowflake | The ID of the user to add |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | None | Target is a member |
| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED`, or locked and the request returns `THREAD_LOCKED` |
| 400 | [error response](/http-api/#error-response) | Thread has 1,000 members and the request returns `MAX_THREAD_MEMBERS` |
| 403 | [error response](/http-api/#error-response) | Caller lacks `SEND_MESSAGES_IN_THREADS`, or the thread is not `invitable`, each returning `MISSING_PERMISSIONS` |
| 403 | [error response](/http-api/#error-response) | Target cannot view the parent and the request returns `MISSING_ACCESS` |
| 403 | [error response](/http-api/#error-response) | Caller is timed out and the request returns `COMMUNICATION_DISABLED` |
| 404 | [error response](/http-api/#error-response) | Target is not a guild member and the request returns `UNKNOWN_MEMBER` |
### Side effects
Adding another user posts a `RECIPIENT_ADD` message in the thread that mentions the target.
### Rate limit
10 requests per 10 seconds for each authenticated user and channel ID, on the `channel:thread:member:put::channel_id` bucket.
## Leave thread
<RouteHeader method="DELETE" path="/v1/channels/{channel_id}/thread-members/@me" bot />
Removes the caller from a thread. Returns 204 with no body. Emits a [Thread Members Update](/gateway/events/#thread-members-update) Gateway event.
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | None | Caller left the thread |
| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` |
| 404 | [error response](/http-api/#error-response) | Caller is not a member and the request returns `UNKNOWN_THREAD_MEMBER` |
### Rate limit
10 requests per 10 seconds for each authenticated user and channel ID, on the `channel:thread:member:delete::channel_id` bucket.
## Remove thread member
<RouteHeader method="DELETE" path="/v1/channels/{channel_id}/thread-members/{user_id}" bot />
Removes a member from a thread. Requires `MANAGE_THREADS`, or ownership of a private thread. Returns 204 with no body. Emits a [Thread Members Update](/gateway/events/#thread-members-update) Gateway event.
`user_id` of `@me` or of the caller behaves as [Leave thread](#leave-thread).
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | None | Member was removed |
| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` |
| 403 | [error response](/http-api/#error-response) | Caller is neither a thread moderator nor the owner of the private thread and the request returns `MISSING_PERMISSIONS` |
| 403 | [error response](/http-api/#error-response) | Caller is timed out and the request returns `COMMUNICATION_DISABLED` |
| 404 | [error response](/http-api/#error-response) | Target is not a member and the request returns `UNKNOWN_THREAD_MEMBER` |
### Side effects
Removing another user posts a `RECIPIENT_REMOVE` message in the thread that mentions the target.
### Rate limit
10 requests per 10 seconds for each authenticated user and channel ID, on the `channel:thread:member:delete::channel_id` bucket.
## Get thread member
<RouteHeader method="GET" path="/v1/channels/{channel_id}/thread-members/{user_id}" bot />
Returns the [thread member object](#thread-member-object) of a user. Bot-only.
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| with_member? | boolean | Whether to include the guild member of the user (default false) |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [thread member](#thread-member-object) object | Member was returned |
| 404 | [error response](/http-api/#error-response) | User is not a member and the request returns `UNKNOWN_THREAD_MEMBER` |
| 404 | [error response](/http-api/#error-response) | Caller is a user and the request returns `NOT_FOUND` |
### Rate limit
40 requests per 10 seconds for each authenticated user and channel ID, on the `channel:thread:member:get::channel_id` bucket.
## List thread members
<RouteHeader method="GET" path="/v1/channels/{channel_id}/thread-members" bot />
Returns the [thread member objects](#thread-member-object) of a thread in ascending user ID order. Bot-only.
A user client reads member lists through the `thread_member_lists` option of [Lazy Request](/gateway/commands/#lazy-request).
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| with_member? | boolean | Whether to include the guild member of each thread member (default false) |
| after? | snowflake | Return members with a user ID above this one |
| limit? | integer | The maximum number of members to return (1-100, default 100) |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | array[[thread member](#thread-member-object) object] | Members were returned |
| 404 | [error response](/http-api/#error-response) | Caller is a user and the request returns `NOT_FOUND` |
### Rate limit
20 requests per 10 seconds for each authenticated user and channel ID, on the `channel:thread:members:list::channel_id` bucket.
## Modify thread member settings
<RouteHeader method="PATCH" path="/v1/channels/{channel_id}/thread-members/@me/settings" />
Changes the caller's notification settings for a thread they joined. User-only. Returns the updated [thread member object](#thread-member-object), or 204 when nothing changed. Emits a [Thread Member Update](/gateway/events/#thread-member-update) Gateway event.
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| flags?<sup>1</sup> | integer | The notification [thread member flags](#thread-member-flags) |
| muted? | boolean | Whether the thread is muted |
| mute_config? | ?[guild mute configuration input](/http-api/users/settings/#guild-mute-configuration-input-object) object | The mute expiry of the thread, or null to clear it |
<sup>1</sup> Replaces the three notification flags and keeps `HAS_INTERACTED`
A muted thread sends no push notification, including for a mention.
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [thread member](#thread-member-object) object | Settings changed |
| 204 | None | The body matched the stored settings |
| 400 | [error response](/http-api/#error-response) | `flags` sets more than one notification flag or any other bit and the request returns `INVALID_THREAD_NOTIFICATION_SETTINGS` |
| 404 | [error response](/http-api/#error-response) | Caller is not a member and the request returns `UNKNOWN_THREAD_MEMBER` |
### Rate limit
10 requests per 10 seconds for each authenticated user and channel ID, on the `channel:thread:member:settings::channel_id` bucket.
@@ -0,0 +1,622 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Threads
description: Thread objects, starting and archiving threads, archived thread lists, thread search, and thread permissions.
draft: true
---
import RouteHeader from '@/components/RouteHeader.astro';
A thread is a channel inside a guild text, announcement, forum, or media channel that holds its own messages. A public thread or an announcement thread is visible to everyone who can view its parent. A private thread is visible to its members and to thread moderators.
[Thread members](/http-api/thread-members/) defines joining, leaving, and member lists. [Forums](/http-api/forums/) defines forum and media channels, where every thread is a post.
:::note[Every route needs the `channel_threads` experiment]
A route on this page answers 404 `NOT_FOUND` unless the experiment is active for the guild and the caller. A user session also declares `channel_threads` in the `X-Fluxer-Features` header, and a bot needs no declaration.
:::
## Thread object
A thread is a [channel object](/http-api/channels/#channel-object) with the thread fields below.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| id<sup>1</sup> | snowflake | The ID of the thread |
| type | integer | The [thread type](#thread-types) |
| guild_id | snowflake | The ID of the guild |
| parent_id | snowflake | The ID of the text, forum, or media channel the thread belongs to |
| owner_id | snowflake | The ID of the user or webhook that created the thread |
| name | string | The name of the thread (1-100 characters) |
| last_message_id | ?snowflake | The ID of the most recent message, or null when the thread has none |
| last_pin_timestamp | ?ISO8601 timestamp | The time a message was most recently pinned, or null when nothing has ever been pinned |
| rate_limit_per_user | integer | The slowmode interval in seconds (0-21,600) |
| flags<sup>2</sup> | integer | The [channel flags](/http-api/forums/#channel-flags) of the thread |
| thread_metadata | [thread metadata](#thread-metadata-object) object | The archive and lock state of the thread |
| message_count<sup>3</sup> | integer | The number of messages in the thread |
| total_message_sent<sup>4</sup> | integer | The number of messages ever sent in the thread |
| member_count<sup>5</sup> | integer | The number of thread members, capped at 50 |
| applied_tags?<sup>6</sup> | array[snowflake] | The IDs of the [forum tags](/http-api/forums/#forum-tag-object) applied to a post (max 5) |
| member_ids_preview?<sup>6</sup> | array[snowflake] | The IDs of the most recently joined members of a post, newest first (max 8) |
| member?<sup>7</sup> | [thread member](/http-api/thread-members/#thread-member-object) object | The thread member object of the caller |
<sup>1</sup> A thread started from a message has the ID of that message. A forum post has the ID of its first message
<sup>2</sup> Only `PINNED` is ever set, and only on a forum or media post
<sup>3</sup> Excludes the first message and every deleted message
<sup>4</sup> Includes deleted messages and never decreases
<sup>5</sup> The value stays at 50 when the thread has more members
<sup>6</sup> Present only on a thread whose parent is a forum or media channel
<sup>7</sup> Present only on [Get channel](/http-api/channels/#get-channel), on the `threads` list of a guild in [Ready](/gateway/events/#ready) and [Guild Create](/gateway/events/#guild-create), and on `thread` of a message, each when the caller is a member
A thread has no `position`, no `permission_overwrites`, and no `nsfw` fields. It uses the permissions of its parent as [Thread permissions](#thread-permissions) describes. An age restriction and a content warning resolve through the parent, then the parent category, and finally the guild.
### Example
```json
{
"id": "1501320000000000000",
"type": 11,
"guild_id": "1501314428688990000",
"parent_id": "1501314428688998182",
"owner_id": "1501314428688992222",
"name": "Release checklist",
"last_message_id": "1501320500000000000",
"last_pin_timestamp": null,
"rate_limit_per_user": 0,
"flags": 0,
"thread_metadata": {
"archived": false,
"auto_archive_duration": 4320,
"archive_timestamp": "2026-09-28T09:14:02.000Z",
"locked": false,
"create_timestamp": "2026-09-28T09:14:02.000Z"
},
"message_count": 12,
"total_message_sent": 14,
"member_count": 3
}
```
## Thread metadata object
The archive and lock state of a thread.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| archived | boolean | Whether the thread is archived |
| auto_archive_duration | integer | The [auto archive duration](#auto-archive-durations) in minutes |
| archive_timestamp<sup>1</sup> | ISO8601 timestamp | The time the archive state last changed |
| locked | boolean | Whether only thread moderators can act in the thread |
| invitable?<sup>2</sup> | boolean | Whether a member who is not a thread moderator can add other members |
| create_timestamp | ISO8601 timestamp | The time the thread was created |
<sup>1</sup> Equal to `create_timestamp` until the thread is first archived or its duration changes
<sup>2</sup> Present only on a private thread
## Thread types
| Value | Name | Description |
| --- | --- | --- |
| 10 | ANNOUNCEMENT_THREAD | A thread in an announcement channel, visible to everyone who can view the parent |
| 11 | PUBLIC_THREAD | A thread visible to everyone who can view the parent |
| 12 | PRIVATE_THREAD | A thread in a guild text channel visible to its members and to thread moderators |
A forum or media post is always `PUBLIC_THREAD`. Every thread in an announcement channel is `ANNOUNCEMENT_THREAD`, and an announcement channel holds no private threads.
Converting a guild text channel into an announcement channel turns each of its public threads into an announcement thread, and converting it back turns them into public threads again. Each active thread emits [Thread Update](/gateway/events/#thread-update) with the new `type`. A guild text channel with a private thread cannot become an announcement channel, and the request returns 400 `CHANNEL_HAS_THREADS`.
A message in an announcement thread cannot be published to following channels, and a follow never delivers it. Publishing the message a thread started from works as for any other announcement, and the copies carry neither `HAS_THREAD` nor `thread`.
## Auto archive durations
| Value | Description |
| --- | --- |
| 60 | One hour |
| 1440 | One day |
| 4320 | Three days, the default |
| 10080 | One week |
A thread inherits the `default_auto_archive_duration` of its parent when the create request omits the field, and uses 4320 when the parent has none.
## Thread lifecycle
Fluxer archives an active thread once no message has arrived for its `auto_archive_duration`. A pinned forum post is never archived automatically. Archiving a pinned post clears its `PINNED` flag.
A guild holds at most 1,000 active threads. Creating or unarchiving a thread past that count returns 400 `MAX_ACTIVE_THREADS`.
Sending a message to an archived thread that is not locked unarchives it. Fluxer emits [Thread Update](/gateway/events/#thread-update) before the message. A locked thread accepts messages, reactions, and member changes only from a thread moderator.
An archived thread accepts no reaction, pin, edit, join, leave, or member change, and each returns 400 `THREAD_ARCHIVED`. Deleting a message in an archived thread is allowed.
### Thread moderators
A thread moderator is the guild owner, a member with `ADMINISTRATOR`, or a member holding `MANAGE_THREADS` in the parent who is not timed out.
## Thread permissions
A thread has no permission overwrites of its own. Fluxer computes permissions in the parent channel and then replaces `SEND_MESSAGES` with `SEND_MESSAGES_IN_THREADS`. Posting in a forum or media channel itself uses the unreplaced `SEND_MESSAGES` of that channel.
| Value | Name | Description |
| --- | --- | --- |
| 1 &lt;&lt; 34 | MANAGE_THREADS<sup>1</sup> | Archive, lock, rename, and delete a thread, and act in a locked thread |
| 1 &lt;&lt; 35 | CREATE_PUBLIC_THREADS | Start a public thread in a guild text channel |
| 1 &lt;&lt; 36 | CREATE_PRIVATE_THREADS | Start a private thread in a guild text channel |
| 1 &lt;&lt; 38 | SEND_MESSAGES_IN_THREADS | Send a message in a thread, and add another member to a thread |
<sup>1</sup> An [elevated permission](/http-api/permissions/#elevated-permissions). The thread owner and member alternatives below are checked first, so a thread owner acting on their own thread needs no authenticator
`ADMINISTRATOR` grants all four bits. A role or overwrite write keeps the stored value of the four bits unless the caller can view threads in the guild.
A timed out member can read a thread and can join or leave one. Every other thread write from a timed out member who is neither the guild owner nor an administrator returns 403 `COMMUNICATION_DISABLED`.
A caller who can view the parent but is neither a member nor a thread moderator receives 403 `MISSING_ACCESS` for a private thread. Losing access to the parent keeps the membership, and the member receives no event from the thread until access returns.
## Start thread from message
<RouteHeader method="POST" path="/v1/channels/{channel_id}/messages/{message_id}/threads" bot auditReason />
Starts a public thread from a message in a guild text channel, or an announcement thread from a message in an announcement channel. Requires `CREATE_PUBLIC_THREADS` and `READ_MESSAGE_HISTORY`. Returns the [thread object](#thread-object), which extends the [channel object](/http-api/channels/#channel-object). Emits a [Thread Create](/gateway/events/#thread-create) Gateway event.
### Limitations
- The channel must be a guild text or announcement channel.
- The message must be a default message or a reply.
- A message starts at most one thread.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| channel_id | snowflake | The ID of the guild text or announcement channel |
| message_id | snowflake | The ID of the message to start the thread from |
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| name | string | The name of the thread (1-100 characters) |
| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes |
| rate_limit_per_user? | integer | The slowmode interval in seconds (0-21,600) |
| location? | string | A value Fluxer accepts and ignores (max 100 characters) |
`rate_limit_per_user` defaults to the `default_thread_rate_limit_per_user` of the parent, or `0` when the parent has none.
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 201 | [thread](#thread-object) object | Thread was started |
| 400 | [error response](/http-api/#error-response) | Channel is not a guild text or announcement channel and the request returns `INVALID_CHANNEL_TYPE` |
| 400 | [error response](/http-api/#error-response) | Message is a system message and the request returns `CANNOT_MODIFY_SYSTEM_WEBHOOK` |
| 400 | [error response](/http-api/#error-response) | Message already started a thread and the request returns `THREAD_ALREADY_CREATED_FOR_MESSAGE` |
| 400 | [error response](/http-api/#error-response) | Guild has 1,000 active threads and the request returns `MAX_ACTIVE_THREADS` |
| 400 | [error response](/http-api/#error-response) | Parent slowmode denies the thread and the request returns `SLOWMODE_RATE_LIMITED` |
| 403 | [error response](/http-api/#error-response) | Caller lacks a required permission and the request returns `MISSING_PERMISSIONS` |
| 403 | [error response](/http-api/#error-response) | Caller is timed out and the request returns `COMMUNICATION_DISABLED` |
| 404 | [error response](/http-api/#error-response) | Channel does not exist and the request returns `UNKNOWN_CHANNEL` |
| 404 | [error response](/http-api/#error-response) | Message does not exist and the request returns `UNKNOWN_MESSAGE` |
### Side effects
- Fluxer sets `HAS_THREAD` on the message and emits [Message Update](/gateway/events/#message-update) with `thread`.
- The thread opens with a `THREAD_STARTER_MESSAGE`, which has the thread ID and references the source message.
- A source message at least five minutes old also produces a `THREAD_CREATED` message in the parent.
- The caller becomes the first thread member.
Starting a thread uses the parent slowmode on its own counter. A bot and a member holding `BYPASS_SLOWMODE` are exempt.
### Rate limit
10 requests per 10 seconds for each authenticated user and channel ID, on the `channel:thread:create::channel_id` bucket.
## Start thread
<RouteHeader method="POST" path="/v1/channels/{channel_id}/threads" bot auditReason />
Starts a thread with no source message, or creates a post in a forum or media channel. Returns the [thread object](#thread-object), which extends the [channel object](/http-api/channels/#channel-object). Emits a [Thread Create](/gateway/events/#thread-create) Gateway event.
### Limitations
- In a guild text channel, a public thread requires `CREATE_PUBLIC_THREADS` and a private thread requires `CREATE_PRIVATE_THREADS`.
- In an announcement channel, a thread requires `CREATE_PUBLIC_THREADS`. Both `ANNOUNCEMENT_THREAD` and `PUBLIC_THREAD` start an announcement thread, and `PRIVATE_THREAD` returns 400 `INVALID_CHANNEL_TYPE`.
- In a guild text channel, `ANNOUNCEMENT_THREAD` returns 400 `INVALID_CHANNEL_TYPE`.
- In a forum or media channel, a post requires `SEND_MESSAGES` in that channel.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| channel_id | snowflake | The ID of the guild text, announcement, forum, or media channel |
### JSON body
The parent type selects the body. A forum or media channel also accepts multipart form data with the JSON in a `payload_json` part and files in `files[n]` parts, as [Create message](/http-api/messages/#create-message) describes.
#### Text and announcement channel body
| Field | Type | Description |
| --- | --- | --- |
| name | string | The name of the thread (1-100 characters) |
| type | integer | The [thread type](#thread-types) to start |
| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes |
| rate_limit_per_user? | integer | The slowmode interval in seconds (0-21,600) |
| invitable?<sup>1</sup> | boolean | Whether a member who is not a thread moderator can add other members (default true) |
| location? | string | A value Fluxer accepts and ignores (max 100 characters) |
<sup>1</sup> Read only for a private thread
#### Forum post body
| Field | Type | Description |
| --- | --- | --- |
| name | string | The name of the post (1-100 characters) |
| message | object | The first message, with the fields of [Create message](/http-api/messages/#create-message) other than `message_reference`, `nonce`, `tts`, and `favorite_meme_id` |
| applied_tags?<sup>1</sup> | array[snowflake] | The IDs of the forum tags to apply (max 5) |
| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes |
| rate_limit_per_user? | integer | The slowmode interval in seconds (0-21,600) |
| type?<sup>2</sup> | integer | The thread type, which is always `11` |
| location? | string | A value Fluxer accepts and ignores (max 100 characters) |
<sup>1</sup> Required when the channel has `REQUIRE_TAG`. A tag marked `moderated` needs a thread moderator
<sup>2</sup> Any other value returns 400 `INVALID_CHANNEL_TYPE`
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 201 | [forum post](/http-api/forums/#forum-post-object) object | Post was created in a forum or media channel |
| 201 | [thread](#thread-object) object | Thread was started in a guild text or announcement channel |
| 400 | [error response](/http-api/#error-response) | Channel cannot hold threads, or the thread type does not fit it, returning `INVALID_CHANNEL_TYPE` |
| 400 | [error response](/http-api/#error-response) | Guild has 1,000 active threads and the request returns `MAX_ACTIVE_THREADS` |
| 400 | [error response](/http-api/#error-response) | Parent slowmode denies the thread and the request returns `SLOWMODE_RATE_LIMITED` |
| 400 | [error response](/http-api/#error-response) | A forum post names no tag under `REQUIRE_TAG` and the request returns `FORUM_TAG_REQUIRED` |
| 400 | [error response](/http-api/#error-response) | As above while every tag is moderated, returning `NO_TAGS_AVAILABLE_TO_NON_MODERATORS` |
| 403 | [error response](/http-api/#error-response) | Caller lacks a required permission or applies a moderated tag, returning `MISSING_PERMISSIONS` |
| 403 | [error response](/http-api/#error-response) | Caller is timed out and the request returns `COMMUNICATION_DISABLED` |
| 404 | [error response](/http-api/#error-response) | Channel does not exist and the request returns `UNKNOWN_CHANNEL` |
| 404 | [error response](/http-api/#error-response) | A tag does not exist and the request returns `UNKNOWN_FORUM_TAG` |
### Side effects
- A public thread in a guild text channel and an announcement thread post a `THREAD_CREATED` message in the parent. A private thread posts none.
- The first message of a forum post has the ID of the post, and a failed first message deletes the post again.
- Creating a forum post marks the forum as read for the caller.
- The caller becomes the first thread member.
### Rate limit
10 requests per 10 seconds for each authenticated user and channel ID, on the `channel:thread:create::channel_id` bucket.
## Modify thread
[Modify channel](/http-api/channels/#modify-channel) on a thread takes the thread body below and returns the updated [thread object](#thread-object). It emits a [Thread Update](/gateway/events/#thread-update) Gateway event.
### Limitations
- `name`, `auto_archive_duration`, and archiving need the thread owner or a thread moderator.
- `locked` needs a thread moderator, and the thread owner can set it to true.
- `rate_limit_per_user` and `flags` need a thread moderator.
- `invitable` needs the thread owner or a thread moderator.
- Unarchiving needs a thread member or the thread owner, and needs a thread moderator when the thread is locked.
- An archived thread accepts no other change unless the same body sets `archived` to false.
### Thread body
| Field | Type | Description |
| --- | --- | --- |
| name? | string | The name of the thread (1-100 characters) |
| archived? | boolean | Whether the thread is archived |
| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes |
| locked? | boolean | Whether only thread moderators can act in the thread |
| rate_limit_per_user? | integer | The slowmode interval in seconds (0-21,600) |
| flags?<sup>1</sup> | integer | The [channel flags](/http-api/forums/#channel-flags) of the thread |
| applied_tags?<sup>2</sup> | array[snowflake] | The complete set of forum tag IDs (max 5) |
| invitable?<sup>3</sup> | boolean | Whether a member who is not a thread moderator can add other members |
<sup>1</sup> Only `PINNED` on a forum or media post. Any other bit returns 400 `INVALID_FORM_BODY`
<sup>2</sup> Only on a forum or media post, and only by the thread owner or a thread moderator. Any other thread returns 400 `INVALID_CHANNEL_TYPE`
<sup>3</sup> Read only for a private thread
Pinning a second post in one forum returns 400 `MAX_PINNED_THREADS_IN_FORUM`. Changing `auto_archive_duration` resets `archive_timestamp`. Renaming a thread posts a `CHANNEL_NAME_CHANGE` message in it.
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [thread](#thread-object) object | Thread was modified |
| 400 | [error response](/http-api/#error-response) | Thread is archived and the body changes another field, returning `THREAD_ARCHIVED` |
| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` |
| 400 | [error response](/http-api/#error-response) | Unarchiving would exceed 1,000 active threads and the request returns `MAX_ACTIVE_THREADS` |
| 400 | [error response](/http-api/#error-response) | Forum already has a pinned post and the request returns `MAX_PINNED_THREADS_IN_FORUM` |
| 403 | [error response](/http-api/#error-response) | Caller lacks the owner or moderator role the field needs and the request returns `MISSING_PERMISSIONS` |
| 403 | [error response](/http-api/#error-response) | Caller is timed out and the request returns `COMMUNICATION_DISABLED` |
### Rate limit
Shared with [Modify channel](/http-api/channels/#modify-channel).
## Delete thread
[Delete or leave channel](/http-api/channels/#delete-or-leave-channel) on a thread deletes it with its messages and memberships. It requires `MANAGE_THREADS` and emits a [Thread Delete](/gateway/events/#thread-delete) Gateway event.
### Side effects
- The source message of the thread loses `HAS_THREAD`, and Fluxer emits [Message Update](/gateway/events/#message-update) for it.
- The guild audit log records action type 112.
### Rate limit
Shared with [Delete or leave channel](/http-api/channels/#delete-or-leave-channel).
## List active guild threads
<RouteHeader method="GET" path="/v1/guilds/{guild_id}/threads/active" bot />
Returns every active thread in the guild that the bot can view, newest first. Bot-only.
A private thread is included when the bot is a member or a thread moderator in its parent.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| guild_id | snowflake | The ID of the guild |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| threads | array[[thread](#thread-object) object] | The active threads |
| members | array[[thread member](/http-api/thread-members/#thread-member-object) object] | A thread member object for each returned thread the bot joined |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | active threads object | Threads were returned |
| 404 | [error response](/http-api/#error-response) | Caller is a user, and the request returns `NOT_FOUND` |
### Rate limit
5 requests per 10 seconds for each authenticated user and guild ID, on the `guild:threads:active::guild_id` bucket.
## List public archived threads
<RouteHeader method="GET" path="/v1/channels/{channel_id}/threads/archived/public" bot />
Returns the archived public threads of a guild text, announcement, forum, or media channel, most recently archived first. An announcement channel returns announcement threads. Requires `READ_MESSAGE_HISTORY`.
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| channel_id | snowflake | The ID of the parent channel |
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| before? | ISO8601 timestamp | Return threads archived before this time |
| limit? | integer | The maximum number of threads to return (2-100, default 50) |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| threads | array[[thread](#thread-object) object] | The archived threads |
| members | array[[thread member](/http-api/thread-members/#thread-member-object) object] | A thread member object for each returned thread the caller joined |
| has_more | boolean | Whether a later request can return more threads |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | archived threads object | Threads were returned |
| 400 | [error response](/http-api/#error-response) | Channel is not a thread parent and the request returns `INVALID_CHANNEL_TYPE` |
| 403 | [error response](/http-api/#error-response) | Caller lacks `READ_MESSAGE_HISTORY` and the request returns `MISSING_PERMISSIONS` |
### Rate limit
20 requests per 10 seconds for each authenticated user and channel ID, on the `channel:threads:archived:list::channel_id` bucket.
## List private archived threads
<RouteHeader method="GET" path="/v1/channels/{channel_id}/threads/archived/private" bot />
Returns the archived private threads of a guild text channel, most recently archived first. Requires `READ_MESSAGE_HISTORY` and `MANAGE_THREADS`.
The path parameters, query parameters, and response body match [List public archived threads](#list-public-archived-threads). An announcement, forum, or media channel returns 400 `INVALID_CHANNEL_TYPE`.
### Rate limit
20 requests per 10 seconds for each authenticated user and channel ID, on the `channel:threads:archived:list::channel_id` bucket.
## List joined private archived threads
<RouteHeader method="GET" path="/v1/channels/{channel_id}/users/@me/threads/archived/private" bot />
Returns the archived private threads of a guild text channel that the caller joined, newest first. Requires `READ_MESSAGE_HISTORY`.
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| before? | snowflake | Return threads with an ID below this thread ID |
| limit? | integer | The maximum number of threads to return (2-100, default 50) |
The response body matches [List public archived threads](#list-public-archived-threads).
### Rate limit
20 requests per 10 seconds for each authenticated user and channel ID, on the `channel:threads:archived:list::channel_id` bucket.
## Search threads
<RouteHeader method="GET" path="/v1/channels/{channel_id}/threads/search" bot />
Searches the threads of a guild text, forum, or media channel by name and tag. Requires `READ_MESSAGE_HISTORY`.
A caller who is not a thread moderator finds public threads and the private threads they joined.
### Query parameters
| Field | Type | Description |
| --- | --- | --- |
| name? | string | The text to match in thread names (max 100 characters) |
| tag? | array[snowflake] | The forum tag IDs to filter by, repeated once per tag (max 20) |
| tag_setting? | string | `match_some` or `match_all`, defaulting to the `default_tag_setting` of the channel |
| archived? | boolean | Whether to return only archived or only active threads (default both) |
| sort_by? | string | `last_message_time`, `archive_time`, `relevance`, or `creation_time` (default `last_message_time`) |
| sort_order? | string | `asc` or `desc` (default `desc`) |
| limit? | integer | The maximum number of threads to return (1-25, default 25) |
| offset? | integer | The number of threads to skip (0-9,975, default 0) |
| max_id? | snowflake | Return threads created before this ID |
| min_id? | snowflake | Return threads created after this ID |
| slop? | integer | A value Fluxer accepts and ignores (0-100, default 2) |
### Response body
| Field | Type | Description |
| --- | --- | --- |
| threads | array[[thread](#thread-object) object] | The matching threads |
| members | array[[thread member](/http-api/thread-members/#thread-member-object) object] | A thread member object for each returned thread the caller joined |
| has_more | boolean | Whether a later request can return more threads |
| total_results | integer | The number of matching threads |
| first_messages?<sup>1</sup> | array[[message](/http-api/messages/#message-object) object] | The first message of each returned post |
<sup>1</sup> Present only for a forum or media channel
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | thread search object | Search completed |
| 202 | index not ready object | The guild thread index is being built |
| 400 | [error response](/http-api/#error-response) | Channel is not a thread parent and the request returns `INVALID_CHANNEL_TYPE` |
| 403 | [error response](/http-api/#error-response) | Caller lacks `READ_MESSAGE_HISTORY` and the request returns `MISSING_PERMISSIONS` |
The 202 body is `code` `SEARCH_INDEX_NOT_READY`, `message`, `documents_indexed` of `0`, and `retry_after` in seconds. The first search in a guild starts the index build.
### Rate limit
20 requests per 10 seconds for each authenticated user and channel ID, on the `channel:threads:search::channel_id` bucket.
## Thread messages
Two message types belong to threads.
| Value | Name | Description |
| --- | --- | --- |
| 18 | THREAD_CREATED | Posted in the parent when a public thread starts |
| 21 | THREAD_STARTER_MESSAGE | The first message of a thread started from a message |
`message_reference.channel_id` of a `THREAD_CREATED` message is the thread, and it has no `message_id`. A `THREAD_STARTER_MESSAGE` has the ID of its thread, references the source message, and cannot be deleted. When the source message is deleted, the thread stays and its `referenced_message` becomes null.
A message that started a thread has `thread`, the [thread object](#thread-object), and the message flag `HAS_THREAD` (1 &lt;&lt; 5). Mentioning roles whose members would push the thread past 250 automatic additions sets `FAILED_TO_MENTION_SOME_ROLES_IN_THREAD` (1 &lt;&lt; 8). Only Fluxer sets either flag.
Sending a message in a thread adds a user sender who is not a member. A mentioned user who can view the parent is added too, unless the thread is private and not `invitable`.
## Thread fields on other objects
| Object | Field | Description |
| --- | --- | --- |
| Guild text channel | default_auto_archive_duration | The auto archive duration new threads inherit |
| Guild text channel | default_thread_rate_limit_per_user | The slowmode interval new threads inherit |
| Read state | flags | `IS_GUILD_CHANNEL` (1 &lt;&lt; 0) and `IS_THREAD` (1 &lt;&lt; 1), set by Fluxer |
| Message search result | threads | The threads that contain a returned message |
| Message search result | members | A thread member object for each of those threads the caller joined |
| Audit log | threads | The threads referenced by the returned entries |
| Experiment assignments | channel_threads | `active` and `config_version`, present only for an account in the rollout |
The guild audit log records thread creation, update, and deletion as action types 110, 111, and 112.
## Thread error codes
### `FORUM_TAG_NAMES_MUST_BE_UNIQUE`
Tag names must be unique
### `FORUM_TAG_REQUIRED`
A tag is required to create a post in this channel
### `HIDE_MEDIA_DOWNLOAD_OPTION_MEDIA_ONLY`
Hiding media download options is only available in media channels
### `INVALID_THREAD_NOTIFICATION_SETTINGS`
Invalid thread notification settings
### `MAX_ACTIVE_THREADS`
This community has reached its active thread limit of {count}
### `MAX_FORUM_TAGS`
This channel has reached its tag limit of {count}
### `MAX_PINNED_THREADS_IN_FORUM`
This channel has reached its pinned post limit of {count}
### `MAX_THREAD_MEMBERS`
This thread has reached its member limit of {count}
### `NO_TAGS_AVAILABLE_TO_NON_MODERATORS`
A tag is required, but only moderators can apply the tags in this channel
### `SEARCH_INDEX_NOT_READY`
The search index isn't ready yet
### `THREAD_ALREADY_CREATED_FOR_MESSAGE`
A thread has already been created for this message
### `THREAD_ARCHIVED`
This thread is archived
### `THREAD_LOCKED`
This thread is locked
### `UNKNOWN_FORUM_TAG`
Tag wasn't found
### `UNKNOWN_THREAD_MEMBER`
Thread member wasn't found
### `WEBHOOK_FORUM_TARGET_CONFLICT`
You can't specify both `thread_id` and `thread_name`
### `WEBHOOK_FORUM_TARGET_REQUIRED`
Webhooks posting to this channel need a `thread_id` or a `thread_name`
### `WEBHOOK_SERVICE_FORUM_UNSUPPORTED`
This webhook service can't post to a forum or media channel without a `thread_id`
### `WEBHOOK_THREAD_NAME_REQUIRES_FORUM`
`thread_name` and `applied_tags` only work in forum and media channels