Skip to main content

Slack API

The Slack integration acts as the authenticated user. OAuth grants a Slack user token (xoxp-…), so Constellation can see every conversation the person can see — public channels, private channels, DMs and group DMs — without a bot being invited anywhere, and can post as them rather than as an app.

An optional bot token (xoxb-…) is also stored when the workspace install grants one. It is only used when you explicitly ask to act as the app.

Overview

CapabilityToken used
List/read public channelsuser token, bot token as fallback
List/read private channelsuser token
List/read DMs and group DMsuser token only
Search messagesuser token only (search:read)
Send / edit / delete messagesuser token by default, bot on request

Slack tokens are long-lived and do not expire unless revoked or unless token rotation is enabled on the Slack app.

Base URL

{CONSTELLATION_URL}/slack

Routes are generated from the service definition, so each method maps to a kebab-case path — list_messagesGET /slack/list-messages.

Authentication

All endpoints require an Access-Token header (the Heimdall access token). Service tokens are resolved from it automatically.

1. Authorize

GET /oauth/slack/authorize

ParameterRequiredDescription
bearer_tokenyes*Heimdall access token. Can also be sent as the Access-Token header.
user_emailnoExtracted from the token when omitted.
return_urlnoPage to return to once the flow completes.

Redirects to Slack's consent screen requesting both bot scopes and user scopes. After approval, Slack redirects to /oauth/slack/callback, which exchanges the code and stores the tokens.

The callback verifies that the Slack account's email matches the email on the Heimdall token, and fails with email_mismatch when they differ.

2. Check status

GET /oauth/slack/status?tenant_id=…&user_email=…

{
"success": true,
"status": "authorized",
"message": "User is authorized",
"has_tokens": true
}

Status values: authorized, needs_oauth, no_access.

3. Revoke

POST /oauth/slack/revoke

Header: Access-Token.

Revokes the bot and user tokens at Slack, clears the connection cache, and deletes the stored credentials.


Conversations

List conversations

GET /slack/list-chats

Channels and DMs unified, most recently active first.

ParameterDefaultDescription
limit20Maximum conversations
include_direct_messagestrueInclude DMs and group DMs
include_channelstrueInclude public/private channels
unread_onlyfalseOnly conversations with messages

DMs are only included when a user token is present.

List channels

GET /slack/list-channels

ParameterDefaultDescription
limit200Maximum channels
exclude_archivedtrueSkip archived channels
typespublic_channel,private_channelAlso accepts mpim, im
cursorPagination cursor
team_idRestrict to a workspace (org-wide tokens)

Get channel

GET /slack/get-channel?channel_id=C123…

List channel members

GET /slack/list-channel-members?channel_id=C123…

Returns user IDs — pair with /slack/get-user to resolve names.


Messages

List messages

GET /slack/list-messages

Works for any conversation — channel, private channel, DM or group DM. The correct token is selected from the conversation ID prefix, so there is no conversation-type parameter to set.

ParameterRequiredDescription
channel_idyesC… channel, G… private/group, D… DM
limitnoMax messages (default 100, capped at 1000)
oldestnoOnly messages after this point
latestnoOnly messages before this point
cursornoPagination cursor from next_cursor
inclusivenoInclude messages on the boundary
curl -H "Access-Token: eyJ..." \
"$CONSTELLATION_URL/slack/list-messages?channel_id=D01ABCDEF&limit=50"

Time bounds

oldest and latest each accept four forms, so there is no need to convert a date by hand:

FormExample
Slack timestamp1712345678.000200
Message id (a message id is its timestamp)1712345678.000200
Epoch seconds1712345678
ISO-8601 date or datetime2026-01-01, 2026-01-01T09:00:00Z

A bare date means midnight UTC, and a datetime without an offset is read as UTC. An unparseable bound returns 400, so a typo never silently widens the result set.

Because a message id doubles as a bound, "around message X" needs no extra endpoint:

# everything before message X (add inclusive=true to include X itself)
curl -H "Access-Token: eyJ..." \
"$CONSTELLATION_URL/slack/list-messages?channel_id=D01ABCDEF&latest=1712345678.000200"

# everything after message X
curl -H "Access-Token: eyJ..." \
"$CONSTELLATION_URL/slack/list-messages?channel_id=D01ABCDEF&oldest=1712345678.000200"

# one calendar day
curl -H "Access-Token: eyJ..." \
"$CONSTELLATION_URL/slack/list-messages?channel_id=C123&oldest=2026-01-01&latest=2026-01-02"

Paging

The response carries has_more and next_cursor. Walk a long history by feeding next_cursor back in as cursor until has_more is false:

{ "messages": [ ... ], "has_more": true, "next_cursor": "dXNlcjpVMDYxTkZUVDI=" }

Get message

GET /slack/get-message?channel_id=…&message_ts=…

List thread replies

GET /slack/list-thread-replies

Takes the same bounds and paging as /slack/list-messages, so a thread longer than limit can be walked rather than silently truncated.

ParameterRequiredDescription
channel_idyesC… channel, G… private/group, D… DM
thread_tsyesTimestamp (id) of the thread's parent message
limitnoMax replies (default 100, capped at 1000)
oldest / latestnoSame four accepted forms as above
cursornoPagination cursor from next_cursor
inclusivenoInclude replies on the boundary

Returns { "messages": [...], "has_more": false, "next_cursor": null }.

Search messages

GET /slack/search-messages

Searches every message the user can see, across channels and DMs. Requires a user token — Slack does not expose search to bots.

ParameterRequiredDefaultDescription
queryyesSlack query, supports modifiers
max_resultsno20Matches per page (max 100)
pageno11-based page number
sortnotimestampOr score for relevance
sort_dirnodescOr asc
curl -H "Access-Token: eyJ..." \
--data-urlencode "query=from:@jane in:#general deploy" \
--get "$CONSTELLATION_URL/slack/search-messages"
{
"matches": [
{
"id": "1734567890.123456",
"provider": "slack",
"channel_id": "C1234567890",
"text": "deploy is done",
"sender": { "id": "U123", "name": "jane" },
"sent_at": "2026-01-15T10:00:00Z"
}
],
"total": 1,
"paging": { "count": 20, "total": 1, "page": 1, "pages": 1 }
}

Send message

POST /slack/send-message

ParameterRequiredDefaultDescription
channel_idyesChannel or conversation ID
textyesMessage text
thread_tsnoReply in a thread
blocksnoBlock Kit layout
reply_broadcastnofalseAlso post the reply to the channel
unfurl_links / unfurl_medianoLink/media previews
attachmentsnoFiles with base64 data
send_asnouseruser posts as the person, bot posts as the app

DMs can only be sent with send_as=user.

curl -X POST -H "Access-Token: eyJ..." -H "Content-Type: application/json" \
-d '{"channel_id":"D01ABCDEF","text":"Hi — sent as me"}' \
"$CONSTELLATION_URL/slack/send-message"

Update message

PATCH /slack/update-message

Requires channel_id, message_ts, text. Optional blocks and send_as.

Slack only lets a message's original author edit it, so send_as must match whichever identity sent it (default user).

Delete message

DELETE /slack/delete-message

Requires channel_id, message_ts. Optional send_as, same authorship rule.


Users

  • GET /slack/list-users?limit=200
  • GET /slack/get-user?user_id=U123…
  • GET /slack/get-connection-info — authenticated user ID and team ID

Error handling

Reconnect required

Returned when an action needs the user token but the stored connection has none — for example reading a DM on a workspace connected before user tokens were requested, or a bot-only install.

{
"detail": {
"error": "slack_reconnect_required",
"message": "This action requires acting as your Slack user (direct messages, private channels, sending as you). The stored Slack connection has no user token — reconnect Slack to grant user-level access.",
"system": "slack"
}
}

Status code: 400. Resolution: re-run /oauth/slack/authorize.

The API never silently falls back to the bot token here — doing so would read the wrong conversation or post as the app instead of as the person.

Bot token required

{
"detail": {
"error": "slack_bot_token_required",
"message": "This action requires the Slack app's bot token, but this workspace was connected with a user token only.",
"system": "slack"
}
}

Status code: 400. Raised for send_as=bot on a user-token-only install.

OAuth required

{
"detail": {
"action": "oauth_required",
"message": "User needs to complete OAuth flow for Slack",
"auth_url": "…/oauth/slack/authorize?bearer_token=…",
"system": "slack"
}
}

Status code: 407.

Slack API errors

Passed through as 400 with Slack API error: <code>. Common codes:

CodeMeaning
channel_not_foundConversation doesn't exist or the token can't see it
not_in_channelBot token used for a channel the bot hasn't joined
missing_scopeThe granted scopes don't cover this call — reconnect
invalid_authToken revoked or invalid

Required Slack app scopes

Configure these under OAuth & Permissions in your Slack app.

User Token Scopes

These deliver the user-level access described above.

chat:write channels:history channels:read
groups:history groups:read im:history
im:read im:write mpim:history
mpim:read mpim:write users:read
users:read.email search:read files:read
files:write reactions:read

Bot Token Scopes

Only needed if you want the app to post as itself or to operate without a user.

chat:write channels:history channels:read
groups:history groups:read im:history
im:read mpim:history mpim:read
users:read users:read.email channels:join

Token storage

Credentials are stored in the external auth service:

{
"access_token": "xoxb-…",
"user_access_token": "xoxp-…",
"team_id": "T1234567890",
"bot_user_id": "U1234567890",
"authed_user_id": "U9876543210",
"scopes": ["chat:write", "channels:history"],
"user_scopes": ["search:read", "im:history"],
"expires_at": "2027-01-15T10:00:00Z"
}

On a user-token-only install the user token is mirrored into access_token so the standard credential contract still holds.

Security

  • OAuth state is a JWT with a nonce and a 10-minute expiry (CSRF protection).
  • The callback verifies the Slack account email against the Heimdall token email.
  • Disconnecting revokes both tokens at Slack, not just one.

Rate limits

Slack rate limits are per-token, so user-token calls are limited per person rather than per app. Most Web API methods are Tier 3 (~50 requests/minute); search.messages is Tier 2 (~20/minute). Retries use exponential backoff.


MCP tools

ToolPurpose
slackchannel:list / slackchannel:readChannels
slackmessage:list / slackmessage:readRead any conversation
slackmessage:searchSearch across channels and DMs
slackmessage:createSend (send_as: user | bot)
slackmessage:update / slackmessage:deleteEdit and remove

Concept naming also works: SlackMessage.search, SlackMessage.send, etc. See the MCP API Reference.