> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Slack

> Install Orbit into a tenant Slack workspace via OAuth v2 and receive Events / slash-command / interactivity payloads

# Slack

The Slack channel lets a tenant install Orbit into **their own** Slack workspace as a Slack App. Operators connect the workspace through the OAuth v2 authorize → callback flow; inbound Events API messages, slash commands, and interactivity payloads are POSTed back to Orbit and verified with the App's signing secret.

<Note>
  This is the **tenant-facing** Slack App — distinct from the internal ops-alert webhook (`DEVOTEL_SLACK_OPS_WEBHOOK_URL`), which only posts Orbit's own operational alerts to a Devotel-internal channel.
</Note>

## Operator setup

Configure the Slack App credentials once per deployment, in Secret Manager (or `.env` for local dev). Every variable below is optional at boot; the Slack channel stays disabled until the credentials are set.

| Variable                        | Required          | Purpose                                                                                                                                                                                                                                                                                                                                 |
| ------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEVOTEL_SLACK_CLIENT_ID`       | To enable install | Slack App OAuth v2 client id. Used to build the `https://slack.com/oauth/v2/authorize` URL.                                                                                                                                                                                                                                             |
| `DEVOTEL_SLACK_CLIENT_SECRET`   | To enable install | Slack App OAuth v2 client secret. Used (HTTP Basic, with the client id) to redeem the temporary code at `https://slack.com/api/oauth.v2.access`.                                                                                                                                                                                        |
| `DEVOTEL_SLACK_SIGNING_SECRET`  | To accept inbound | HMAC-SHA256 key that verifies inbound Events / slash-command / interactivity requests. **Fail-closed** (see below).                                                                                                                                                                                                                     |
| `DEVOTEL_SLACK_BOT_SCOPES`      | Optional          | Comma-separated OAuth v2 bot scopes requested at install. Defaults to the baseline set baked into `packages/shared/src/env.ts`; override only if the App adds optional scopes (e.g. `assistant:write`).                                                                                                                                 |
| `DEVOTEL_SLACK_DEFAULT_CHANNEL` | Optional          | **Legacy — not used by this OAuth app.** Belongs to a separate, older internal Slack-forwarding path (override → this variable → `general`). It has **no effect** on the tenant-facing OAuth app documented here, which posts to the channel chosen when the workspace is connected. See [Default post channel](#default-post-channel). |

In your Slack App configuration (api.slack.com/apps), find the client id + secret under **Basic Information → App Credentials**, and the signing secret on the same screen. Set the Slack App's **Redirect URL** to:

```
https://api.orbit.devotel.io/api/v1/integrations/slack/oauth/callback
```

### Default bot scopes

If `DEVOTEL_SLACK_BOT_SCOPES` is unset, Orbit requests the following baseline scopes:

```
chat:write, chat:write.public, commands, app_mentions:read,
channels:history, channels:read, groups:history, groups:read,
im:history, im:write, users:read, users:read.email,
reactions:read, links:read, files:write
```

### Default post channel

When the connected Slack App posts a tenant-side event (new conversation, campaign
completed, SLA breach, voice-queue SLA warning) it resolves the destination channel
in this order:

1. A channel explicitly named on the individual notification, when one is supplied.
2. The channel chosen when the workspace was connected.

There is **no environment-variable or `general` fallback** on this path. If no channel
is configured — or the workspace has no active connection — the App **drops the
notification silently**: it logs at debug level and never throws or fails the action
behind it. This is deliberate: notification delivery is fail-open so that a Slack
misconfiguration can never block the campaign send, conversation create, or SLA
escalation that triggered it. Choose a notifications channel when connecting the
workspace so these events have a place to land.

The bot must be a member of the target channel, or hold the `chat:write.public` scope
(included in the default scope set above) to post to a public channel it has not
joined; otherwise Slack returns `channel_not_found` and the post fails.

<Note>
  `DEVOTEL_SLACK_DEFAULT_CHANNEL` and the `general` fallback belong to a **separate,
  legacy** internal Slack-forwarding path, not to this OAuth app. They have no effect on
  tenant notification delivery through the connected Slack App.
</Note>

## Install flow (OAuth v2)

1. From the dashboard, navigate to **Channels → Slack** and click **Connect Workspace**. This calls the authenticated start route, which mints an HMAC-signed `state` token and redirects the operator's browser to Slack's authorize screen.

   ```
   GET /api/v1/integrations/slack/oauth/start
   ```

2. The operator approves the requested bot scopes in Slack.

3. Slack redirects the browser (a top-level navigation) back to the **public** callback route, which verifies the signed `state`, resolves the tenant from the verified payload, redeems the temporary code, and stores the workspace credentials in the tenant schema.

   ```
   GET /api/v1/integrations/slack/oauth/callback
   ```

<Note>
  The callback is intentionally registered **before** Clerk auth (public scope) because browsers do not reliably carry the session cookie across Slack's cross-site redirect. Trust is anchored on the HMAC-signed `state` round-trip, not on a session cookie.
</Note>

## Inbound request verification

Point your Slack App's **Event Subscriptions**, **Slash Commands**, and **Interactivity** Request URLs at either of the equivalent inbound endpoints:

```
POST /api/v1/integrations/webhooks/slack    (canonical)
POST /api/v1/webhooks/slack/events          (Slack-convention alias)
```

Both mounts share one handler — the same signature gate, tenant resolution, and dispatch. Orbit verifies every inbound request per Slack's [verifying-requests-from-slack](https://api.slack.com/authentication/verifying-requests-from-slack) spec:

```
X-Slack-Signature: v0=<hex>
where  v0  = HMAC-SHA256(DEVOTEL_SLACK_SIGNING_SECRET, "v0:" + X-Slack-Request-Timestamp + ":" + rawBody)
```

The request timestamp (`X-Slack-Request-Timestamp`) must be within **5 minutes** of now (Slack's documented replay-protection window).

### Fail-closed behaviour

Per platform invariant #29 the receiver fails **closed**:

| `DEVOTEL_SLACK_SIGNING_SECRET` | Inbound request                                      | Result                                                  |
| ------------------------------ | ---------------------------------------------------- | ------------------------------------------------------- |
| Set                            | Valid signature, fresh timestamp                     | `200` — processed                                       |
| Set                            | Bad/missing signature, or timestamp older than 5 min | `401` — rejected                                        |
| Unset                          | Any                                                  | `503` — channel disabled (never accept unsigned events) |

So an operator must set the signing secret **before** enabling the Slack channel; otherwise inbound deliveries return `503` and Slack will disable the subscription after repeated non-200s.

## Common errors

| Symptom                             | Cause                                                           | Fix                                                                             |
| ----------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Install returns `503` from `/start` | `DEVOTEL_SLACK_CLIENT_ID` / `DEVOTEL_SLACK_CLIENT_SECRET` unset | Set the App credentials in Secret Manager and redeploy.                         |
| Inbound events return `503`         | `DEVOTEL_SLACK_SIGNING_SECRET` unset                            | Set the signing secret before enabling the channel.                             |
| Inbound events return `401`         | Signing-secret mismatch or stale timestamp (clock skew > 5 min) | Re-copy the signing secret from **Basic Information**; verify host clock (NTP). |
| Slack disabled the subscription     | Repeated non-200s on the Request URL                            | Fix the secret, then re-enable Event Subscriptions in the Slack App.            |
