# Slash commands

> Register a command, receive it, and answer the person who ran it.

A slash command is something a person types in the message box, such as `/deploy staging`. Amedeus
sends it to your app, and your app answers. You register commands under _Manage → Installed apps →
the app → Slash commands_; the app needs the `commands` permission.

## Registering a command

| Field             | Rules                                                                                |
| ----------------- | ------------------------------------------------------------------------------------ |
| Command           | 1 to 32 letters, numbers, dashes or underscores. `/deploy` and `deploy` are the same |
| Request URL       | Where the command is sent. Blank uses the app's events URL                           |
| Short description | Up to 200 characters, shown in the suggestions                                       |
| Usage hint        | Up to 100 characters, such as `[environment]`. Only a hint: it is not validated      |

A command name is unique in a workspace, and an app can have up to 20 commands. These names belong to
Amedeus: `help`, `me`, `shrug`, `invite`, `remind`, `topic`, `join`, `leave`, `mute`, `call`, `status`.

## What your app receives

An HTTPS `POST` with a JSON body, signed like an [event](/guides/events/#verifying-a-request)
(`X-Amedeus-Event-Type: slash_command`):

```json
{
  "type": "slash_command",
  "trigger_id": "0f3c…",
  "workspace_id": "80f1d0c5-9e89-4657-88e0-754997a08bfa",
  "app_id": "2362dcb0-4f20-4fc7-b1a6-9cd06923679d",
  "bot_user_id": "e83f53c0-f56b-42a5-89e4-f18b5c9a31f4",
  "command": "/deploy",
  "text": "staging",
  "channel": "76f5b2fa-4ae3-4e55-a15e-40301f7fcfa7",
  "channel_kind": "public",
  "user": "7118b1bc-22c0-4bdd-98bd-2014b89b0e66",
  "created_at": "2026-11-12T19:23:46.122Z",
  "response_url": "https://app.example.com/api/public/apps/responses/9c1f…"
}
```

| Field          | Meaning                                                                             |
| -------------- | ----------------------------------------------------------------------------------- |
| `command`      | The command, with its slash                                                         |
| `text`         | What the person typed after it, up to 4000 characters. Amedeus doesn't interpret it |
| `channel`      | Where it was run                                                                    |
| `channel_kind` | `public`, `private` or `dm`                                                         |
| `user`         | Who ran it                                                                          |
| `trigger_id`   | Unique per run                                                                      |
| `response_url` | Where to answer (below)                                                             |

The command is delivered **once, with no retries**. The app must belong to the conversation where it
runs, and in a direct message the app must accept messages (_App Home_).

**Answer `200` within 3 seconds**, then do the work. The reply is a separate call.

## Answering: the response URL

`response_url` is a temporary secret URL. It needs no token, works for **30 minutes** and **5 times**, and
is tied to the person who ran the command and the channel where they ran it. Treat it as a secret.

**curl**

```sh
curl -X POST "$RESPONSE_URL" \
  -H 'Content-Type: application/json' \
  -d '{"text": "**Deploy queued** to staging. I'll tell you when it's done."}'
```

**JavaScript**

```js
await fetch(process.env.RESPONSE_URL, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ text: "**Deploy queued** to staging. I'll tell you when it's done." }),
});
```

**Python**

```python

request = urllib.request.Request(
    os.environ["RESPONSE_URL"],
    data=json.dumps({"text": "**Deploy queued** to staging. I'll tell you when it's done."}).encode(),
    headers={"Content-Type": "application/json", "User-Agent": "my-app/1.0"},
    method="POST",
)
urllib.request.urlopen(request).read()
```

| Field           | Meaning                                                                                                                                   |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `text`          | Required. Markdown, up to 8000 characters                                                                                                 |
| `response_type` | `ephemeral` (default): only the person who ran the command can read it. `in_channel`: a message from the app in the channel, for everyone |

Success is `{ "ok": true }`. Errors use the [usual format](/reference/errors/); the ones specific to this URL are
`response_url_invalid` (unknown or expired, `404`) and `response_url_exhausted` (used 5 times, `429`).

A private reply is the right place for "you're not allowed to do that" or "deploy queued". Use
`in_channel` (or `POST /messages`) when the whole channel should see it.

## What the person sees

Only they can read these, kept for 7 days:

- Right after running the command: "**/deploy** sent to _App_." Nothing else happens in the channel: the
command is not a message.
- If the delivery fails (the URL answered an error, timed out, couldn't be reached, or the app can't
receive requests): "/deploy didn't reach _App_: …", with the reason. The raw technical error is never
shown.
- Your private replies, as they arrive.

