Buttons and forms
An app's messages can carry components: buttons and select menus under the message. A click, a choice or a form submission is a signed request to the app's interactions URL, the same way slash commands arrive. Apps can also open forms (modals) and send private replies only one person sees.
Components follow Discord's v1 action rows, so existing knowledge and libraries carry over.
Add components to a message
Any message an app sends may have components: a new post (POST /v1/channels/{channelId}/messages), an edit (PATCH …/messages/{messageId}), an interaction reply, a follow-up or an @original edit.
{
"content": "Deploy to production?",
"components": [
{ "type": 1, "components": [
{ "type": 2, "style": 3, "label": "Deploy", "custom_id": "deploy:yes" },
{ "type": 2, "style": 4, "label": "Cancel", "custom_id": "deploy:no" },
{ "type": 2, "style": 5, "label": "Runbook", "url": "https://example.com/runbook" }
] }
]
}
- Up to 5 rows. A row holds up to 5 buttons, or exactly one select menu.
- Buttons: styles 1 (primary), 2 (secondary), 3 (success), 4 (danger) with a
custom_id, or 5 (link) with an httpsurl. - Select menus: type 3 (your
options), 5 (people), 6 (roles), 8 (channels), withmin_values(0–25) andmax_values(1–25). - Every
custom_idis 1–100 characters and unique in the message; labels are at most 80 characters;disabled: truegreys one out.
People can't send components; a message that breaks a rule is refused with invalid_components and a sentence saying which.
Answer a click
A click is a signed POST with type: 3, the person as user / member, your message as message (with flags: 64 if it was a private reply), and data: { custom_id, component_type, values?, resolved? }. Select values are checked before you see them: string choices you offered, people in the org, real roles, and channels the person can see (never a direct message).
Answer within 3 seconds:
type |
Does |
|---|---|
| 4 | Posts a new message (private to the clicker with data.flags: 64). |
| 5 | Posts "thinking…" to finish later (also private with flags: 64). |
| 6 | Acknowledges now; edit @original later. |
| 7 | Updates the message the button is on (content, embeds, components; fields you leave out keep their value). |
| 9 | Opens a form. |
A bot only ever changes its own messages. A double click is one interaction.
Forms (modals)
Answer type: 9 with:
{ "type": 9, "data": { "custom_id": "feedback", "title": "Send feedback", "components": [
{ "type": 1, "components": [
{ "type": 4, "custom_id": "text", "style": 2, "label": "What happened?", "required": true, "max_length": 1000 } ] } ] } }
A form has a title of up to 45 characters and 1–5 rows of one text input each: style 1 (short) or 2 (paragraph), a label of up to 45 characters, required (default true), min_length / max_length up to 4 000, placeholder and a prefilled value.
The person submits once. You get type: 5 with data: { custom_id, components: [{ type: 1, components: [{ type: 4, custom_id, value }] }] } and may answer 4, 5, 6 or 7 (6 and 7 only when the form was opened from a button), never another form.
Private replies
flags: 64 on a reply or follow-up shows it only to the person who ran the command or clicked. It stays on their screens for 15 minutes (a reload shows it; they can dismiss it) and never reaches other people, search, events, exports or notifications. A follow-up with flags: 64 answers with the private message's id instead of a channel message.
When things change
If the app leaves the channel, is paused or is uninstalled, its components show as disabled and a click answers 410. Clicks share the command budget: 20 per person every 10 seconds, 120 a minute per install.
The TypeScript SDK has builders that check these limits with the server's own wording, and routes clicks and submissions by custom_id.