Skip to documentation
Documentation

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:

WorkspaceAgent
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:

HTTP
POST /api/v1/developer/slack-apps
Authorization: Bearer <key>
Content-Type: application/json

{
  "bot_token": "xoxb-…",
  "signing_secret": "…",
  "client_id": "…",
  "client_secret": "…"
}
JSON
{
  "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

HTTP
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.

Customers rarely know their team ID. Make a link for their agent instead:

HTTP
GET /api/v1/developer/slack-apps/A0123/install-link?external_ref=acme&team_id=T0AAA
Authorization: Bearer <key>
JSON
{ "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_id set): 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:

JSON
{
  "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:

JSON
{
  "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:

HTTP
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:

typeWhenExtra fields
slack.installedA 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.uninstalledSlack reports app_uninstalled for a workspace.teamId
slack.unbound_messageSomeone 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
JSON
{
  "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": "…" }. GET on …/operators lists them and DELETE removes 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

StatusWhen
400Malformed team_id or slack_user_id, or not exactly one of agent_id / external_ref.
402plan_required: sandbox organizations can't register apps, bind or make links.
403The key is scoped to one agent, or lacks deployments:write (reads accept deployments:read).
404The app, agent, binding or operator isn't in your organization.
409The app has no OAuth client ID and secret, so it can't make install links.