API reference
Message Components
Buttons, select menus, layouts built from containers and sections, and forms (modals), like Discord's.
Components put things people can use on your bot's messages: buttons, select menus, and with the IS_COMPONENTS_V2 flag a whole message laid out from containers, sections, text, images and separators. Bots can also open modals, small forms with text inputs.
The shapes, type numbers and field names are Discord's, so existing component builders work. Field names are snake_case (custom_id, min_values); camelCase (customId) is accepted too. Messages come back with components normalized: numeric type and style, and defaults filled in.
Who can send them
| Sender | What |
|---|---|
Bots (Authorization: Bot) | Everything: on POST and PATCH /api/channels/{channelId}/messages, interaction answers and follow-ups (ephemeral or not) |
| Webhooks | Only what nobody has to answer: link buttons and layout components. Buttons with a custom_id and select menus are refused |
| People | Nothing: 403 components_not_allowed |
Clicks go to the bot that sent the message. Published copies of announcements keep their components with buttons and select menus switched off.
Two ways to build a message
Classic ("v1"). components holds up to 5 action rows shown under the message's text and embeds. Each row holds 1-5 buttons or exactly one select menu.
{
"content": "Ready for the raid?",
"components": [
{
"type": 1,
"components": [
{ "type": 2, "style": 3, "label": "I'm in", "custom_id": "raid:join" },
{ "type": 2, "style": 2, "label": "Maybe", "custom_id": "raid:maybe", "emoji": { "name": "🤔" } },
{ "type": 2, "style": 5, "label": "Raid guide", "url": "https://example.com/guide" }
]
}
]
}Components only ("v2"). Set "flags": 32768 (IS_COMPONENTS_V2, 1 << 15) and the whole message is built from components: no content, no embeds, no files or polls. Text goes in text displays. A message sent with the flag keeps it: edits can't remove it (invalid_flags), and an edit can turn a classic message into v2 if its text and embeds are gone.
{
"flags": 32768,
"components": [
{
"type": 17,
"accent_color": 3065014,
"components": [
{ "type": 10, "content": "**Patch 1.4** is out, <@103600000000000000>!" },
{
"type": 9,
"components": [
{ "type": 10, "content": "**New map: Harbor**" },
{ "type": 10, "content": "Night fights, fog and a lighthouse." }
],
"accessory": { "type": 11, "media": { "url": "https://example.com/harbor.png" }, "description": "Harbor at night" }
},
{ "type": 14, "divider": true, "spacing": 1 },
{ "type": 12, "items": [{ "media": { "url": "https://example.com/1.png" } }, { "media": { "url": "https://example.com/2.png" }, "spoiler": true }] },
{ "type": 1, "components": [{ "type": 2, "style": 1, "label": "Send feedback", "custom_id": "feedback" }] }
]
}
]
}Component types
type | Name | Where |
|---|---|---|
1 | Action Row | Top of a message (v1 and v2), inside containers; holds buttons or one select |
2 | Button | Action rows, section accessories |
3 | String Select | Action rows |
4 | Text Input | Modals only |
5 | User Select | Action rows |
6 | Role Select | Action rows |
7 | Mentionable Select (people and roles) | Action rows |
8 | Channel Select | Action rows |
9 | Section | v2: top level, containers |
10 | Text Display | v2: top level, containers, sections |
11 | Thumbnail | Section accessories |
12 | Media Gallery | v2: top level, containers |
14 | Separator | v2: top level, containers |
17 | Container | v2: top level |
File components (13) and premium buttons (style 6) aren't supported yet. Every component may have an optional numeric id, unique within the message.
Buttons
| Field | Notes |
|---|---|
style | 1 primary (teal), 2 secondary (grey), 3 success (green), 4 danger (red), 5 link. Names ("primary") work too |
label | Up to 80 characters. A button needs a label, an emoji, or both |
emoji | { "name": "🎉" }, or a custom emoji { "id": "...", "name": "baff", "animated": false } |
custom_id | 1-100 characters, unique within the message. Every style but link |
url | Link buttons only: an http(s) URL. People are asked before a link leaves baff.gg |
disabled | Shows the button switched off; clicks are refused |
Select menus
| Field | Notes |
|---|---|
custom_id | 1-100 characters, unique within the message |
placeholder | Up to 150 characters, shown while nothing is picked |
min_values / max_values | 0-25 and 1-25 (both default to 1). One pick sends right away; menus that take more send when they close |
disabled | Switched off |
options | String selects only: 1-25 of { label, value, description?, emoji?, default? } (label and value 1-100 characters, values unique, description up to 100) |
default_values | User, role, mentionable and channel selects: picked to begin with, [{ "id": "...", "type": "user" | "role" | "channel" }] |
channel_types | Channel selects only: Discord's numbers, 0 text, 2 voice, 4 category, 5 announcement, 10-12 threads |
User selects list the server's members (in a DM, the two of you), role selects its roles, channel selects the channels the person can see.
Layout (v2)
| Component | Fields |
|---|---|
| Text Display | content: Markdown like messages; mentions ping and render as pills |
| Section | components: 1-3 text displays; accessory: a thumbnail or a button (required) |
| Thumbnail | media: { url }, description (up to 1024), spoiler |
| Media Gallery | items: 1-10 of { media: { url }, description?, spoiler? }; laid out in Discord's grids and opened in the image viewer |
| Separator | divider (draw a line, default true), spacing: 1 small (default) or 2 large |
| Container | components: action rows, text displays, sections, galleries, separators; accent_color (0-0xFFFFFF, the bar on the left); spoiler (covered until clicked) |
Media URLs must be http(s) (no attachment:// yet).
Limits
| Limit | Value |
|---|---|
| Action rows in a classic message | 5 |
| Components in one message, nested ones and accessories included | 40 |
| Characters across all text displays | 4000 |
| Buttons per row | 5 (or 1 select menu) |
| Text displays per section | 1-3 |
| Gallery items | 1-10 |
| Select options | 25 |
Anything wrong answers 400 invalid_components naming the spot, for example components[0].components[2].label can be up to 80 characters. A v2 message with text or embeds gets invalid_components too; unknown flags get invalid_flags.
Text displays count as the message's text: their mentions ping, and a dev://experiment link in one makes the message experiment-only, like in content. A reply to a v2 message previews its first text display.
Clicks and choices
Anyone who can see the message may click a button or pick in a select menu, as often as they like (a rate limit applies). Switched-off components refuse clicks (component_disabled). Your bot gets an interaction with "type": "component":
{
"type": "component",
"id": "103745871234569999",
"token": "9c2d...",
"applicationId": "103712345678901247",
"channelId": "103700000000000001",
"serverId": "103700000000000000",
"user": { "id": "103600000000000000", "username": "andre", "displayName": "Andre", "avatarUrl": null, "roles": [] },
"message": { "id": "103745871234568500", "content": "Ready for the raid?", "components": [], "flags": 0, "interactionName": "" },
"data": { "customId": "raid:join", "componentType": 2, "label": "I'm in" }
}message is the full message object the component is on (interactionName = the command it answered, or ""). For select menus, data has values (the option values, or ids) and, for user, role, mentionable and channel selects, resolved:
"data": {
"customId": "pick-mod",
"componentType": 5,
"values": ["103600000000000007"],
"resolved": {
"users": { "103600000000000007": { "id": "103600000000000007", "username": "mira", "displayName": "Mira", "avatarUrl": null } }
}
}resolved can also hold roles ({ id, name, color }) and channels ({ id, name, type, parentId }). Values are checked first: options that don't exist, people not in the server, channels the person can't see or of the wrong type, and too few or too many values are refused before your bot hears about them.
Answering clicks
Answer within 3 seconds like any interaction (callback). Besides message and defer, clicks take:
type | What happens |
|---|---|
update (7) | Edits the message the component is on: content, embeds, components, flags (absent ones are kept). Everyone gets MESSAGE_UPDATE; it isn't marked "(edited)" |
deferUpdate (6) | Acknowledges now. Edit the message later (within 15 minutes) with PATCH /api/interactions/{id}/{token}/original and the same fields |
modal (9) | Opens a form, see below |
// A counter: each click updates the same message.
if (i.type === 'component' && i.data.customId.startsWith('count:')) {
const n = Number(i.data.customId.split(':')[1]) + 1;
await post(`/interactions/${i.id}/${i.token}/callback`, {
type: 'update',
components: [{ type: 1, components: [{ type: 2, style: 1, label: `Clicked ${n} times`, custom_id: `count:${n}` }] }]
});
}If your bot doesn't answer in time, the person sees that it didn't respond. Ephemeral answers with components can be clicked (and updated) by the person they were shown to for 15 minutes.
Bots that sent a plain list of buttons (without an action row, with "type": "button") keep working: such a list is put into rows of 5.
Modals
Answer a command or a click with modal to open a form (not a modal submit: one form per interaction):
{
"type": "modal",
"title": "Send feedback",
"custom_id": "feedback",
"components": [
{ "type": 1, "components": [{ "type": 4, "custom_id": "topic", "label": "Topic", "style": 1, "max_length": 60 }] },
{ "type": 1, "components": [{ "type": 4, "custom_id": "details", "label": "Details", "style": 2, "min_length": 10, "max_length": 500, "required": false }] }
]
}| Field | Notes |
|---|---|
title | 1-45 characters |
custom_id | 1-100 characters, sent back on submit |
components | 1-5 action rows, each with one text input |
Text input style | 1 short (one line) or 2 paragraph |
label | 1-45 characters |
placeholder | Up to 100 characters |
min_length / max_length | 0-4000 and 1-4000 |
required | Default true |
value | Prefilled text |
When the person sends it, required fields and lengths are checked, then your bot gets "type": "modal_submit" (with message too when the form was opened from a click):
{
"type": "modal_submit",
"id": "103745871234570001",
"token": "1f0a...",
"data": {
"customId": "feedback",
"fields": [
{ "customId": "topic", "value": "Buttons" },
{ "customId": "details", "value": "They work nicely." }
]
}
}Answer it like a command (message, defer, and update / deferUpdate when it came from a click). Each form can be sent once; closing it sends nothing.