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
| Capability | Token used |
|---|---|
| List/read public channels | user token, bot token as fallback |
| List/read private channels | user token |
| List/read DMs and group DMs | user token only |
| Search messages | user token only (search:read) |
| Send / edit / delete messages | user 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_messages → GET /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
| Parameter | Required | Description |
|---|---|---|
bearer_token | yes* | Heimdall access token. Can also be sent as the Access-Token header. |
user_email | no | Extracted from the token when omitted. |
return_url | no | Page 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.
| Parameter | Default | Description |
|---|---|---|
limit | 20 | Maximum conversations |
include_direct_messages | true | Include DMs and group DMs |
include_channels | true | Include public/private channels |
unread_only | false | Only conversations with messages |
DMs are only included when a user token is present.
List channels
GET /slack/list-channels
| Parameter | Default | Description |
|---|---|---|
limit | 200 | Maximum channels |
exclude_archived | true | Skip archived channels |
types | public_channel,private_channel | Also accepts mpim, im |
cursor | – | Pagination cursor |
team_id | – | Restrict 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.
| Parameter | Required | Description |
|---|---|---|
channel_id | yes | C… channel, G… private/group, D… DM |
limit | no | Max messages (default 100, capped at 1000) |
oldest | no | Only messages after this point |
latest | no | Only messages before this point |
cursor | no | Pagination cursor from next_cursor |
inclusive | no | Include 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:
| Form | Example |
|---|---|
| Slack timestamp | 1712345678.000200 |
| Message id (a message id is its timestamp) | 1712345678.000200 |
| Epoch seconds | 1712345678 |
| ISO-8601 date or datetime | 2026-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.
| Parameter | Required | Description |
|---|---|---|
channel_id | yes | C… channel, G… private/group, D… DM |
thread_ts | yes | Timestamp (id) of the thread's parent message |
limit | no | Max replies (default 100, capped at 1000) |
oldest / latest | no | Same four accepted forms as above |
cursor | no | Pagination cursor from next_cursor |
inclusive | no | Include 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.
| Parameter | Required | Default | Description |
|---|---|---|---|
query | yes | – | Slack query, supports modifiers |
max_results | no | 20 | Matches per page (max 100) |
page | no | 1 | 1-based page number |
sort | no | timestamp | Or score for relevance |
sort_dir | no | desc | Or 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
| Parameter | Required | Default | Description |
|---|---|---|---|
channel_id | yes | – | Channel or conversation ID |
text | yes | – | Message text |
thread_ts | no | – | Reply in a thread |
blocks | no | – | Block Kit layout |
reply_broadcast | no | false | Also post the reply to the channel |
unfurl_links / unfurl_media | no | – | Link/media previews |
attachments | no | – | Files with base64 data |
send_as | no | user | user 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:
| Code | Meaning |
|---|---|
channel_not_found | Conversation doesn't exist or the token can't see it |
not_in_channel | Bot token used for a channel the bot hasn't joined |
missing_scope | The granted scopes don't cover this call — reconnect |
invalid_auth | Token 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
| Tool | Purpose |
|---|---|
slackchannel:list / slackchannel:read | Channels |
slackmessage:list / slackmessage:read | Read any conversation |
slackmessage:search | Search across channels and DMs |
slackmessage:create | Send (send_as: user | bot) |
slackmessage:update / slackmessage:delete | Edit and remove |
Concept naming also works: SlackMessage.search, SlackMessage.send, etc.
See the MCP API Reference.