Claude API Slack Bot Integration Guide
Building a Slack bot that talks to Claude means wiring together three things: a Slack app that listens for messages, a backend that calls the Claude API, and a way to get answers back into Slack fast enough to satisfy Slack's response window. This guide walks through the full setup, from creating the Slack app to handling threaded conversations and streaming-style updates.
If you just want the shortest path to a working bot, the flow looks like this: create a Slack app with Socket Mode or Events API enabled, stand up a small server (Node.js works well with Slack's Bolt SDK), forward incoming messages to Claude, and post the reply back to the same channel or thread. The rest of this article covers each step and the edge cases that trip people up.
Setting Up the Slack App
Start at api.slack.com/apps and create a new app "from scratch." You'll need:
- Bot Token Scopes:
chat:write,app_mentions:read,channels:history(orim:historyfor DMs) - Event Subscriptions: subscribe to
app_mentionand optionallymessage.imfor direct messages - Socket Mode (recommended for development): avoids needing a public HTTPS endpoint while you build
Install the app to your workspace and grab the Bot User OAuth Token (xoxb-...). If you're using Socket Mode, also generate an app-level token (xapp-...).
Handling the 3-Second Response Rule
Slack expects an HTTP 200 acknowledgment within 3 seconds of an event, but Claude responses — especially longer ones — often take several seconds to generate. The standard pattern is:
- Acknowledge the Slack event immediately
- Post a placeholder message ("Thinking...") or use
chat.postMessageafter the fact - Call Claude asynchronously
- Update the placeholder with
chat.update, or post a new message once the response is ready
Using Bolt, this looks like:
const { App } = require('@slack/bolt');
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
appToken: process.env.SLACK_APP_TOKEN,
socketMode: true,
});
app.event('app_mention', async ({ event, client, say }) => {
const placeholder = await say({ text: 'Thinking...', thread_ts: event.ts });
const reply = await askClaude(event.text);
await client.chat.update({
channel: event.channel,
ts: placeholder.ts,
text: reply,
});
});
app.start();
Keeping the reply inside thread_ts avoids cluttering the channel and preserves conversational context for anyone scrolling back.
Calling Claude from the Bot
The askClaude function is where you send the user's message to the Claude API. If you're managing your own Anthropic account and keys directly, this involves handling model versions, rate limits, and usage tracking yourself. Many teams instead route requests through SubToAPI, which exposes Claude access as a straightforward HTTPS API with application keys (sub_live_...), so the Slack bot doesn't need direct access to your Anthropic credentials — useful if multiple internal tools share the same Claude subscription.
async function askClaude(text) {
const res = 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: 800,
messages: [{ role: 'user', content: text }],
}),
});
const data = await res.json();
return data.content[0].text;
}
The request shape matches Anthropic's Messages API, so if you've used Claude directly before, nothing here should feel unfamiliar. Full request and response details are in the Messages API docs.
Maintaining Conversation Context
Slack threads map naturally onto Claude's conversation format. Rather than sending a single message each time, fetch the thread history with conversations.replies and build a messages array:
const history = await client.conversations.replies({
channel: event.channel,
ts: event.thread_ts || event.ts,
});
const messages = history.messages.map((m) => ({
role: m.bot_id ? 'assistant' : 'user',
content: m.text,
}));
Pass this array as the messages field instead of a single string. Keep in mind Slack message history includes formatting artifacts (mentions like <@U123>) that you'll want to strip before sending to Claude, otherwise responses can get confused by raw Slack markup.
Streaming-Style Updates
Claude's API supports streaming responses, but Slack doesn't have a native "typing" stream for bot messages the way a chat UI would. The common workaround is to buffer the stream in chunks and call chat.update every second or two, giving users a sense of progress without hammering Slack's rate limits (roughly one update per second is a safe ceiling per message). If your bot doesn't need real-time feel, it's simpler to wait for the full response and post once — see the streaming guide for details on consuming streamed output if you decide to implement it.
Adding Tool Use
If your bot needs to look things up — search a knowledge base, query a ticketing system, check a calendar — Claude's tool use lets the model request a function call mid-conversation instead of guessing. Define tools in your request, check the response for tool_use blocks, execute the corresponding function in your bot's backend, and send the result back as a tool_result. This is what turns a Slack bot from "answers questions from memory" into "actually does things in your workspace." The tool use docs cover the request/response shape in detail.
Deploying to Production
Socket Mode is fine for development but for production most teams switch to the Events API with a public endpoint, since it scales better across multiple instances and doesn't require a persistent WebSocket connection per bot process. Whichever mode you use, keep an eye on:
- Rate limits: both Slack's API and your Claude usage tier
- Error handling: post a friendly fallback message if Claude times out
- Secrets: store Slack and Claude tokens in environment variables or a secrets manager, never in code
If you're prototyping quickly, signing up for a Claude API proxy with per-key usage tracking makes it easier to see exactly how much the Slack bot is costing without digging through Anthropic's console — check pricing for plan details.
FAQ
Do I need a separate Anthropic account for a Slack bot? No. You can use any valid Claude API access, including a proxied key from a service like SubToAPI, as long as your bot's backend can make authenticated HTTPS requests.
How do I keep the bot from responding to every message in a channel? Subscribe only to app_mention events (and DMs separately) rather than message.channels, so the bot only replies when explicitly addressed.
Can the bot handle long Claude responses in Slack? Yes, but Slack messages have a ~4,000 character limit per block. Split long responses across multiple chat.postMessage calls or use Block Kit sections for readability.