Ship a Slack agent on Dairo: register a Slack app, hand out install links, receive mentions and DMs as messages, and reply with one send call.
Give your product a Slack presence. Your customers install your Slack app into their own workspaces, their @mentions and DMs arrive as messages in your Dairo mailbox, and your agent replies with the same send call it uses for email. Dairo carries the messages; every reply is written by your code.
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 creates it for you from a Slack app-configuration token (apps made outside Dairo cannot be connected).
- 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.
There is no built-in responder anywhere in this path. Dairo stores what arrives and delivers what you send — nothing goes out until your code calls the send endpoint, and a send that names no conversation is rejected rather than guessed.
Register your Slack app
POST /v1/slack/apps registers the app whose name and icon your customers
see. Pass 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, and
the app's secrets are encrypted at rest and never returned by any read.
The response is the app's public identity — never its secrets. Connecting an app you created yourself in Slack is not supported: Slack offers no way to prove that the account importing an app owns it, so every app is created by Dairo.
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.
An ambient app carries an ambientMode, settable with
PATCH /v1/slack/apps/{appId} (its only mutable field):
ambientMode | Behavior |
|---|---|
"off" (default) | Channel chatter that does not @mention the bot is discarded. |
"store" | That chatter is stored in the mailbox (with isMention: false) for later reading, without firing message.received. |
@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
one channel: "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(codevalidation_failed, with a message that starts withslack_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
with channel: "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:
Everything your agent needs to reply is in channelMetadata:
| Field | What it is |
|---|---|
teamId | The Slack workspace the message came from — T…. |
channelId | The conversation to reply into — C… public, D… DM, G… private. |
ts | The message's own timestamp; the thread root when a reply threads on it. |
threadTs | The thread the message belongs to, when it arrived inside one; otherwise null. |
slackUserId | Who sent it — U…. Also surfaced as from.address (slack:U456DEF). |
eventId | Slack's delivery id, for your own dedup if you want it. |
isMention | true for an @mention in a channel; false for a DM. |
channelType | "dm", "channel", or "private_channel" — who can see the conversation. |
Slack ids are case-sensitive. Store and echo them back exactly as received — a lowercased id targets the wrong conversation, or none.
Branch on channelType before answering. "dm" is a private 1:1 —
account-specific details are fine. "channel" and "private_channel" are
rooms where other people are watching: do not disclose account data, balances,
or anything you would not post publicly. Answer briefly, or offer to continue
in a DM. Dairo gives you the fact; the judgment is your agent's.
Reply
Reply with the same POST /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 exactly one of these targets in to — a second recipient, or any cc or
bcc, is a 400 (code invalid_request, with a message that starts with
slack_single_recipient), because a Slack send posts one message
into one conversation:
to value | Where it posts |
|---|---|
slack:{channelId} | Into that conversation, unthreaded. |
slack:{channelId}:{threadTs} | Into that conversation, in the given thread. Recommended for replies. |
slack:{userId} | Into that user's latest conversation with the bot — a channel mention is answered in its thread, a DM is answered flat. |
To keep a channel reply attached to the question, echo back
slack:{channelId}:{threadTs ?? ts} from the inbound message's
channelMetadata.
The response comes back with 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(codeinvalid_request, with a message that starts withslack_target_required). slack:{userId}for a user who has never messaged the bot —422; address thechannelIdexplicitly instead.- The workspace uninstalled the app, or the app was deleted —
422(codevalidation_failed, with a message that starts withslack_disconnected).
From webhook to reply
The whole loop: a message.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.
Behavior and limits
- Text only, for now. Attachments, scheduled sends (
sendAt), inline buttons, and Telegram-only payloads return a400on the Slack channel. Anhtmlbody is flattened to plain text — Slack renders its own formatting, not HTML. - Plan limit. Each workspace binding counts against your plan's inbox limit;
an install over the limit is refused, and the install callback reports
plan_limit_reached. - 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 and 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.