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.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.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.
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:
Default bot scopes
IfDEVOTEL_SLACK_BOT_SCOPES is unset, Orbit requests the following baseline scopes:
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:- A channel explicitly named on the individual notification, when one is supplied.
- The channel chosen when the workspace was connected.
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.
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.Install flow (OAuth v2)
-
From the dashboard, navigate to Channels → Slack and click Connect Workspace. This calls the authenticated start route, which mints an HMAC-signed
statetoken and redirects the operator’s browser to Slack’s authorize screen. - The operator approves the requested bot scopes in Slack.
-
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.
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.Worked example — start the install (curl)
The start route requires an authenticated session (Clerk cookie) or a valid API key. A cold request with no session returns 401:authorize_url body field is the Slack authorize URL — the browser is redirected there on the dashboard click (this route returns JSON so the dashboard can surface a proper error rather than a bare redirect). The state query parameter has the form:
orgId is the tenant’s Orbit org; ts enforces a 10-minute TTL on the round-trip; nonce prevents replay. The callback rejects a tampered or expired state with 401 INVALID_STATE (see the errors table below).
A continued install flow is purely browser-side: the operator clicks the authorize_url, approves in Slack, and Slack’s redirect hits GET /api/v1/integrations/slack/oauth/callback?code=…&state=…. There is no need to call /callback directly from your own code — it is part of the OAuth redirect.
Worked example — callback (browser round-trip)
After the operator approves:SLACK_NOT_CONFIGURED means DEVOTEL_SLACK_SIGNING_SECRET (or the client id/secret pair) is not set on the deployment — see the Common errors table.
Inbound request verification
Point your Slack App’s Event Subscriptions, Slash Commands, and Interactivity Request URLs at either of the equivalent inbound endpoints:X-Slack-Request-Timestamp) must be within 5 minutes of now (Slack’s documented replay-protection window).
Worked example — Events API inbound (curl)
Slack’s Events API POSTs the raw JSON body to the Request URL. TheX-Slack-Signature / X-Slack-Request-Timestamp headers authenticate the request as coming from Slack; the body is whatever the App subscribed to.
v0 is HMAC-SHA256 of v0:${TS}:${BODY} keyed on the Slack App’s signing secret — Slack computes it per delivery. During development you can replay a captured delivery with curl only within the 5-minute window Slack’s own spec allows; after that, re-derive the signature or let Slack retry.
200 OK — a connected workspace, event persisted + dispatched:
duplicate: true when Slack retries a previously-acked event (Slack retries up to 3 times on non-200; Orbit deduplicates on event_id).
401 Unauthorized — forged or stale signature:
DEVOTEL_SLACK_SIGNING_SECRET is unset (fail-closed; never accept unsigned events):
Node.js SDK (optional)
Slack is not on the SDK’s resource list — use the low-levelrequest() escape hatch to mint an authorize URL or check the integration status.
request() helper handles API-key attach, JSON serialisation, idempotency, 429/5xx retry, and OrbitApiError non-2xx handling — same as every SDK resource.
Fail-closed behaviour
The receiver fails closed — it will not accept inbound events it cannot authenticate:
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.