clawskills-mcp 0.1.4 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +8 -7
- package/dist/index.test.js +48 -0
- package/package.json +1 -1
- package/skills/INDEX.md +19 -1
- package/skills/figma/skill.md +2 -2
- package/skills/slack/skill.md +740 -0
package/dist/index.js
CHANGED
|
@@ -41,13 +41,14 @@ function loadSkills(skillsDir) {
|
|
|
41
41
|
const SECTION_ALIASES = {
|
|
42
42
|
auth: "Authentication & permissions",
|
|
43
43
|
authentication: "Authentication & permissions",
|
|
44
|
-
"rate-limits": "
|
|
45
|
-
"rate_limits": "
|
|
46
|
-
ratelimits: "
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
"rate-limits": "Reliability: rate limits, retries, idempotency",
|
|
45
|
+
"rate_limits": "Reliability: rate limits, retries, idempotency",
|
|
46
|
+
ratelimits: "Reliability: rate limits, retries, idempotency",
|
|
47
|
+
reliability: "Reliability: rate limits, retries, idempotency",
|
|
48
|
+
errors: "Error handling & troubleshooting",
|
|
49
|
+
"error-handling": "Error handling & troubleshooting",
|
|
50
|
+
pagination: "Query patterns & filtering",
|
|
51
|
+
recipes: "Common workflows (recipes)",
|
|
51
52
|
gotchas: "Gotchas",
|
|
52
53
|
webhooks: "Webhooks",
|
|
53
54
|
overview: "Overview",
|
package/dist/index.test.js
CHANGED
|
@@ -2,7 +2,10 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as os from "os";
|
|
4
4
|
import * as path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
5
6
|
import { loadSkills, extractSection, findSkill, searchSkills, skillSummary } from "./index.js";
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const REAL_SKILLS_DIR = path.resolve(__dirname, "../../skills");
|
|
6
9
|
// ---------------------------------------------------------------------------
|
|
7
10
|
// Helpers
|
|
8
11
|
// ---------------------------------------------------------------------------
|
|
@@ -169,3 +172,48 @@ describe("skillSummary", () => {
|
|
|
169
172
|
expect(summary).toBe("");
|
|
170
173
|
});
|
|
171
174
|
});
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Real skills validation
|
|
177
|
+
// Each skill added to the repo must pass these checks.
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
const REQUIRED_SECTIONS = [
|
|
180
|
+
"Authentication & permissions",
|
|
181
|
+
"Reliability: rate limits, retries, idempotency",
|
|
182
|
+
"Error handling & troubleshooting",
|
|
183
|
+
"Common workflows (recipes)",
|
|
184
|
+
];
|
|
185
|
+
const KNOWN_SKILLS = [
|
|
186
|
+
"asana", "dynamics365", "figma", "github", "hubspot",
|
|
187
|
+
"jira", "monday", "salesforce", "servicenow", "zendesk",
|
|
188
|
+
"slack",
|
|
189
|
+
];
|
|
190
|
+
describe("real skills", () => {
|
|
191
|
+
let skills;
|
|
192
|
+
beforeAll(() => {
|
|
193
|
+
skills = loadSkills(REAL_SKILLS_DIR);
|
|
194
|
+
});
|
|
195
|
+
it("loads all expected skills", () => {
|
|
196
|
+
for (const slug of KNOWN_SKILLS) {
|
|
197
|
+
expect(skills.has(slug), `Missing skill: ${slug}`).toBe(true);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
it("every skill has a non-empty summary line", () => {
|
|
201
|
+
for (const [slug, content] of skills.entries()) {
|
|
202
|
+
const summary = skillSummary(content);
|
|
203
|
+
expect(summary, `${slug}: empty summary`).not.toBe("");
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
it("every skill contains the required sections", () => {
|
|
207
|
+
for (const [slug, content] of skills.entries()) {
|
|
208
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
209
|
+
const extracted = extractSection(content, section);
|
|
210
|
+
expect(extracted, `${slug}: missing section "${section}"`).not.toBeNull();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
it("every skill file is at least 5KB", () => {
|
|
215
|
+
for (const [slug, content] of skills.entries()) {
|
|
216
|
+
expect(content.length, `${slug}: suspiciously short skill file`).toBeGreaterThan(5000);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
package/package.json
CHANGED
package/skills/INDEX.md
CHANGED
|
@@ -20,6 +20,7 @@ A curated library of integration skill documents for the most common SaaS platfo
|
|
|
20
20
|
| Asana | [asana/skill.md](./asana/skill.md) | Project Manager, RevOps | Task and project management with portfolio tracking |
|
|
21
21
|
| GitHub | [github/skill.md](./github/skill.md) | Engineering, DevOps | Source control, PRs, CI/CD automation via REST + GraphQL |
|
|
22
22
|
| Figma | [figma/skill.md](./figma/skill.md) | Design, Product, DevOps | Design file inspection, asset export, webhooks, design-to-dev handoff |
|
|
23
|
+
| Slack | [slack/skill.md](./slack/skill.md) | All teams | Real-time messaging, notifications, event-driven automation via Web API |
|
|
23
24
|
|
|
24
25
|
---
|
|
25
26
|
|
|
@@ -165,6 +166,20 @@ A curated library of integration skill documents for the most common SaaS platfo
|
|
|
165
166
|
|
|
166
167
|
---
|
|
167
168
|
|
|
169
|
+
### Slack → [skill.md](./slack/skill.md)
|
|
170
|
+
1. Post a rich Block Kit message to a channel
|
|
171
|
+
2. Reply in a thread using `thread_ts`
|
|
172
|
+
3. Send a direct message (open DM channel + postMessage)
|
|
173
|
+
4. Look up a user by email address
|
|
174
|
+
5. Update a message in place as status changes
|
|
175
|
+
6. Upload a file using the two-step external upload API
|
|
176
|
+
7. Add a reaction to a message
|
|
177
|
+
8. List all channels and build a name → ID cache
|
|
178
|
+
9. Read channel history incrementally using cursor pagination
|
|
179
|
+
10. Receive, verify, and process Events API payloads
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
168
183
|
## Common Cross-Tool Patterns
|
|
169
184
|
|
|
170
185
|
These scenarios span multiple skill docs — reference both tools:
|
|
@@ -199,6 +214,7 @@ These scenarios span multiple skill docs — reference both tools:
|
|
|
199
214
|
| Asana | Personal Access Token (PAT) | Bearer token | Inherits user permissions |
|
|
200
215
|
| GitHub | Fine-grained PAT or GitHub App installation token | Bearer token | Per-resource permissions (contents, issues, pull_requests, etc.) |
|
|
201
216
|
| Figma | Personal Access Token (PAT) or OAuth 2.0 | `X-Figma-Token` (PAT) / `Authorization: Bearer` (OAuth) | Granular scopes: `file_content:read`, `webhooks:write`, etc. |
|
|
217
|
+
| Slack | Bot token (OAuth 2.0 app install) | `xoxb-...` Bearer token | Per-method scopes: `chat:write`, `channels:read`, `users:read`, etc. |
|
|
202
218
|
|
|
203
219
|
---
|
|
204
220
|
|
|
@@ -216,10 +232,11 @@ These scenarios span multiple skill docs — reference both tools:
|
|
|
216
232
|
| Asana | 1,500 req/min + 150 concurrent | 1 minute | `Retry-After` on 429 |
|
|
217
233
|
| GitHub | PAT: 5,000/hr · GitHub App: 5k–12.5k/hr · Enterprise Cloud: 15,000/hr · Secondary: 900 pts/min, 80 create/min | 1 hour (primary) + sliding (secondary) | `x-ratelimit-remaining` / `x-ratelimit-reset`; 403 or 429 |
|
|
218
234
|
| Figma | Starter: 10/min (T1), 25/min (T2) · Professional: 15/min (T1), 50/min (T2) · Org: 20/min (T1), 100/min (T2) | Per minute (leaky bucket) | `Retry-After` / `X-Figma-Rate-Limit-Type` / `X-Figma-Plan-Tier` |
|
|
235
|
+
| Slack | Tier 1: 1/min · Tier 2: 20/min · Tier 3: 50/min · Tier 4: 100/min · `chat.postMessage`: 1/sec/channel sub-limit | Per method per workspace | HTTP 429 + `Retry-After` header (seconds) |
|
|
219
236
|
|
|
220
237
|
---
|
|
221
238
|
|
|
222
|
-
*Last updated: 2026-02-
|
|
239
|
+
*Last updated: 2026-02-26 (Slack skill added) | See [ROADMAP.md](./ROADMAP.md) for versioning and governance details.*
|
|
223
240
|
|
|
224
241
|
### API version reference (as of 2026-02-22)
|
|
225
242
|
|
|
@@ -235,3 +252,4 @@ These scenarios span multiple skill docs — reference both tools:
|
|
|
235
252
|
| Asana | REST API 1.0 | No breaking changes; always use `opt_fields` |
|
|
236
253
|
| GitHub | REST API + GraphQL v4 | Pin `X-GitHub-Api-Version: 2022-11-28`; fine-grained PATs recommended |
|
|
237
254
|
| Figma | REST API v1 / Webhooks V2 | Path-based versioning (`/v1/`, `/v2/`); `files:read` scope deprecated — use granular scopes |
|
|
255
|
+
| Slack | Web API (no versioned path) | Always check `ok` field (HTTP 200 even on errors); `files.upload` deprecated — use `files.getUploadURLExternal` |
|
package/skills/figma/skill.md
CHANGED
|
@@ -735,7 +735,7 @@ if current_version != cached_version:
|
|
|
735
735
|
|
|
736
736
|
---
|
|
737
737
|
|
|
738
|
-
##
|
|
738
|
+
## Reliability: rate limits, retries, idempotency
|
|
739
739
|
|
|
740
740
|
### Tier definitions (as of Nov 17, 2025)
|
|
741
741
|
|
|
@@ -794,7 +794,7 @@ def figma_request(url: str, token: str, params: dict = None, max_retries: int =
|
|
|
794
794
|
|
|
795
795
|
---
|
|
796
796
|
|
|
797
|
-
## Error handling
|
|
797
|
+
## Error handling & troubleshooting
|
|
798
798
|
|
|
799
799
|
| Code | When | Action |
|
|
800
800
|
|------|------|--------|
|
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
# Slack Skill
|
|
2
|
+
|
|
3
|
+
> **Last validated:** 2026-02-26 | **API:** Slack Web API (no versioned URL path)
|
|
4
|
+
> **REST base URL:** `https://slack.com/api/`
|
|
5
|
+
> **Assumed product:** Slack (cloud). Slack Enterprise Grid adds org-level APIs but the core methods are identical.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What this skill enables
|
|
10
|
+
|
|
11
|
+
- Send messages and rich Block Kit cards to any channel, DM, or thread from external systems.
|
|
12
|
+
- Build notification pipelines: alert on-call engineers, post deploy summaries, surface CRM events in team channels.
|
|
13
|
+
- Manage channels programmatically: create, archive, invite users, and sync membership from external directories.
|
|
14
|
+
- Respond to user actions in real time: slash commands, interactive buttons, modals (views), and workflow steps.
|
|
15
|
+
- Sync data bi-directionally: listen to Slack events (messages, reactions, channel membership) and reflect changes in external tools.
|
|
16
|
+
- Upload and share files: attach reports, screenshots, and exports directly in channels.
|
|
17
|
+
- Search and audit: query message history for compliance or reporting pipelines.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Best-fit use cases
|
|
22
|
+
|
|
23
|
+
| Use case | Why it matters | Primary objects | Typical trigger | Success criteria |
|
|
24
|
+
|----------|---------------|-----------------|-----------------|-----------------|
|
|
25
|
+
| Post alert to on-call channel | Instant human awareness of incidents | Message, Channel | PagerDuty/Datadog webhook | Message posted with link to runbook within 2s |
|
|
26
|
+
| Deploy summary notification | Keep team informed without polling dashboards | Message, Block Kit | CI/CD pipeline completes | Formatted message with pass/fail + diff link |
|
|
27
|
+
| Sync CRM deal update to sales channel | Surface revenue moments to the team | Message, Thread | HubSpot/Salesforce webhook | Deal name, stage, and owner in message |
|
|
28
|
+
| Create ticket from Slack reaction | Lightweight triage without leaving Slack | Reaction event, Message | User adds 🎫 emoji to a message | Jira/Zendesk ticket created, permalink posted in thread |
|
|
29
|
+
| Slash command → workflow | Self-service automation from Slack UI | Slash command, Modal | User types `/create-ticket` | Modal opens, user fills form, ticket created |
|
|
30
|
+
| User lookup by email | Resolve an email address to a Slack user ID | User | Before sending a DM | `user_id` returned for `conversations.open` |
|
|
31
|
+
| Daily digest | Scheduled summary of CRM/ops data | Message, Scheduled post | Cron job | Summary message posted at 9 AM team timezone |
|
|
32
|
+
| Channel membership sync | Keep project channels aligned with HRIS/CRM | Channel, User | User onboarding/offboarding event | User invited or removed from correct channels |
|
|
33
|
+
| File report delivery | Share generated reports without email | File | Scheduled job or request | File uploaded, shared in channel with comment |
|
|
34
|
+
| Webhook event to external system | Mirror Slack activity to ops tools | Events API | Message posted, reaction added | External system receives and processes event within 3s |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Key concepts & data model
|
|
39
|
+
|
|
40
|
+
### Core objects
|
|
41
|
+
|
|
42
|
+
| Object | Description | Identified by |
|
|
43
|
+
|--------|-------------|---------------|
|
|
44
|
+
| **Workspace** | A Slack organization | `team_id` (e.g., `T01ABCDEF`) |
|
|
45
|
+
| **Channel** | Public channel, private channel, DM, or MPIM (group DM) | `channel_id` (e.g., `C01ABCDEF` public, `G01…` private, `D01…` DM) |
|
|
46
|
+
| **User** | A person in the workspace | `user_id` (e.g., `U01ABCDEF`) |
|
|
47
|
+
| **Bot** | A non-human entity representing a Slack app | `bot_id` (e.g., `B01ABCDEF`) |
|
|
48
|
+
| **Message** | A chat message in a channel or thread | `(channel_id, ts)` tuple — `ts` is a Unix timestamp string like `"1706200000.123456"` |
|
|
49
|
+
| **Thread** | A sub-conversation anchored to a message | Parent message's `ts` used as `thread_ts` |
|
|
50
|
+
| **File** | An uploaded file or snippet | `file_id` (e.g., `F01ABCDEF`) |
|
|
51
|
+
| **Reaction** | An emoji added to a message | `(name, user_id, item)` — `name` is the emoji name without colons |
|
|
52
|
+
| **App** | A Slack integration/bot | `app_id` (e.g., `A01ABCDEF`) |
|
|
53
|
+
| **View** | A modal or App Home surface | `view_id` returned by `views.open` |
|
|
54
|
+
|
|
55
|
+
### Channel types and ID prefixes
|
|
56
|
+
|
|
57
|
+
| Prefix | Type | Notes |
|
|
58
|
+
|--------|------|-------|
|
|
59
|
+
| `C` | Public channel | Visible to all workspace members |
|
|
60
|
+
| `G` | Private channel / MPIM | Invite-only; group DMs also use `G` |
|
|
61
|
+
| `D` | Direct message | Between bot and one user |
|
|
62
|
+
|
|
63
|
+
> **Tip:** Always resolve channel names to IDs at startup using `conversations.list` and cache them. Names can change; IDs are stable.
|
|
64
|
+
|
|
65
|
+
### Message timestamps (`ts`)
|
|
66
|
+
|
|
67
|
+
Slack uses `ts` (timestamp) as the primary message identifier — it is a string like `"1706200000.123456"`, not a number. Always store and compare as a string. The `ts` of the parent message is passed as `thread_ts` to post into a thread.
|
|
68
|
+
|
|
69
|
+
### Relationships
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
Workspace (team_id)
|
|
73
|
+
├── Channel (C / G / D)
|
|
74
|
+
│ ├── Message (channel + ts)
|
|
75
|
+
│ │ ├── Thread replies (thread_ts = parent ts)
|
|
76
|
+
│ │ ├── Reactions
|
|
77
|
+
│ │ └── File attachments
|
|
78
|
+
│ └── Members (user_ids)
|
|
79
|
+
├── User (U)
|
|
80
|
+
└── App / Bot (A / B)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Authentication & permissions
|
|
86
|
+
|
|
87
|
+
### Supported auth methods
|
|
88
|
+
|
|
89
|
+
| Method | Best for | Token format |
|
|
90
|
+
|--------|----------|-------------|
|
|
91
|
+
| **Bot token** | All server-to-server automation (recommended) | `xoxb-...` |
|
|
92
|
+
| **User token** | Acting as a specific user; required for some user-only methods | `xoxp-...` |
|
|
93
|
+
| **App-level token** | Socket Mode (WebSocket) connections only | `xapp-...` |
|
|
94
|
+
| **Incoming Webhook URL** | Simple one-way message posting with no SDK | Full URL, e.g. `https://hooks.slack.com/services/T.../B.../...` |
|
|
95
|
+
|
|
96
|
+
**Bot tokens are recommended** for all automation — they have stable identity, are scoped to the app, and do not expose any individual user's data.
|
|
97
|
+
|
|
98
|
+
### Creating a Slack App and getting a bot token
|
|
99
|
+
|
|
100
|
+
1. Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App** → **From Manifest** (or From Scratch).
|
|
101
|
+
2. Under **OAuth & Permissions** → **Bot Token Scopes**, add the scopes your app needs (see table below).
|
|
102
|
+
3. **Install App** to your workspace → copy the **Bot User OAuth Token** (`xoxb-...`).
|
|
103
|
+
4. Store the token as an environment variable: `SLACK_BOT_TOKEN`.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
curl -s https://slack.com/api/auth.test \
|
|
107
|
+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
|
108
|
+
-H "Content-Type: application/json"
|
|
109
|
+
# Response: {"ok": true, "team": "Acme Corp", "user": "my-bot", "team_id": "T01...", "user_id": "U01..."}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Required scopes (bot token — least privilege)
|
|
113
|
+
|
|
114
|
+
| Scope | Purpose |
|
|
115
|
+
|-------|---------|
|
|
116
|
+
| `chat:write` | Post messages as the bot |
|
|
117
|
+
| `chat:write.public` | Post to public channels without joining first |
|
|
118
|
+
| `channels:read` | List public channels, get channel info |
|
|
119
|
+
| `channels:history` | Read messages in public channels the bot has joined |
|
|
120
|
+
| `groups:read` | List private channels the bot is a member of |
|
|
121
|
+
| `groups:history` | Read messages in private channels |
|
|
122
|
+
| `im:read` | List DMs the bot has open |
|
|
123
|
+
| `im:write` | Open DMs with users |
|
|
124
|
+
| `im:history` | Read DM messages |
|
|
125
|
+
| `users:read` | Get user info (name, display name, tz) |
|
|
126
|
+
| `users:read.email` | Look up users by email address |
|
|
127
|
+
| `files:write` | Upload files |
|
|
128
|
+
| `reactions:read` | Read reactions on messages |
|
|
129
|
+
| `reactions:write` | Add/remove reactions |
|
|
130
|
+
| `channels:join` | Join public channels programmatically |
|
|
131
|
+
| `channels:manage` | Create/archive public channels |
|
|
132
|
+
| `groups:write` | Create/archive private channels |
|
|
133
|
+
| `team:read` | Get workspace info |
|
|
134
|
+
| `search:read` | Search messages (user token only — not available to bots) |
|
|
135
|
+
|
|
136
|
+
> **Note:** `search:read` requires a **user token** (`xoxp-...`). There is no bot-accessible search API; use `conversations.history` + cursor pagination for bot-accessible message scanning.
|
|
137
|
+
|
|
138
|
+
### Required headers (always include)
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
Authorization: Bearer xoxb-your-token
|
|
142
|
+
Content-Type: application/json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
> **Legacy note:** Some Slack docs show `application/x-www-form-urlencoded` POST bodies. This still works but JSON (`application/json`) is the modern approach and is required for Block Kit payloads.
|
|
146
|
+
|
|
147
|
+
### Token storage
|
|
148
|
+
|
|
149
|
+
- Store bot tokens in environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault). Never commit to code.
|
|
150
|
+
- Bot tokens do not expire unless the app is uninstalled or the token is revoked. Implement health checks (call `auth.test` on startup) to detect revocation early.
|
|
151
|
+
- For multi-workspace (Slack Connect / distribution), use OAuth 2.0 and store one bot token per workspace in your database.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Common workflows (recipes)
|
|
156
|
+
|
|
157
|
+
### Recipe 1: Post a message to a channel
|
|
158
|
+
|
|
159
|
+
**Goal:** Send a formatted notification to a channel.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
curl -s -X POST https://slack.com/api/chat.postMessage \
|
|
163
|
+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
|
164
|
+
-H "Content-Type: application/json" \
|
|
165
|
+
-d '{
|
|
166
|
+
"channel": "C01ABCDEF",
|
|
167
|
+
"text": "Deployment complete: *api-service v2.3.1* deployed to production.",
|
|
168
|
+
"blocks": [
|
|
169
|
+
{
|
|
170
|
+
"type": "section",
|
|
171
|
+
"text": {
|
|
172
|
+
"type": "mrkdwn",
|
|
173
|
+
"text": ":white_check_mark: *api-service v2.3.1* deployed to *production*\n*Duration:* 2m 14s | *By:* @janesmith"
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"type": "actions",
|
|
178
|
+
"elements": [
|
|
179
|
+
{
|
|
180
|
+
"type": "button",
|
|
181
|
+
"text": { "type": "plain_text", "text": "View Logs" },
|
|
182
|
+
"url": "https://logs.example.com/deploy/1234",
|
|
183
|
+
"action_id": "view_logs"
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}'
|
|
189
|
+
# Response: {"ok": true, "channel": "C01ABCDEF", "ts": "1706200000.123456", "message": {...}}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Always include both `text` and `blocks`.** The `text` field is the fallback for notifications and accessibility tools — it is displayed when blocks cannot be rendered.
|
|
193
|
+
|
|
194
|
+
**Edge cases:**
|
|
195
|
+
- Bot must be a member of the channel (or use the `chat:write.public` scope for public channels).
|
|
196
|
+
- If `channel` is a user ID (e.g., `U01...`), Slack will send a DM. Alternatively, use Recipe 3 to open a DM first.
|
|
197
|
+
- To post to a private channel, the bot must be invited first: `conversations.invite`.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### Recipe 2: Post a reply in a thread
|
|
202
|
+
|
|
203
|
+
**Goal:** Add a reply to an existing message thread.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
curl -s -X POST https://slack.com/api/chat.postMessage \
|
|
207
|
+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
|
208
|
+
-H "Content-Type: application/json" \
|
|
209
|
+
-d '{
|
|
210
|
+
"channel": "C01ABCDEF",
|
|
211
|
+
"thread_ts": "1706200000.123456",
|
|
212
|
+
"text": "Incident resolved. Root cause: connection pool exhaustion. Post-mortem linked below.",
|
|
213
|
+
"reply_broadcast": false
|
|
214
|
+
}'
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**`reply_broadcast: true`** also sends the reply to the main channel (appears in both thread and channel).
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
### Recipe 3: Send a direct message to a user
|
|
222
|
+
|
|
223
|
+
**Goal:** Open a DM channel with a user and post a message.
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Step 1 — open a DM channel (idempotent — returns existing channel if open)
|
|
227
|
+
DM_CHANNEL=$(curl -s -X POST https://slack.com/api/conversations.open \
|
|
228
|
+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
|
229
|
+
-H "Content-Type: application/json" \
|
|
230
|
+
-d '{"users": "U01ABCDEF"}' \
|
|
231
|
+
| jq -r '.channel.id')
|
|
232
|
+
|
|
233
|
+
# Step 2 — post message
|
|
234
|
+
curl -s -X POST https://slack.com/api/chat.postMessage \
|
|
235
|
+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
|
236
|
+
-H "Content-Type: application/json" \
|
|
237
|
+
-d "{
|
|
238
|
+
\"channel\": \"$DM_CHANNEL\",
|
|
239
|
+
\"text\": \"Hi! Your Salesforce deal has moved to Proposal stage.\"
|
|
240
|
+
}"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### Recipe 4: Look up a user by email
|
|
246
|
+
|
|
247
|
+
**Goal:** Find a user's Slack ID from their email address (e.g., from a CRM record).
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
curl -s -G "https://slack.com/api/users.lookupByEmail" \
|
|
251
|
+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
|
252
|
+
--data-urlencode "email=janesmith@example.com"
|
|
253
|
+
# Response: {"ok": true, "user": {"id": "U01ABCDEF", "name": "janesmith", "tz": "America/New_York", ...}}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Requires `users:read.email` scope. Store the resolved `user_id` — do not call this on every request.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
### Recipe 5: Update or delete a message
|
|
261
|
+
|
|
262
|
+
**Goal:** Edit a previously posted message (e.g., update a status message as a deployment progresses).
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# Update
|
|
266
|
+
curl -s -X POST https://slack.com/api/chat.update \
|
|
267
|
+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
|
268
|
+
-H "Content-Type: application/json" \
|
|
269
|
+
-d '{
|
|
270
|
+
"channel": "C01ABCDEF",
|
|
271
|
+
"ts": "1706200000.123456",
|
|
272
|
+
"text": "Deployment FAILED: api-service v2.3.1",
|
|
273
|
+
"blocks": [
|
|
274
|
+
{
|
|
275
|
+
"type": "section",
|
|
276
|
+
"text": { "type": "mrkdwn", "text": ":x: *Deployment FAILED* — api-service v2.3.1\nSee <https://logs.example.com/1234|logs> for details." }
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
}'
|
|
280
|
+
|
|
281
|
+
# Delete
|
|
282
|
+
curl -s -X POST https://slack.com/api/chat.delete \
|
|
283
|
+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
|
284
|
+
-H "Content-Type: application/json" \
|
|
285
|
+
-d '{"channel": "C01ABCDEF", "ts": "1706200000.123456"}'
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Pattern:** Post a "starting…" message, save the returned `ts`, then update it in place as the operation progresses. This avoids flooding the channel with multiple messages.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
### Recipe 6: Upload a file to a channel
|
|
293
|
+
|
|
294
|
+
**Goal:** Share a generated report or screenshot in a channel. Use the two-step upload API (recommended since 2023).
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
import requests, os
|
|
298
|
+
|
|
299
|
+
token = os.environ["SLACK_BOT_TOKEN"]
|
|
300
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
301
|
+
|
|
302
|
+
# Step 1 — get an upload URL
|
|
303
|
+
resp = requests.get(
|
|
304
|
+
"https://slack.com/api/files.getUploadURLExternal",
|
|
305
|
+
headers=headers,
|
|
306
|
+
params={"filename": "report.csv", "length": os.path.getsize("report.csv")}
|
|
307
|
+
)
|
|
308
|
+
data = resp.json()
|
|
309
|
+
upload_url = data["upload_url"]
|
|
310
|
+
file_id = data["file_id"]
|
|
311
|
+
|
|
312
|
+
# Step 2 — upload the file content to the pre-signed URL
|
|
313
|
+
with open("report.csv", "rb") as f:
|
|
314
|
+
requests.post(upload_url, data=f)
|
|
315
|
+
|
|
316
|
+
# Step 3 — complete the upload and share to channel
|
|
317
|
+
requests.post(
|
|
318
|
+
"https://slack.com/api/files.completeUploadExternal",
|
|
319
|
+
headers=headers,
|
|
320
|
+
json={
|
|
321
|
+
"files": [{"id": file_id, "title": "Weekly Report"}],
|
|
322
|
+
"channel_id": "C01ABCDEF",
|
|
323
|
+
"initial_comment": "Here's the weekly ops report."
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
> **Avoid** `files.upload` (the old single-call method) — it is deprecated and has a 1 GB limit vs. much larger limits on the new API.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
### Recipe 7: Add a reaction to a message
|
|
333
|
+
|
|
334
|
+
**Goal:** React to a message programmatically (e.g., acknowledge receipt by adding ✅).
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
curl -s -X POST https://slack.com/api/reactions.add \
|
|
338
|
+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
|
339
|
+
-H "Content-Type: application/json" \
|
|
340
|
+
-d '{
|
|
341
|
+
"channel": "C01ABCDEF",
|
|
342
|
+
"timestamp": "1706200000.123456",
|
|
343
|
+
"name": "white_check_mark"
|
|
344
|
+
}'
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Emoji `name` is the text between colons (e.g., `:white_check_mark:` → `"white_check_mark"`). Custom emoji are referenced by their workspace name.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
### Recipe 8: List channels and resolve names to IDs
|
|
352
|
+
|
|
353
|
+
**Goal:** Build and cache a channel name → ID map at startup.
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
import requests
|
|
357
|
+
|
|
358
|
+
def get_channel_map(token: str) -> dict[str, str]:
|
|
359
|
+
"""Returns {channel_name: channel_id} for all public channels."""
|
|
360
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
361
|
+
channel_map = {}
|
|
362
|
+
cursor = None
|
|
363
|
+
|
|
364
|
+
while True:
|
|
365
|
+
params = {"limit": 200, "exclude_archived": True, "types": "public_channel"}
|
|
366
|
+
if cursor:
|
|
367
|
+
params["cursor"] = cursor
|
|
368
|
+
|
|
369
|
+
resp = requests.get(
|
|
370
|
+
"https://slack.com/api/conversations.list",
|
|
371
|
+
headers=headers,
|
|
372
|
+
params=params
|
|
373
|
+
).json()
|
|
374
|
+
|
|
375
|
+
if not resp["ok"]:
|
|
376
|
+
raise Exception(f"Slack error: {resp['error']}")
|
|
377
|
+
|
|
378
|
+
for ch in resp["channels"]:
|
|
379
|
+
channel_map[ch["name"]] = ch["id"]
|
|
380
|
+
|
|
381
|
+
cursor = resp.get("response_metadata", {}).get("next_cursor")
|
|
382
|
+
if not cursor:
|
|
383
|
+
break
|
|
384
|
+
|
|
385
|
+
return channel_map
|
|
386
|
+
|
|
387
|
+
# Usage
|
|
388
|
+
CHANNELS = get_channel_map(os.environ["SLACK_BOT_TOKEN"])
|
|
389
|
+
post_to = CHANNELS["engineering-deploys"]
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
### Recipe 9: Read channel history (incremental sync)
|
|
395
|
+
|
|
396
|
+
**Goal:** Fetch messages posted since the last sync run.
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
import requests, time
|
|
400
|
+
|
|
401
|
+
def fetch_messages_since(token: str, channel_id: str, oldest_ts: str) -> list:
|
|
402
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
403
|
+
messages = []
|
|
404
|
+
cursor = None
|
|
405
|
+
|
|
406
|
+
while True:
|
|
407
|
+
params = {"channel": channel_id, "oldest": oldest_ts, "limit": 200}
|
|
408
|
+
if cursor:
|
|
409
|
+
params["cursor"] = cursor
|
|
410
|
+
|
|
411
|
+
resp = requests.get(
|
|
412
|
+
"https://slack.com/api/conversations.history",
|
|
413
|
+
headers=headers,
|
|
414
|
+
params=params
|
|
415
|
+
).json()
|
|
416
|
+
|
|
417
|
+
if not resp["ok"]:
|
|
418
|
+
raise Exception(f"Slack error: {resp['error']}")
|
|
419
|
+
|
|
420
|
+
messages.extend(resp["messages"])
|
|
421
|
+
|
|
422
|
+
if not resp.get("has_more"):
|
|
423
|
+
break
|
|
424
|
+
cursor = resp["response_metadata"]["next_cursor"]
|
|
425
|
+
|
|
426
|
+
return messages
|
|
427
|
+
|
|
428
|
+
# Store the latest ts after each run as the next oldest
|
|
429
|
+
latest_ts = str(time.time() - 3600) # last hour on first run
|
|
430
|
+
messages = fetch_messages_since(token, "C01ABCDEF", latest_ts)
|
|
431
|
+
next_run_ts = messages[0]["ts"] if messages else latest_ts
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
### Recipe 10: Receive and verify Events API payloads
|
|
437
|
+
|
|
438
|
+
**Goal:** Handle real-time Slack events (message posted, reaction added, member joined, etc.) in your webhook endpoint.
|
|
439
|
+
|
|
440
|
+
**App setup:** In your Slack App config → **Event Subscriptions** → enable, set Request URL to your endpoint, subscribe to bot events (e.g., `message.channels`, `reaction_added`).
|
|
441
|
+
|
|
442
|
+
**URL verification (one-time, on first save):**
|
|
443
|
+
```python
|
|
444
|
+
from flask import Flask, request, jsonify
|
|
445
|
+
|
|
446
|
+
app = Flask(__name__)
|
|
447
|
+
|
|
448
|
+
@app.route("/slack/events", methods=["POST"])
|
|
449
|
+
def slack_events():
|
|
450
|
+
body = request.json
|
|
451
|
+
|
|
452
|
+
# Slack URL verification challenge
|
|
453
|
+
if body.get("type") == "url_verification":
|
|
454
|
+
return jsonify({"challenge": body["challenge"]})
|
|
455
|
+
|
|
456
|
+
# Verify signature before processing
|
|
457
|
+
if not verify_slack_signature(request):
|
|
458
|
+
return "", 403
|
|
459
|
+
|
|
460
|
+
event = body.get("event", {})
|
|
461
|
+
# Handle asynchronously — must return 200 within 3 seconds
|
|
462
|
+
handle_event_async(event)
|
|
463
|
+
return "", 200
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Signature verification (required — never skip this):**
|
|
467
|
+
```python
|
|
468
|
+
import hmac, hashlib, time
|
|
469
|
+
|
|
470
|
+
SLACK_SIGNING_SECRET = os.environ["SLACK_SIGNING_SECRET"]
|
|
471
|
+
|
|
472
|
+
def verify_slack_signature(request) -> bool:
|
|
473
|
+
timestamp = request.headers.get("X-Slack-Request-Timestamp", "")
|
|
474
|
+
sig_header = request.headers.get("X-Slack-Signature", "")
|
|
475
|
+
|
|
476
|
+
# Reject requests older than 5 minutes (replay attack prevention)
|
|
477
|
+
if abs(time.time() - int(timestamp)) > 300:
|
|
478
|
+
return False
|
|
479
|
+
|
|
480
|
+
base_string = f"v0:{timestamp}:{request.get_data(as_text=True)}"
|
|
481
|
+
expected = "v0=" + hmac.new(
|
|
482
|
+
SLACK_SIGNING_SECRET.encode(),
|
|
483
|
+
base_string.encode(),
|
|
484
|
+
hashlib.sha256
|
|
485
|
+
).hexdigest()
|
|
486
|
+
|
|
487
|
+
return hmac.compare_digest(expected, sig_header)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Critical:** Respond with HTTP 200 within **3 seconds**. If processing takes longer, return 200 immediately and process asynchronously (queue the event). Slack retries failed deliveries 3 times.
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
### Recipe 11: Invite a user to a channel
|
|
495
|
+
|
|
496
|
+
**Goal:** Add a user to a private channel when they join a project.
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
curl -s -X POST https://slack.com/api/conversations.invite \
|
|
500
|
+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
|
501
|
+
-H "Content-Type: application/json" \
|
|
502
|
+
-d '{
|
|
503
|
+
"channel": "G01ABCDEF",
|
|
504
|
+
"users": "U01ABCDEF,U02ABCDEF"
|
|
505
|
+
}'
|
|
506
|
+
# Response: {"ok": true, "channel": {...}}
|
|
507
|
+
# Error if user is already a member: {"ok": false, "error": "already_in_channel"}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Multiple users can be invited in one call by comma-separating `users`.
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## Query patterns & filtering
|
|
515
|
+
|
|
516
|
+
### Pagination
|
|
517
|
+
|
|
518
|
+
All list endpoints use **cursor-based pagination**. Never use `page`/`offset` (not supported on most Slack endpoints).
|
|
519
|
+
|
|
520
|
+
```python
|
|
521
|
+
def paginate_slack(token: str, method: str, params: dict) -> list:
|
|
522
|
+
"""Generic cursor-based paginator for Slack list methods."""
|
|
523
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
524
|
+
results = []
|
|
525
|
+
cursor = None
|
|
526
|
+
list_key = None # auto-detected
|
|
527
|
+
|
|
528
|
+
while True:
|
|
529
|
+
p = {**params, "limit": 200}
|
|
530
|
+
if cursor:
|
|
531
|
+
p["cursor"] = cursor
|
|
532
|
+
|
|
533
|
+
resp = requests.get(
|
|
534
|
+
f"https://slack.com/api/{method}",
|
|
535
|
+
headers=headers,
|
|
536
|
+
params=p
|
|
537
|
+
).json()
|
|
538
|
+
|
|
539
|
+
if not resp["ok"]:
|
|
540
|
+
raise Exception(f"Slack error {method}: {resp['error']}")
|
|
541
|
+
|
|
542
|
+
# Detect the list key (channels, members, users, messages, etc.)
|
|
543
|
+
if list_key is None:
|
|
544
|
+
list_key = next((k for k in resp if isinstance(resp[k], list) and k != "warnings"), None)
|
|
545
|
+
if list_key:
|
|
546
|
+
results.extend(resp[list_key])
|
|
547
|
+
|
|
548
|
+
cursor = resp.get("response_metadata", {}).get("next_cursor")
|
|
549
|
+
if not cursor:
|
|
550
|
+
break
|
|
551
|
+
|
|
552
|
+
return results
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Filtering messages
|
|
556
|
+
|
|
557
|
+
- **By time range:** Use `oldest` and `latest` parameters (Unix timestamp strings) on `conversations.history`.
|
|
558
|
+
- **By thread:** Use `conversations.replies` with `ts` of the parent message.
|
|
559
|
+
- **Bot vs human messages:** Check `subtype` field — bot messages have `subtype: "bot_message"` or `bot_id` present.
|
|
560
|
+
- **Ignore system messages:** Filter out messages with `subtype` set (e.g., `channel_join`, `channel_leave`).
|
|
561
|
+
|
|
562
|
+
### Message deduplication
|
|
563
|
+
|
|
564
|
+
Use `(channel_id, ts)` as a composite key — `ts` is unique within a channel. For Events API, Slack may retry the same event; use the event's `event_id` as an idempotency key:
|
|
565
|
+
|
|
566
|
+
```python
|
|
567
|
+
processed_events = set() # use Redis in production
|
|
568
|
+
|
|
569
|
+
def handle_event_async(event_wrapper: dict):
|
|
570
|
+
event_id = event_wrapper.get("event_id")
|
|
571
|
+
if event_id in processed_events:
|
|
572
|
+
return # already handled
|
|
573
|
+
processed_events.add(event_id)
|
|
574
|
+
# ... process event
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## Reliability: rate limits, retries, idempotency
|
|
580
|
+
|
|
581
|
+
### Rate limits (verified 2026-02-26)
|
|
582
|
+
|
|
583
|
+
Slack uses a **tiered** rate limit system, applied per workspace per method:
|
|
584
|
+
|
|
585
|
+
| Tier | Limit | Examples |
|
|
586
|
+
|------|-------|---------|
|
|
587
|
+
| **Tier 1** | 1+ req/min | `conversations.create`, `admin.*` |
|
|
588
|
+
| **Tier 2** | 20+ req/min | `auth.test`, `conversations.info`, `files.info` |
|
|
589
|
+
| **Tier 3** | 50+ req/min | `conversations.history`, `conversations.list`, `users.info`, `reactions.add` |
|
|
590
|
+
| **Tier 4** | 100+ req/min | `chat.postMessage`, `chat.update`, `users.list` |
|
|
591
|
+
| **Special** | 1 req/sec per channel | `chat.postMessage` per-channel sub-limit (regardless of Tier 4) |
|
|
592
|
+
|
|
593
|
+
When rate limited, Slack returns **HTTP 429** with a `Retry-After` header (seconds to wait).
|
|
594
|
+
|
|
595
|
+
> **Practical limit for `chat.postMessage`:** Even though it is Tier 4 (100/min workspace-wide), there is a per-channel limit of ~1 req/sec. For burst messaging to the same channel, add a 1-second delay between calls or use `chat.scheduleMessage`.
|
|
596
|
+
|
|
597
|
+
### Retry strategy
|
|
598
|
+
|
|
599
|
+
```python
|
|
600
|
+
import time, requests
|
|
601
|
+
|
|
602
|
+
def slack_api(method: str, token: str, **kwargs) -> dict:
|
|
603
|
+
url = f"https://slack.com/api/{method}"
|
|
604
|
+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
605
|
+
|
|
606
|
+
for attempt in range(5):
|
|
607
|
+
resp = requests.post(url, headers=headers, json=kwargs)
|
|
608
|
+
|
|
609
|
+
if resp.status_code == 429:
|
|
610
|
+
retry_after = int(resp.headers.get("Retry-After", 5))
|
|
611
|
+
time.sleep(retry_after)
|
|
612
|
+
continue
|
|
613
|
+
|
|
614
|
+
if resp.status_code >= 500:
|
|
615
|
+
time.sleep(2 ** attempt)
|
|
616
|
+
continue
|
|
617
|
+
|
|
618
|
+
data = resp.json()
|
|
619
|
+
if not data["ok"]:
|
|
620
|
+
if data["error"] == "ratelimited":
|
|
621
|
+
time.sleep(int(resp.headers.get("Retry-After", 5)))
|
|
622
|
+
continue
|
|
623
|
+
raise Exception(f"Slack API error: {data['error']}")
|
|
624
|
+
|
|
625
|
+
return data
|
|
626
|
+
|
|
627
|
+
raise Exception("Max retries exceeded")
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
> **Note:** Slack always returns HTTP 200, even for application-level errors. Always check `data["ok"]` — not just the HTTP status code.
|
|
631
|
+
|
|
632
|
+
### Idempotency patterns
|
|
633
|
+
|
|
634
|
+
- **`chat.postMessage`:** Not natively idempotent. To avoid duplicate messages, use an external idempotency key: check whether a message with your known content/ts already exists in the channel before posting, or store the returned `ts` in your database.
|
|
635
|
+
- **`conversations.invite`:** Already idempotent — returns `already_in_channel` error (not a failure) if user is a member.
|
|
636
|
+
- **`reactions.add`:** Returns `already_reacted` if reaction already exists — safe to ignore.
|
|
637
|
+
- **Events API:** Use `event_id` as an idempotency key in Redis/database to deduplicate retried events.
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Error handling & troubleshooting
|
|
642
|
+
|
|
643
|
+
### All Slack errors follow this pattern
|
|
644
|
+
|
|
645
|
+
```json
|
|
646
|
+
{ "ok": false, "error": "channel_not_found" }
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
HTTP status is always 200. Parse the response body to detect errors.
|
|
650
|
+
|
|
651
|
+
### Common errors
|
|
652
|
+
|
|
653
|
+
| Error string | Meaning | Fix |
|
|
654
|
+
|-------------|---------|-----|
|
|
655
|
+
| `not_in_channel` | Bot is not a member of the channel | Invite the bot or use `chat:write.public` scope |
|
|
656
|
+
| `channel_not_found` | Channel ID is wrong or bot can't see it | Verify channel ID; check bot is in workspace |
|
|
657
|
+
| `invalid_auth` | Token is invalid or revoked | Re-generate token; check env var |
|
|
658
|
+
| `missing_scope` | Bot token lacks required scope | Add scope in App config → reinstall app |
|
|
659
|
+
| `ratelimited` | Rate limit hit | Respect `Retry-After` header |
|
|
660
|
+
| `already_in_channel` | User already a channel member | Ignore — treat as success |
|
|
661
|
+
| `already_reacted` | Reaction already exists | Ignore — treat as success |
|
|
662
|
+
| `cant_invite_self` | Trying to invite the bot to a channel it's in | Ignore |
|
|
663
|
+
| `msg_too_long` | Message text exceeds 40,000 chars | Split into multiple messages |
|
|
664
|
+
| `no_text` | Empty `text` and no `blocks` | Always provide `text` as fallback |
|
|
665
|
+
| `restricted_action` | Workspace admin has blocked this action | Contact workspace admin |
|
|
666
|
+
| `is_archived` | Channel is archived | Unarchive or use a different channel |
|
|
667
|
+
| `thread_not_found` | `thread_ts` doesn't match any message | Verify `ts` is stored as a string, not float |
|
|
668
|
+
| `ekm_access_denied` | Enterprise Key Management restriction | Use an org-approved token |
|
|
669
|
+
|
|
670
|
+
### "If you see X, do Y" playbook
|
|
671
|
+
|
|
672
|
+
- **Bot posts in the wrong channel / no message appears:** Verify the `channel` value is a channel ID (`C...`) not a name. Channel names are not accepted by `chat.postMessage`.
|
|
673
|
+
- **Messages appear in the channel but with broken formatting:** You passed `text` with `*bold*` or `_italic_` — use `mrkdwn` format (Slack's dialect), not standard Markdown. Key differences: `*bold*`, `_italic_`, `~strike~`, `<URL|link text>`, `<@U01...>` mentions.
|
|
674
|
+
- **Events API URL fails verification:** Your endpoint didn't return `{"challenge": ...}` within 3 seconds, or returned incorrect content. Ensure JSON response with `Content-Type: application/json`.
|
|
675
|
+
- **Events arrive but signature verification fails:** Make sure you're reading the raw request body (not parsed JSON) for HMAC calculation. Some frameworks re-encode the body — use the raw bytes.
|
|
676
|
+
- **`files.upload` returns `ok: false`:** The old upload method is deprecated. Switch to `files.getUploadURLExternal` + `files.completeUploadExternal`.
|
|
677
|
+
- **`chat.update` returns `cant_update_message`:** Only the bot that originally posted the message can update it. You cannot update messages posted by other bots or users.
|
|
678
|
+
- **Thread replies not appearing in thread:** You passed `thread_ts` correctly but used a different channel ID. The `channel` and `thread_ts` must both match the original message.
|
|
679
|
+
|
|
680
|
+
### Debug logging
|
|
681
|
+
|
|
682
|
+
Log: API method, channel/user ID, response `ok` field, `error` field (if any), HTTP status, `Retry-After` (on 429).
|
|
683
|
+
Do NOT log: token values, full message text (may contain PII), file contents.
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
## Security, privacy, compliance
|
|
688
|
+
|
|
689
|
+
- **Least privilege:** Request only the scopes your app needs. Slack's permission model is per-scope, not per-channel — a `channels:history` scope grants access to all public channels the bot is in.
|
|
690
|
+
- **Token storage:** Never expose bot tokens in client-side code, logs, or URLs. Store in environment variables or a secrets manager. Bot tokens do not expire but can be revoked — implement `auth.test` health checks.
|
|
691
|
+
- **Signing secret:** Always verify `X-Slack-Signature` on all incoming webhooks and Events API payloads. Do not process unsigned requests.
|
|
692
|
+
- **DMs and private channels:** Private messages are sensitive. Minimize bot access to DMs and private channels; request only the scopes needed. Slack Enterprise Grid adds Data Loss Prevention (DLP) controls.
|
|
693
|
+
- **User data (PII):** `users.list` and `users.info` return display names, real names, and email addresses. Handle this data per your privacy policy — do not store more than necessary.
|
|
694
|
+
- **Message content:** Slack messages can contain PII, credentials, or confidential data. Treat message content as sensitive; do not log full message text in production.
|
|
695
|
+
- **Audit logs:** Slack Enterprise Grid provides an Audit Logs API (`audit.logs.v1`) for compliance logging. Standard Slack does not expose per-workspace audit logs via API.
|
|
696
|
+
- **Bot impersonation:** Never use `as_user: true` with a user token to post as a human user in automated flows — this is misleading and may violate your workspace's acceptable use policy.
|
|
697
|
+
- **GDPR:** Slack supports data export requests. If a user requests deletion, messages they sent cannot be deleted via API by bots (only the user or workspace admin can delete messages). Plan for this in your data retention policy.
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
## Testing checklist
|
|
702
|
+
|
|
703
|
+
- [ ] **Auth test:** `GET https://slack.com/api/auth.test` → `ok: true`, bot name and team visible.
|
|
704
|
+
- [ ] **Post message test:** `chat.postMessage` to a test channel → message appears with correct formatting; both `text` and `blocks` render correctly.
|
|
705
|
+
- [ ] **Thread test:** Reply to the posted message using `thread_ts` → reply appears in thread, not in main channel.
|
|
706
|
+
- [ ] **DM test:** `conversations.open` with a test user ID → `chat.postMessage` to returned DM channel → message arrives.
|
|
707
|
+
- [ ] **User lookup test:** `users.lookupByEmail` with a valid email → returns correct `user_id`.
|
|
708
|
+
- [ ] **Channel list test:** `conversations.list` with pagination → all channels returned; cursor exhausted.
|
|
709
|
+
- [ ] **History test:** `conversations.history` with `oldest` param → only messages after cutoff returned.
|
|
710
|
+
- [ ] **File upload test:** Full three-step upload flow → file appears in channel with comment.
|
|
711
|
+
- [ ] **Reaction test:** `reactions.add` → emoji visible on message; calling again returns `already_reacted` (not an error).
|
|
712
|
+
- [ ] **Rate limit test:** Send >1 req/sec to the same channel → 429 received → `Retry-After` respected → retry succeeds.
|
|
713
|
+
- [ ] **Events API test:** Post a message in a subscribed channel → event received at your endpoint within 3s → `event_id` logged; posting same message again doesn't reprocess.
|
|
714
|
+
- [ ] **Signature verification test:** Send request with invalid signature → 403 returned.
|
|
715
|
+
- [ ] **Negative tests:**
|
|
716
|
+
- Bot not in channel → `not_in_channel`
|
|
717
|
+
- Invalid token → `invalid_auth`
|
|
718
|
+
- Non-existent channel ID → `channel_not_found`
|
|
719
|
+
- `thread_ts` stored as float → `thread_not_found` (must be string)
|
|
720
|
+
- Missing `text` field with blocks → `no_text`
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
## Sources
|
|
725
|
+
|
|
726
|
+
- Slack Web API methods: https://api.slack.com/methods
|
|
727
|
+
- Authentication & token types: https://api.slack.com/authentication/token-types
|
|
728
|
+
- OAuth scopes: https://api.slack.com/scopes
|
|
729
|
+
- Block Kit (rich message formatting): https://api.slack.com/block-kit
|
|
730
|
+
- Block Kit Builder (interactive): https://app.slack.com/block-kit-builder
|
|
731
|
+
- Events API: https://api.slack.com/events-api
|
|
732
|
+
- Signing secrets & request verification: https://api.slack.com/authentication/verifying-requests-from-slack
|
|
733
|
+
- Rate limits: https://api.slack.com/docs/rate-limits
|
|
734
|
+
- File upload (new API): https://api.slack.com/methods/files.getUploadURLExternal
|
|
735
|
+
- Conversations API: https://api.slack.com/docs/conversations-api
|
|
736
|
+
- Pagination: https://api.slack.com/docs/pagination
|
|
737
|
+
- Incoming Webhooks: https://api.slack.com/messaging/webhooks
|
|
738
|
+
- Socket Mode: https://api.slack.com/apis/socket-mode
|
|
739
|
+
- Enterprise Grid Audit Logs API: https://api.slack.com/admins/audit-logs
|
|
740
|
+
- mrkdwn formatting reference: https://api.slack.com/reference/surfaces/formatting
|