How it works
Slack follows the same receive-and-reply model as Telegram, with an OAuth install flow instead of a pasted bot token:- Register a Slack app for your account — Dairo can mint one for you, or you import one you already own.
- Mint an install URL and put it behind the “Add to Slack” button in your product.
- A customer approves the install; their workspace binds to your account as a
channel: "slack"inbox. - @mentions of the bot and DMs to it land in your mailbox and fire the
message.receivedwebhook; your agent replies withPOST /v1/messages.
Register your Slack app
POST /v1/slack/apps registers the app whose name and icon your customers
see, in one of two ways:
- Managed — pass
origin: "managed"and a Slack app-configuration token from your Slack app dashboard. Dairo creates the app for you and wires up its event delivery. The token is used once and never stored. - Bring your own — pass
origin: "byo"with an existing app’sappId,clientId,clientSecret, andsigningSecret. The secrets are encrypted at rest and never returned by any read.
409.
If you prefer to create the app in Slack’s UI yourself,
GET /v1/slack/apps/manifest-template returns a manifest to paste into
Slack’s “Create app from manifest” flow; import the resulting credentials with
origin: "byo" afterward.
Ambient channel reading
contextMode on the create call decides what the app listens to:
"mentions"(default) — @mentions of the bot and DMs to it, nothing else."ambient"— additionally subscribes to the channel message stream.
ambientMode, settable with
PATCH /v1/slack/apps/{appId} (its only mutable field):
@mentions and DMs always notify as usual;
ambientMode only governs the
non-mention channel stream. Setting "store" on a "mentions" app is a
400.
Mint the install URL
POST /v1/slack/oauth/start returns a signed
https://slack.com/oauth/v2/authorize URL. Embed it as your “Add to Slack”
button — the signed state inside it ties the resulting workspace binding to
your account.
oauth/start resolves your account’s single Slack app. With zero or more than
one app registered it returns a 400 naming the explicit form,
POST /v1/slack/apps/{appId}/install-url, which mints the same URL for a
specific app.
The
state in the URL is signed, single-use, and expires after 10 minutes.
Mint a fresh URL per click rather than caching one — a stale link fails the
install.What an install creates
When a customer approves the install, their workspace binds to your account as onechannel: "slack" inbox. From that moment the workspace’s traffic to your
bot is yours to receive and reply to.
- One workspace, one inbox per app. The same owner reinstalling reconnects the existing inbox in place — fresh credentials, same inbox id.
- A workspace already bound to a different Dairo account through the same app is refused; a binding belongs to exactly one account.
- If the workspace later removes the app, the inbox is marked disconnected.
Sends to it return a definitive
422(slack_disconnected) until the app is reinstalled — never a retryable error, because no retry can succeed.
Receive @mentions and DMs
Every @mention of the bot and every DM to it lands in the mailbox as a message withchannel: "slack" and fires the message.received
webhook — the same event email and Telegram fire, so your agent reacts instead
of polling.
Webhook payloads are metadata-first: the event carries the messageId, and
you fetch the full message for the Slack coordinates. GET /v1/messages/{messageId}
returns:
An @mention, fetched by id and trimmed
channelMetadata:
Slack ids are case-sensitive. Store and echo them back exactly as received —
a lowercased id targets the wrong conversation, or none.
Reply
Reply with the samePOST /v1/messages call you use on every channel, pointed
at the Slack inbox. One Slack-specific rule: you must address the conversation
explicitly. A Slack inbox is a whole workspace of conversations, so there is
deliberately no “reply to whoever spoke last” default — that would race one
user’s answer into another user’s DM.
Put one of these targets in to:
To keep a channel reply attached to the question, echo back
slack:{channelId}:{threadTs ?? ts} from the inbound message’s
channelMetadata.
status: "sent" and channel: "slack", and the
send shows up in GET /v1/messages like any other outbound message.
Reply errors are definitive, never guesses:
- No
slack:target into—400,slack_target_required. slack:{userId}for a user who has never messaged the bot —422; address thechannelIdexplicitly instead.- The workspace uninstalled the app —
422,slack_disconnected.
From webhook to reply
The whole loop: amessage.received event arrives, your agent decides what to
say, and the reply lands in the right conversation with the right privacy.
Signature verification is elided here — wire it up as in
Build a webhook receiver.
Node.js
Behavior and limits
- Text only, for now. Attachments and scheduled sends (
sendAt) return a400on the Slack channel. Anhtmlbody is flattened to plain text — Slack renders its own formatting, not HTML. - No notification injection. Relayed content cannot trigger a
<!channel>-style mass notification: Slack control sequences in the body are neutralized at delivery and render as literal text. - No bot loops. Messages authored by bots — including your bot’s own replies echoing back — are never ingested, so two bots cannot ping-pong.
- Threads map to threads. Replies inside a Slack thread share one Dairo
threadId, so a conversation reads as one thread in the mailbox. - Scopes. Reading rides
messages:readand sending ridesmessages:send. Everything under/v1/slack/*— apps, manifest template, install URLs — ridesinboxes:write, the same scope as creating an inbox. - Quota. Each Slack send counts as one message against your monthly
message quota; a send over quota returns a
429before anything posts.
Related
- Channels — the one-inbox, one-message model Slack plugs into.
- Messages & threads — read a mixed-channel mailbox as structured messages.
- Build a support inbox agent — a worked agent over the unified inbox.