Skip to content

Developer docs

Slash commands and interactive components

Offer slash commands, buttons, select menus, forms and private replies, and answer them in time.

A bot with the commands scope, in a hub that granted it, can offer slash commands, message and user commands, and put buttons and menus on its messages. Each use reaches the bot as INTERACTION_CREATE over the gateway, or as a signed request to its own HTTPS address. A bot that takes interactions over the gateway must be connected; when it is not, people see at once that it is offline.

Register commands

PUT /api/developer/v1/applications/@me/commands (global) or PUT /api/developer/v1/hubs/:id/commands (one hub) replaces that list. Up to 100 commands of each, 25 options per command and 25 choices per option. Names use a to z, 0 to 9, _ and -, up to 32 characters. A hub command replaces a global one with the same name in that hub.

A command
{
  "name": "roll",
  "description": "Roll some dice",
  "options": [
    {
      "type": "integer",
      "name": "sides",
      "description": "How many sides",
      "required": true,
      "minValue": 2,
      "maxValue": 100
    },
    {
      "type": "string",
      "name": "style",
      "description": "How to show it",
      "choices": [
        {
          "name": "Plain",
          "value": "plain"
        }
      ]
    },
    {
      "type": "string",
      "name": "game",
      "description": "Which game",
      "autocomplete": true
    }
  ]
}

Option types are string, integer, number, boolean, user, channel, role, mentionable and attachment. Required options come first, and an option has choices or autocomplete, not both. See Command for every field.

A command with type: "message" or type: "user" is an item in the Apps part of a message's menu or a person's profile, not a slash command. It has a name people read ("Report message", up to 32 characters, capitals and spaces allowed), no description and no options. A bot has up to 5 of each type. Its interaction is a command whose data has type, targetId, and the message (id, text, author, time) or the user (and in a hub their member role and community roles) that was picked. The message must be one the person can read in that channel.

Files

An attachment option asks the person for a file. It goes through the same upload as the message box, with the same types, size and storage limits, and the option's value is { id, name, contentType, size, url, expiresAt }. The url is a path on the API host that serves the file without a token for 10 minutes: download it then if you keep it.

Direct messages

A global command opens itself to direct messages with contexts: ["hub"] (the default), "dm" and "group_dm". It shows in a conversation the bot takes part in, and in any conversation of a person who added the bot to their account (the install screen offers that once a bot has such a command; Settings, Connections, Connected apps lists and removes them). There the interaction has hubId: null and context: "dm" or "group_dm", no member, and options for people take people in the conversation (channels and roles are refused). When the bot is not in the conversation, every answer is private to the person who used the command.

Where and for whom

Hub owners and admins choose, per command, in Manage, Bots: who may use it (everyone who can post, owner and admins, or off), and rules by community role and by channel. A denied role or channel wins; a list of allowed ones admits only those. Role rules never apply to the hub's owner and admins. A command a person may not use is left out of their list and refused if used anyway, before the bot hears anything.

INTERACTION_CREATE

JSON
{
  "id": "7f8e9d0c-1b2a-4c3d-9e4f-5a6b7c8d9e0f",
  "type": "command",
  "applicationId": "c4d5e6f7-a8b9-4c0d-8e1f-2a3b4c5d6e7f",
  "hubId": "6f1c2b9a-3d4e-4f50-8a61-7b8c9d0e1f20",
  "channelId": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d",
  "user": {
    "id": "5d6e7f80-9a1b-4c2d-9e3f-4a5b6c7d8e9f",
    "handle": "river",
    "displayName": "River"
  },
  "member": {
    "role": "member"
  },
  "data": {
    "id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
    "name": "roll",
    "options": [
      {
        "name": "sides",
        "type": "integer",
        "value": 20
      }
    ]
  },
  "createdAt": "2026-10-11T16:00:00.000Z",
  "answerBy": "2026-10-11T16:00:03.000Z"
}
  • command: someone used a slash command. data has the command and its options.
  • autocomplete: someone is typing in an autocomplete option; the option being typed has focused: true.
  • component: someone pressed a button or picked from a menu. data is { customId, componentType, values } and message says which message.
  • modal_submit: someone sent a form. data is { customId, fields: [{ customId, value }] }.

Answer within 3 seconds

POST /api/developer/v1/interactions/:id/callback once, within 3 seconds. The person sees the answer right away. A late answer gets BOT_INTERACTION_EXPIRED.

typeForDoes
replycommand, component, formPosts a message, public or only for that person (ephemeral: true)
defercommand, component, formShows that person a thinking note until a follow-up
updatecomponent, formChanges the message the control was on
defer_updatecomponent, formAnswers with no visible change
modalcommand, componentOpens a form with up to 5 fields
autocompleteautocompleteSuggests up to 25 choices

After the first answer you have 15 minutes to send follow-ups (POST .../followups) and edit the first answer (PATCH .../original). Private replies show "Only you can see this" and are gone after 60 minutes.

Embeds and components

Messages, answers, follow-ups and edits can carry up to 5 embeds (cards of text, 6,000 characters in all) and up to 5 rows of components: each row holds up to 5 buttons or one select menu.

Buttons and a menu
{
  "content": "Pick a game for tonight.",
  "components": [
    {
      "type": "row",
      "components": [
        {
          "type": "button",
          "style": "primary",
          "label": "Join",
          "customId": "join"
        },
        {
          "type": "button",
          "style": "link",
          "label": "Rules",
          "url": "https://example.com/rules"
        }
      ]
    },
    {
      "type": "row",
      "components": [
        {
          "type": "select",
          "customId": "game",
          "placeholder": "Choose a game",
          "options": [
            {
              "label": "Chess",
              "value": "chess"
            },
            {
              "label": "Go",
              "value": "go"
            }
          ]
        }
      ]
    }
  ]
}

Link buttons open an https address and never reach the bot. Pictures from other sites are not supported in embeds on purpose: loading them would tell those sites who is reading.

What Hubs checks for you

  • The person can open and post in the channel, and the bot can open it.
  • Options match the command: required ones present, types, ranges, lengths and choices; people and channels in options belong to this hub.
  • A button or menu press names a control that is really on that message, from your bot, and enabled. Menu values are the menu's own options.
  • A form answer comes from the person the form was shown to.
  • Each person can start 30 interactions a minute.

Interactions over HTTPS

Instead of a gateway connection, a bot can take interactions at its own address. Set it in the developer portal, Install tab, Interactions endpoint. Hubs then sends each interaction as a POST with the same JSON as INTERACTION_CREATE and reads your first answer from the response.

  • X-Signature-Timestamp is Unix seconds and X-Signature-Ed25519 is the hex Ed25519 signature of the timestamp followed by the raw body, made with your application's own key. Check it with the public key the portal shows, and answer 401 when it does not match.
  • Refuse a timestamp more than 5 minutes away from your clock, and an id you already handled, so a captured request cannot be played again.
  • Answer with status 200 and the body you would send to the callback ({ "type": "reply", "data": { ... } }, defer, update, modal, autocomplete) within 3 seconds. Anything else tells the person the bot did not answer. Follow-ups and edits use the REST routes with your bot token.
  • Saving the address checks it: Hubs sends a signed { "type": "ping" } and wants { "type": "pong" } back, then sends one with a wrong signature and wants a 401.
  • The address must be public HTTPS on the standard port. Hubs never follows a redirect and never sends to a private network address.