baff.gg Docs Developer Portal

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

SenderWhat
Bots (Authorization: Bot)Everything: on POST and PATCH /api/channels/{channelId}/messages, interaction answers and follow-ups (ephemeral or not)
WebhooksOnly what nobody has to answer: link buttons and layout components. Buttons with a custom_id and select menus are refused
PeopleNothing: 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.

json
{
	"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.

json
{
	"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

typeNameWhere
1Action RowTop of a message (v1 and v2), inside containers; holds buttons or one select
2ButtonAction rows, section accessories
3String SelectAction rows
4Text InputModals only
5User SelectAction rows
6Role SelectAction rows
7Mentionable Select (people and roles)Action rows
8Channel SelectAction rows
9Sectionv2: top level, containers
10Text Displayv2: top level, containers, sections
11ThumbnailSection accessories
12Media Galleryv2: top level, containers
14Separatorv2: top level, containers
17Containerv2: 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

FieldNotes
style1 primary (teal), 2 secondary (grey), 3 success (green), 4 danger (red), 5 link. Names ("primary") work too
labelUp to 80 characters. A button needs a label, an emoji, or both
emoji{ "name": "🎉" }, or a custom emoji { "id": "...", "name": "baff", "animated": false }
custom_id1-100 characters, unique within the message. Every style but link
urlLink buttons only: an http(s) URL. People are asked before a link leaves baff.gg
disabledShows the button switched off; clicks are refused

Select menus

FieldNotes
custom_id1-100 characters, unique within the message
placeholderUp to 150 characters, shown while nothing is picked
min_values / max_values0-25 and 1-25 (both default to 1). One pick sends right away; menus that take more send when they close
disabledSwitched off
optionsString selects only: 1-25 of { label, value, description?, emoji?, default? } (label and value 1-100 characters, values unique, description up to 100)
default_valuesUser, role, mentionable and channel selects: picked to begin with, [{ "id": "...", "type": "user" | "role" | "channel" }]
channel_typesChannel 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)

ComponentFields
Text Displaycontent: Markdown like messages; mentions ping and render as pills
Sectioncomponents: 1-3 text displays; accessory: a thumbnail or a button (required)
Thumbnailmedia: { url }, description (up to 1024), spoiler
Media Galleryitems: 1-10 of { media: { url }, description?, spoiler? }; laid out in Discord's grids and opened in the image viewer
Separatordivider (draw a line, default true), spacing: 1 small (default) or 2 large
Containercomponents: 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

LimitValue
Action rows in a classic message5
Components in one message, nested ones and accessories included40
Characters across all text displays4000
Buttons per row5 (or 1 select menu)
Text displays per section1-3
Gallery items1-10
Select options25

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":

json
{
	"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:

json
"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:

typeWhat 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
js
// 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):

json
{
	"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 }] }
	]
}
FieldNotes
title1-45 characters
custom_id1-100 characters, sent back on submit
components1-5 action rows, each with one text input
Text input style1 short (one line) or 2 paragraph
label1-45 characters
placeholderUp to 100 characters
min_length / max_length0-4000 and 1-4000
requiredDefault true
valuePrefilled 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):

json
{
	"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.