Claude API Integration with Slack Bot: A Dev Guide
Claude API Integration with Slack Bot: A Dev Guide
If you're trying to wire Claude into a Slack bot, you need three pieces working together: a Slack app that can receive mentions and DMs, a backend that forwards those messages to Claude, and a reliable way to call the Claude API without your key living in a Slack app's environment variables or a shared Heroku dyno. This guide covers all three, with working code for the Slack event handling and the Claude request itself.
The short answer: create a Slack app with bot token scopes and Events API subscriptions, stand up a small HTTP endpoint that verifies Slack's signature and responds within 3 seconds, then call Claude asynchronously and post the reply back with chat.postMessage. The tricky parts are Slack's response-time limit, thread context, and streaming — all addressed below.
Architecture overview
A Slack-to-Claude bot has four moving parts:
- Slack App — registered at api.slack.com/apps, with Bot Token Scopes (
chat:write,app_mentions:read,im:historyif you want DMs) and Event Subscriptions pointed at your server. - Event receiver — an HTTPS endpoint that verifies the Slack signing secret and acknowledges events immediately.
- Claude call — your backend sends the conversation (plus any thread history) to Claude and gets a response.
- Reply delivery — posting the Claude response back into the same channel or thread via the Slack Web API.
Slack requires your event endpoint to respond within 3 seconds, but Claude generation routinely takes longer, especially for longer answers. So step 2 and step 3 need to be decoupled: acknowledge the Slack event instantly, then process and post the reply asynchronously (a queue, a background worker, or just an unawaited async function, depending on your stack).
Setting up the Slack app
In the Slack app config:
- Enable Event Subscriptions, add your endpoint URL, and subscribe to
app_mention(andmessage.imfor DMs). - Add Bot Token Scopes:
chat:write,app_mentions:read,im:history,im:write. - Install the app to your workspace and grab the Bot User OAuth Token (
xoxb-...).
Handling Slack events
A minimal Node/Express handler that verifies the request and acknowledges fast:
import crypto from "crypto";
app.post("/slack/events", express.raw({ type: "*/*" }), (req, res) => {
const signature = req.headers["x-slack-signature"];
const timestamp = req.headers["x-slack-request-timestamp"];
const base = `v0:${timestamp}:${req.body}`;
const hmac = crypto
.createHmac("sha256", process.env.SLACK_SIGNING_SECRET)
.update(base)
.digest("hex");
if (`v0=${hmac}` !== signature) return res.status(401).send("invalid signature");
const payload = JSON.parse(req.body);
if (payload.type === "url_verification") {
return res.send(payload.challenge);
}
res.status(200).send(); // ack immediately
handleEvent(payload.event); // process async
});
The key detail: res.status(200).send() happens before you do anything with Claude. Slack retries events it thinks failed, and a slow Claude response will cause duplicate messages if you don't ack first.
Calling Claude from the event handler
Once you have the event, extract the text (stripping the <@BOT_ID> mention), and send it to Claude. If you're using SubToAPI as your Claude access layer, the request looks like a standard Messages call:
async function handleEvent(event) {
if (event.bot_id) return; // ignore bot's own messages
const text = event.text.replace(/<@[^>]+>\s*/, "");
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
messages: [{ role: "user", content: text }],
}),
});
const data = await response.json();
const reply = data.content[0].text;
await postToSlack(event.channel, reply, event.thread_ts || event.ts);
}
Posting back is a standard chat.postMessage call with the Slack bot token, using thread_ts so replies stay threaded rather than cluttering the channel.
Thread context matters more than you'd think
Slack threads are the natural conversation boundary for a Claude bot. When a user replies in a thread, fetch the thread history with conversations.replies and pass it as the messages array to Claude, mapping Slack users to user role and your bot to assistant. Without this, every message looks like a fresh conversation and the bot "forgets" what was just said — a common complaint in early Slack bot builds. The full request/response shape for multi-turn conversations is covered in the Messages docs.
Streaming into Slack
Slack doesn't support token-by-token streaming in a message the way a chat UI does, but you can still get a responsive feel by editing the message as chunks arrive: post a placeholder ("Thinking…"), then call chat.update every second or two with the accumulated text as Claude streams. If your Claude access point supports streaming responses, see streaming for the event format — you'll buffer deltas client-side and batch the Slack updates to avoid hitting Slack's rate limits on chat.update.
Tool use for real actions
A lot of Slack bots end up doing more than answering questions — looking up a ticket, querying a database, posting to a calendar. That's where Claude's tool use comes in: define tools (functions) the model can call, and your bot executes them server-side before returning the final answer to Slack. The tools docs walk through tool schemas and multi-step tool loops in detail.
Key management and multi-bot setups
If you're running more than one Slack bot, or a bot alongside other internal tools, managing raw Anthropic keys gets messy fast — rotating one key means touching every integration that uses it. SubToAPI issues per-application sub_live_... keys from a single underlying Claude subscription, so your Slack bot, internal dashboard, and any other integration each get their own revocable key with separate usage visibility. Setup takes about five minutes — see the quickstart — and plans start at €9/month with a free trial at signup.
Questions
Does the Slack event endpoint need to respond before calling Claude? Yes. Slack requires a 200 response within 3 seconds or it retries the event, which can cause duplicate replies. Acknowledge immediately, then process the Claude call asynchronously.
How do I keep Claude aware of Slack thread context? Fetch the thread's message history with conversations.replies, map Slack users and your bot to user/assistant roles, and send that array as the messages payload on each Claude request.
Can a Slack bot stream Claude's response token by token? Slack messages can't stream natively, but you can simulate it by posting a placeholder message and calling chat.update periodically as the response streams in, batching updates to respect Slack's rate limits.