One Slack app for many customer workspaces
Ship your own branded Slack app and route each customer's workspace to that customer's agent.
If you deploy an agent for each of your customers through partner deployments, you can give all of them one Slack app carrying your name and logo. Each customer installs that app into their own workspace, and Keiki sends every message from a workspace to the agent bound to it. Customer A's workspace talks to customer A's agent, and B's to B's, all through one app.
You register the app once, then bind each workspace to an agent: by its team ID if you know it, or through an install link made for that agent.
How routing works
A shared app has no agent of its own. Each workspace that installs it gets a binding from its Slack team ID to one of your organization's agents:
| Workspace | Agent |
|---|---|
T0AAA (Acme) | agt_acme |
T0BBB (Globex) | agt_globex |
T0CCC (just installed) | none yet |
Every Slack conversation is keyed by app, workspace, channel and thread, so two workspaces never share history, sandbox files or approval actions. A binding can only point at an agent in your own organization.
Someone writing in a workspace that has no agent yet gets a reply only they can see:
This workspace isn't connected to an agent yet. Ask whoever gave you this app to finish setting it up.
The install is still recorded, and last_unbound_event_at on the workspace shows when
someone last ran into this.
1. Create and register the app
Create a Slack app from the manifest on the dashboard's Slack channel page, then turn on
Manage Distribution → Activate Public Distribution in Slack's app settings so other
workspaces can install it. Install it in your own workspace and register it with an
org-wide key with the deployments:write scope:
POST /api/v1/developer/slack-apps
Authorization: Bearer <key>
Content-Type: application/json
{
"bot_token": "xoxb-…",
"signing_secret": "…",
"client_id": "…",
"client_secret": "…"
}{
"app": {
"id": "gw_…",
"app_id": "A0123",
"install_url": "https://…/channels/slack/install/gw_…",
"workspace": { "team_id": "T0HOME", "name": "Your company" }
}
}You get 201 the first time and 200 if the app is already registered. The bot token
comes from your own workspace, which becomes the app's home workspace
(home_team_id). That workspace is recorded as an install but has no agent until you
bind one. Every route below takes either id or the Slack app_id in :id.
2. Bind workspaces to agents
When you know the team ID
PUT /api/v1/developer/slack-apps/A0123/teams/T0AAA
Authorization: Bearer <key>
Content-Type: application/json
{ "external_ref": "acme" }Send exactly one of agent_id or external_ref, the ref you used when deploying the
agent. You can bind a workspace before it installs the app; its messages reach the agent
as soon as it does. Binding again with the same agent changes nothing. Binding to a
different agent moves the workspace: later messages go to the new agent, and pending
approval actions and tracked threads from the old agent are retired.
DELETE /api/v1/developer/slack-apps/:id/teams/:team_id removes the binding. The
workspace stays installed and gets the "not connected yet" reply.
With an install link
Customers rarely know their team ID. Make a link for their agent instead:
GET /api/v1/developer/slack-apps/A0123/install-link?external_ref=acme&team_id=T0AAA
Authorization: Bearer <key>{ "url": "https://…/channels/slack/install/gw_…?agent=agt_acme&team=T0AAA&sig=…" }Send the URL to the customer. It opens Slack's install screen, and when the install
finishes, the workspace is bound to that agent. team_id is optional:
- Pinned (
team_idset): Slack opens that workspace, and the link binds nothing else. - Unpinned: the customer picks any workspace. Anyone holding the link can connect their own workspace to that agent, so pin it when you know the team ID.
Links are signed and do not expire. Rotating the app's signing secret revokes them all.
A link never takes over a workspace that is already bound to an agent; use PUT to move
one.
From a deployment
A deployment can bind the workspace in the same call that creates the agent:
{
"external_ref": "acme",
"agent": { "template": "support" },
"channels": [{ "type": "slack", "app": "A0123", "team_id": "T0AAA" }]
}Leave out team_id and the response includes an install link for the new agent
instead. app can't be combined with bot_token or signing_secret, and doesn't take
ref.
See what's installed
GET /api/v1/developer/slack-apps/:id/teams lists every workspace that installed the app
or has a binding:
{
"teams": [
{
"team_id": "T0AAA",
"agent_id": "agt_acme",
"external_ref": "acme",
"installed": true,
"workspace_name": "Acme",
"installed_at": "2026-10-26T18:00:00.000Z",
"last_unbound_event_at": null,
"bound_at": "2026-10-26T18:01:00.000Z"
}
]
}GET /api/v1/developer/slack-apps lists your shared apps with home_team_id and the
number of installed workspaces.
Manage workspaces from the dashboard
On the dashboard's Slack channel page, open Workspaces on the shared app. The table
shows the same workspaces as GET …/teams. Each row has the workspace name and team ID,
whether the app is installed there, the agent it's bound to, and Last unanswered: the
last time someone wrote there while it had no agent.
Organization owners and admins can:
- choose another agent in a row to move that workspace, or Unbind it;
- Revoke an install. This disconnects the app from that workspace but keeps the binding, so a reinstall goes back to the same agent;
- bind a workspace that hasn't installed yet, under Bind a workspace by team ID;
- make a link under Install link for an agent, optionally pinned to one team ID, and copy it with Copy link.
Members see the table but can't change it. If an install lacks permissions the app now asks for, its row lists them with a link to reinstall.
The dashboard follows the same rules as the API: a sandbox organization gets
plan_required when it binds a workspace or makes a link, and a binding can only point
at an agent in your organization. The same actions are tools on Keiki's MCP server:
get_identities_slack_teams, put_identities_slack_teams_by_teamId,
delete_identities_slack_teams_by_teamId and post_identities_slack_install_link.
Workspace events
Keiki can tell your system when a workspace installs the app, uninstalls it, or writes to it with no agent bound. Point the app at one of your organization's saved webhooks:
PUT /api/v1/developer/slack-apps/A0123/events-webhook
Authorization: Bearer <key>
Content-Type: application/json
{ "webhook_id": "wh_…" }Send null to stop events. The webhook must belong to your organization, and
GET /api/v1/developer/slack-apps/:id returns the current one as events_webhook_id. On
the dashboard, the same setting is Workspace events in the app's Workspaces
dialog, and the MCP tool is put_identities_slack_events_webhook. A webhook that a Slack
app sends events to can't be deleted (409) until you choose another one or stop events.
Each event is a JSON POST:
type | When | Extra fields |
|---|---|---|
slack.installed | A workspace finishes installing the app, including a reinstall. If the install came from an install link, it fires after the workspace is bound. | teamId, enterpriseId, workspaceName |
slack.uninstalled | Slack reports app_uninstalled for a workspace. | teamId |
slack.unbound_message | Someone writes to the app in a workspace with no agent bound. Sent at most once an hour per workspace; last_unbound_event_at still updates on every message. | teamId, channelId, userId |
{
"type": "slack.unbound_message",
"teamId": "T0CCC",
"channelId": "C0123",
"userId": "U0456",
"orgId": "org_…",
"slackApp": { "id": "gw_…", "appId": "A0123" },
"occurredAt": "2026-10-26T18:05:00.000Z"
}Each event is sent once, with no retry, so treat GET …/teams as the source of truth and
use events to know when to look.
Who approves actions
When an agent asks for approval in Slack, these people can approve inside a customer's workspace:
- that workspace's own Slack admins and owners;
- whoever installed the app there;
- people you add for that workspace:
PUT /api/v1/developer/slack-apps/:id/teams/:team_id/operators/:slack_user_id, with an optional{ "label": "…" }.GETon…/operatorslists them andDELETEremoves one; - your organization's owners and admins, in every workspace, matched by their Slack email.
The operator list for the whole app on the dashboard counts only in your home workspace. Slack member IDs are only unique within a workspace, so a grant from one workspace never carries over to another.
Escalations
On a shared app, an agent's escalations to Slack
must name a team_id (a dedicated single-workspace app can leave it out). It can only be the agent's own bound workspace or your home workspace.
This is checked when the route is saved and again before each handoff is posted, so
moving a workspace to another agent stops the old agent from posting there.
Errors
| Status | When |
|---|---|
400 | Malformed team_id or slack_user_id, or not exactly one of agent_id / external_ref. |
402 | plan_required: sandbox organizations can't register apps, bind or make links. |
403 | The key is scoped to one agent, or lacks deployments:write (reads accept deployments:read). |
404 | The app, agent, binding or operator isn't in your organization. |
409 | The app has no OAuth client ID and secret, so it can't make install links. |
Slack agents
How an agent behaves in Slack — when it answers, who it thinks is talking, how it handles bursts of messages, and how you stop it.
Migrate a bespoke Slack agent
Move a Slack assistant you built yourself onto Keiki — one agent per customer organization, your tools and rules, Keiki's runtime.