Reference
Interactions
Registering slash commands, the INTERACTION_CREATE payload, callback types, the original response and follow-ups, ephemeral replies, buttons and select menus, embed limits and the error codes.
Slash commands and message components work the Discord way: register commands over REST, receive INTERACTION_CREATE on the gateway, answer through the callback endpoint, edit or follow up through the webhook-token routes. There is no interactions endpoint URL delivery, no PING, no autocomplete and no modals.
Registering slash commands
| Scope | Endpoints |
|---|---|
| Global | GET POST /applications/{app}/commands · GET PATCH DELETE /applications/{app}/commands/{cmd} |
| Per server | GET POST /applications/{app}/guilds/{guild}/commands · GET PATCH DELETE /applications/{app}/guilds/{guild}/commands/{cmd} |
Up to 100 commands per scope. The Discord-shaped body (name, description, options, …) is stored and delivered back in INTERACTION_CREATE; options with autocomplete: true are stored but the autocomplete round-trip never happens.
Bulk overwrite works: PUT /applications/{application_id}/commands (and the guild variant) replaces the whole set in one transaction, so CommandTree.sync() and discord.js's rest.put(Routes.applicationCommands(id), { body }) deploy scripts run unchanged. Command permissions (…/commands/permissions) are not in the subset.
Receiving an interaction
A user invoking your command or clicking a component arrives as INTERACTION_CREATE — regardless of intents, like on Discord:
{
"id": "…", "application_id": "…", "token": "…", "version": 1,
"type": 2,
"data": { "id": "…", "name": "ping", "options": [] },
"guild_id": "…", "channel_id": "…",
"member": { "user": { "id": "…", "username": "…" }, "roles": ["…"] }
} type2 = application command, 3 = message component.datais Discord-shaped — the commandnameandoptions, or the component'scustom_idandvalues— and for components themessagethe component sits on is included.memberin servers,userin DMs.- The
tokenis valid for 15 minutes. It is the credential for the callback and follow-up routes — those routes take noAuthorizationheader.
An interaction that fires while your bot has no live gateway session is dropped; it is not queued for RESUME.
Responding
POST /interactions/{id}/{token}/callback with { "type": <n>, "data": { … } }:
| Type | Name | On Voidcom |
|---|---|---|
| 1 | PONG | answers 204 locally (there is no PING to answer) |
| 4 | CHANNEL_MESSAGE_WITH_SOURCE | posts the response message |
| 5 | DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE | shows "thinking"; post the message later via @original |
| 6 | DEFERRED_UPDATE_MESSAGE | components: acknowledge, edit later |
| 7 | UPDATE_MESSAGE | components: edit the message the component is on |
| 8 | APPLICATION_COMMAND_AUTOCOMPLETE_RESULT | 501, code 50035 |
| 9 | MODAL | 501, code 50035 |
The callback answers 204; with ?with_response=true it answers 200 with Discord's { "interaction", "resource" } object (the resource carries the posted message for types 4 and 7).
data takes content, embeds, components and flags. Ephemeral (flags: 64) applies only on the callback that answers the interaction (type 4, or the first post after a deferral). A follow-up sent once the response exists is a normal message in the channel — there is no ephemeral plain send — so a late "ephemeral" follow-up is public.
The original response and follow-ups
The webhook-token routes use your application id and the interaction token:
| Route | What |
|---|---|
POST /webhooks/{application_id}/{token} | follow-up. While the interaction is still pending or deferred this is the response (type 4); afterwards a plain message as the bot into the interaction's channel |
GET PATCH DELETE /webhooks/{application_id}/{token}/messages/{message_id} | fetch, edit, delete a response — message_id is @original for the initial response or a follow-up's id. A PATCH of @original while deferred posts the response — defer() then edit_original_response() works |
On PATCH, "embeds": [] together with "components": [] clears them; omitting both keys keeps them; sending one embed replaces the whole set (no partial embed merge).
Components
Components ride in components as action rows, Discord's shape:
- Action row (type 1): up to 5 rows per message; a row holds up to 5 buttons or one select menu.
- Button (type 2):
style1–4 need acustom_id(≤ 100 chars) and alabel(≤ 80) or anemoji; style 5 (link) needs aurland nocustom_id. - String select (type 3): 1–25 options (
label/value/description≤ 100 chars each),placeholder≤ 150,min_values/max_valueswithin the option count. - User, role, mentionable and channel selects (types 5–8) are accepted as
{ type, custom_id, disabled }only: the client renders them greyed out and they never fire.
Clicks and selections arrive as INTERACTION_CREATE type 3 with data.custom_id (and data.values for selects).
Embeds
Up to 10 embeds per message, 6 000 characters in total across them, whole payload ≤ 32 KB. Per embed:
| Field | Limit |
|---|---|
title | 256 |
description | 4 096 |
fields | 25 · name 256 · value 1 024 |
footer.text | 2 048 |
author.name | 256 |
color | ≤ 0xFFFFFF |
url, timestamp, image, thumbnail and footer.icon_url are accepted. Anything over a limit is rejected with 400 code 50035 and a message naming the offending path (for example embeds[0].fields exceeds 25 entries).
Error codes
| HTTP | code | When |
|---|---|---|
404 | 10062 | unknown interaction: the token expired (15 min), was never issued, or the callback used a different interaction id |
400 | 40060 | the interaction was already acknowledged |
400 | 50035 | invalid payload — embed / component limits, missing custom_id, bad button style |
501 | 50035 | callback type 8 or 9 |