> ## 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 onboarding: connect a workspace end to end

> Install the Orbit Slack App on your workspace via OAuth v2, verify signed inbound events, route Events API / slash command / interactivity payloads to webhooks, and pin the channel Orbit posts notifications to.

# Slack Onboarding: Connect a Workspace End to End

This guide walks you from a new Slack App at api.slack.com to a working Slack channel in Orbit: OAuth v2 install, signed inbound delivery, webhook routing, and a pinned notifications channel. It complements the [Slack channel page](/channels/slack), which covers the operator-level configuration and the full error catalog — this guide is the ordered path you follow the first time, and re-run whenever you install or re-verify the App.

<Note>
  This is the **tenant-facing** Slack App — the one your workspace installs to receive Events API, slash commands, and interactivity payloads. It is not the internal ops-alert webhook that posts Orbit's own operational alerts to a Devotel-internal channel; that path is not part of your setup.
</Note>

## 1. What the Slack App does

There are three inbound surfaces and one outbound surface, all tied to the same Slack App:

* **Events API** — Slack POSTs workspace events (`app_mention`, `message`, `message.channels`, `message.im`, `app_home_opened`, `team_join`, `reaction_added`, `link_shared`) to Orbit's inbound endpoint. Every event is persisted and re-ACK'd safely on retry (`duplicate: true`).
* **Slash commands** — the `/orbit ...` command returns an immediate ephemeral response and fans out the payload as a tenant webhook.
* **Interactivity** — Block Kit buttons, shortcuts, and `view_submission` payloads follow the same signed, persisted, fanned-out path.
* **Outbound notifications** — Orbit posts tenant-side events (new conversation, campaign completed, SLA breach, voice-queue SLA warning, and similar notifications) to the channel you pin (see [section 6](#6-pin-the-post-channel)).

All inbound requests are authenticated with the App's signing secret before anything is dispatched — see [section 4](#4-verify-the-signing-secret).

## 2. Create the Slack App at api.slack.com

1. Sign your workspace in to [api.slack.com/apps](https://api.slack.com/apps) and create an app with **Create an app → From scratch**.

2. On the app's **Basic Information** page, copy the **Client ID** and **Client Secret**. You need these on the Orbit operator; see the [Slack channel page — operator setup](/channels/slack).

3. Go to **OAuth & Permissions → Redirect URLs** and add:

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

4. On the same page, request the bot scopes your Slack App will ask for at install. The baseline set Orbit requests by default is:

   ```
   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
   ```

   If your operator has overridden the default scope list for your deployment, use that exact list; otherwise the baseline above covers install, inbound events, slash commands, interactivity, and posting.

## 3. Run the OAuth v2 install

The install is browser-driven and fully round-trips before anything is stored:

1. From the Orbit dashboard, open **Channels → Slack** and click **Connect Workspace**.
2. Orbit mints an HMAC-signed `state` token (10-minute TTL, nonce-based, one use) and redirects your browser to Slack's approve screen.
3. Approve the scopes Slack asks for. Slack redirects back to the callback URL above.
4. Orbit verifies the signed state, resolves your tenant from it, redeems the temporary code, and stores the workspace credentials in your tenant schema.

A successful install returns the workspace identity:

```json theme={null}
{
  "data": {
    "ok": true,
    "team_id": "T012345",
    "team_name": "Acme Corp",
    "bot_user_id": "U06ABC123"
  }
}
```

If the `state` is tampered or older than 10 minutes, the callback returns **401 `INVALID_STATE`** — restart the install from the dashboard. A **503 `SLACK_NOT_CONFIGURED`** means the operator credentials are not set on the deployment; see the [Slack channel page — operator setup](/channels/slack) and retry.

<Note>
  The callback is intentionally public (no session cookie). Do not close the browser redirect flow early or re-run `/callback` by hand — trust is anchored to the signed state round-trip, and re-playing the callback a second time returns the same 401 `INVALID_STATE`.
</Note>

## 4. Verify the signing secret

Orbit verifies every inbound Events API, slash-command, and interactivity request using the App's signing secret. 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          (alias)
```

Verification follows Slack's documented spec: `X-Slack-Signature: v0=<HMAC-SHA256(secret, "v0:" + timestamp + ":" + rawBody)>` with a 5-minute timestamp window. The behavior is **fail-closed** — anything that cannot be authenticated is rejected, and an unchecked signing secret disables the channel entirely rather than accept unsigned events:

| Signing secret | Inbound request                                    | Result                   |
| -------------- | -------------------------------------------------- | ------------------------ |
| Set            | Valid signature, fresh timestamp                   | `200` — processed        |
| Set            | Bad or missing signature, or timestamp > 5 min old | `401` — rejected         |
| Unset          | Any                                                | `503` — channel disabled |

Avoid the 503 branch by confirming the operator credentials are set before enabling the channel — a deployment that leaves the signing secret unset will dispatch `503` to Slack and eventually have the subscription disabled by Slack after repeated non-200s.

Test the channel with a real Slack delivery: slash commands and Events API retries are the fastest to eyeball. A forged signature or a replayed timestamp returns **401 `INVALID_SIGNATURE`**, which is the correct outcome — do not "fix" this in the receiver.

## 5. Route inbound events to webhooks

Events fan out as three webhook event types — subscribe the ones your integration needs and keep one handler for all three:

| Webhook event type     | Fired for                                                                                                                                           | Payload                                                                                                                   |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `slack.event_received` | Events API deliveries (`app_mention`, `message`, `message.channels`, `message.im`, `app_home_opened`, `team_join`, `reaction_added`, `link_shared`) | `slack_event_type`, `slack_team_id`, `slack_channel_id`, `slack_user_id`, `text`, `event_id`, `event_ts`, `raw`           |
| `slack.slash_command`  | `/orbit ...` invocations                                                                                                                            | `slack_team_id`, `slack_channel_id`, `slack_user_id`, `command`, `text`, `response_url`, `trigger_id`                     |
| `slack.interactivity`  | Block Kit buttons, shortcuts, `view_submission`                                                                                                     | `interactivity_type`, `slack_team_id`, `slack_user_id`, `trigger_id`, `actions`, `view_callback_id`, `callback_id`, `raw` |

Two routing notes:

* **Events dedupe** — every Events API delivery is keyed on Slack's `event_id`. A retry within the dedupe window returns `200` with `duplicate: true`, so your webhook handler should treat `slack.event_received` as at-most-once per `event_id`.
* **Slash commands ack immediately** — the ephemeral reply to the user is returned synchronously; the `slack.slash_command` webhook fan-out happens asynchronously, so a slow subscriber never holds the Slack ack open.

For a worked inbound example and the exact verifier math, see the [Slack channel page — inbound request verification](/channels/slack).

## 6. Pin the post channel

Outbound notifications (new conversations, campaign completion, SLA breaches, voice-queue SLA warnings, and similar) post to the channel you pin. Until you pin one, the publishers resolve to `no_channel` and drop the notification silently — the fail-open behavior is deliberate so a Slack misconfiguration never blocks the action behind it.

Pick a channel from the workspace and pin it:

```bash theme={null}
# List the workspace's channels (helps you discover the id)
curl -s -X GET "https://api.orbit.devotel.io/api/v1/integrations/slack/channels" \
  -H "X-API-Key: dv_live_sk_your_key_here"

# Pin the channel Orbit posts notifications to
curl -s -X PATCH "https://api.orbit.devotel.io/api/v1/integrations/slack/notifications-channel" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"channel_id": "C0123456789"}'
```

A successful pin returns the pinned channel and the active connection:

```json theme={null}
{
  "data": {
    "ok": true,
    "notifications_channel_id": "C0123456789"
  }
}
```

The bot must be a member of the pinned channel, or you must have granted `chat:write.public` (in the default scope set) to post to a public channel without a `/invite`. Otherwise Slack returns `channel_not_found` and the post fails. A pinned id that is not a public or private channel (a DM, or a name instead of an id) returns **400 `INVALID_SLACK_CHANNEL_ID`**.

## 7. Troubleshooting

Match the symptom to its cause before touching the App:

| Symptom                                                | Cause                                                                   | Fix                                                                                                     |
| ------------------------------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Install returns `503` from the start route             | Operator client id / secret unset                                       | Have the operator set the Slack App credentials and retry (channel page, operator setup).               |
| Inbound events return `503`                            | Signing secret unset                                                    | Set the signing secret before enabling the channel.                                                     |
| Inbound events return `401`                            | Signing-secret mismatch, or clock skew > 5 min                          | Re-copy the secret from **Basic Information → App Credentials**; verify host clock via NTP.             |
| Callback returns `401 INVALID_STATE`                   | Replayed or expired install state                                       | Restart the install from the dashboard — the state is single-use and 10-minute TTL.                     |
| Slack disabled the subscription                        | Repeated non-200s on the Request URL                                    | Fix the secret, then re-enable Event Subscriptions in the Slack App.                                    |
| Commands and buttons land but nothing fires downstream | You are not subscribed to `slack.slash_command` / `slack.interactivity` | Add the webhook subscriptions; the ingress ack is synchronous, the webhook fan-out is what routes them. |
| Outbound notifications never arrive                    | No pinned `notifications_channel_id`                                    | Call the pin endpoint in section 6.                                                                     |
| Outbound notifications drop after pinning              | Bot not in the channel, no `chat:write.public`                          | Invite the bot into the channel or add the scope and reinstall.                                         |

For anything not covered above — scopes, per-event-type handling, or the full operator variable set — see the [Slack channel page](/channels/slack).
