Slash commands and interactions
Register commands, receive an interaction when someone uses one, and answer it in public, privately, or after some thinking time.
On this page
How it works
- Your bot registers commands with names, descriptions and options.
- When someone types
/in a channel, Palloo lists the commands of the bots in that server that can see the channel. People need Use Commands there, which@everyonehas by default. - When they use one, Palloo checks the options and sends your bot an
interaction_createevent on the gateway. Interactions are delivered only over the gateway, so your bot must be connected to receive them. - Your bot answers within 3 seconds, with a message or by deferring. It can then complete a deferred answer and send follow-ups for 15 minutes.
Registering commands
A command is global (usable in every server the bot is in) or belongs to one server the bot is in. Register with the bot's token:
curl -sS -X POST https://api.palloo.app/v1/bots/@me/commands \
-H "Authorization: Bearer $PALLOO_BOT_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "roll",
"description": "Roll a die",
"options": [
{"name": "sides", "description": "How many sides", "type": "integer", "required": false, "min_value": 2, "max_value": 100}
]
}'
For one server, post the same body to /bots/@me/communities/{id}/commands.
Registering a name that already exists in the same scope replaces that command and answers 200; a new name answers 201. There is no bulk overwrite: register each command, and delete the ones you no longer want. There is no gateway event for command changes; the app reads the list when someone starts typing /.
| Rule | Limit |
|---|---|
| Command and option names | ^[a-z0-9_-]{1,32}$, unique within the command |
| Descriptions | 1 to 100 characters |
| Options per command | 25; required options come before optional ones |
| Option types | string, integer, boolean, user, channel, role |
| Option constraints | min_length and max_length (1 to 4000) for strings, min_value and max_value for integers |
| Commands per bot | 100 global, and 100 in each server |
Each option needs name, description, type and required. A server's commands are removed when the bot leaves it; global commands when the bot is deleted. An invalid command answers 400 invalid_command, too many 409 command_limit_reached. See the Commands and interactions reference.
Receiving an interaction
The interaction_create event reaches only the command's bot:
{
"op": "dispatch",
"t": "interaction_create",
"d": {
"id": "01JA2B3C4D5E6F7G8H9J0K1M2Q",
"token": "palloo_int_...",
"community_id": "01J9ZQ3V5K8M2N4P6R8T0W2Y4M",
"channel_id": "01J9ZQ3V5K8M2N4P6R8T0W2Y4N",
"command": {"id": "01JA2B3C4D5E6F7G8H9J0K1M2C", "name": "roll"},
"user": {"id": "01J9ZQ3V5K8M2N4P6R8T0W2Y4A", "username": "alice", "display_name": "Alice", "kind": "user"},
"options": [{"name": "sides", "type": "integer", "value": 20}],
"resolved": {"users": [], "channels": [], "roles": []},
"created_at": "2026-10-15T09:00:00Z",
"respond_by": "2026-10-15T09:00:03Z",
"expires_at": "2026-10-15T09:15:00Z"
}
}
Options have already been checked against your command: known names, each given once, every required one present, the right type, your constraints, and references (a user is a member, a channel is in the server and visible to the person, a role is in the server). User, channel and role options carry an ID in value, and resolved holds the matching users, channels and roles. Options someone left out are missing from options.
Answering
Every answer uses the bot's own token and the interaction's id and token. A wrong token, or an interaction of another bot, answers 404 interaction_not_found.
Answer at once
POST /interactions/{id}/{token}/callback, once, before respond_by:
{"type": "message", "content": "You rolled a 17."}
The answer is 204. Everyone who can see the channel receives the message, which carries interaction: {"id", "name", "user"} so the app can show who used which command.
Answer privately
Add "ephemeral": true and only the person who used the command sees the answer:
{"type": "message", "content": "Only you can see this.", "ephemeral": true}
Ephemeral messages are never stored. They reach only the connections that person has open at that moment, history never returns them, and they are gone after a reload or on another device.
Defer, then complete
If you need more than 3 seconds, defer first. The person sees that your bot is thinking:
{"type": "deferred", "ephemeral": true}
Then, within 15 minutes, complete it with PATCH /interactions/{id}/{token}/response:
{"content": "Here is your report."}
The completed message is ephemeral when the deferral was. A deferral that is not completed by expires_at fails with reason expired.
Follow up
After the first answer you can send more messages for 15 minutes with POST /interactions/{id}/{token}/followups, each {"content", "ephemeral"}.
In code
Inside the gateway handler from the gateway guide:
const api = 'https://api.palloo.app/v1';
async function answer(interaction, body) {
const response = await fetch(`${api}/interactions/${interaction.id}/${interaction.token}/callback`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!response.ok) console.error('Answer failed', response.status, (await response.json()).error.code);
}
// in the message listener:
if (frame.op === 'dispatch' && frame.t === 'interaction_create' && frame.d.command.name === 'roll') {
const sides = frame.d.options.find((option) => option.name === 'sides')?.value ?? 6;
answer(frame.d, { type: 'message', content: `You rolled a ${1 + Math.floor(Math.random() * sides)}.` });
}
When things go wrong
| Answer | Why |
|---|---|
409 interaction_already_acknowledged |
You already answered. Use a follow-up instead. |
409 interaction_expired |
The 3 seconds (or, for completing and follow-ups, the 15 minutes) have passed. |
409 interaction_not_deferred |
You tried to complete an interaction that was not deferred, or was already completed. |
409 interaction_not_acknowledged |
You sent a follow-up before answering. |
400 invalid_interaction_response |
type must be message with content, or deferred without it. |
403 missing_permission |
A message everyone sees needs Send Messages for the bot in that channel. Ephemeral answers need only View Channel. |
403 bot_required |
A person's token was used. Only bots answer interactions. |
If your bot does not answer by respond_by, the interaction fails and the person's app receives interaction_failure with reason timeout.
Good to know
- Answers never ping
@everyoneor@here, even when the bot could. - Deferral and failure are shown only to the person who used the command. Everyone else sees the answer once it is posted.
- Answers with a message, completions and follow-ups count against the bot's message rate limit (20 at once, then 4 a second). Deferring does not.
- The ephemeral switch is the
ephemeralfield. Discord'sflags: 64is not accepted: unknown fields are refused with400 invalid_body. - There are no buttons, select menus, modals, autocomplete or context menu commands yet.