How to Connect Claude API to a Slack Bot
Connecting Claude to a Slack bot means building a small server that listens for Slack events (like a mention or a DM), forwards the message text to Claude's messages endpoint, and posts the reply back into the channel or thread. There's no native Slack-to-Claude integration — you're wiring two APIs together yourself, and the whole thing can be running in under an hour once you understand the pieces.
This guide walks through the full path: creating a Slack app, subscribing to the right events, verifying requests, calling Claude, and posting responses back — including how to handle Slack's 3-second acknowledgement window, which is the part most people get stuck on.
The architecture in one sentence
Slack sends an HTTP POST to your server whenever your bot is mentioned or messaged → your server extracts the text → your server calls the Claude API → your server posts the response back to Slack using the chat.postMessage API (not the original webhook response, because Claude takes longer than 3 seconds to reply).
That last point matters. Slack's Events API requires you to acknowledge the incoming event within 3 seconds, but a Claude completion can take several seconds longer, especially for longer responses. So the pattern is: acknowledge immediately with a 200, then call Claude asynchronously and post the result with a separate API call.
Step 1: Create the Slack app
- Go to
api.slack.com/appsand create a new app "from scratch." - Under OAuth & Permissions, add these bot token scopes:
chat:writeapp_mentions:readim:history(if you want DMs to work)
- Install the app to your workspace and copy the Bot User OAuth Token (starts with
xoxb-). - Under Event Subscriptions, enable events and set your Request URL to your server's endpoint (e.g.
https://yourapp.com/slack/events). Subscribe toapp_mentionandmessage.im.
Slack will send a verification challenge to your endpoint the first time you save this — your server needs to echo back the challenge value to confirm ownership.
Step 2: Set up the server
A minimal Express server handling Slack's URL verification and events:
import express from "express";
import { WebClient } from "@slack/web-api";
const app = express();
app.use(express.json());
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
app.post("/slack/events", async (req, res) => {
if (req.body.type === "url_verification") {
return res.send(req.body.challenge);
}
res.sendStatus(200); // acknowledge immediately
const event = req.body.event;
if (event && event.type === "app_mention") {
handleMention(event);
}
});
async function handleMention(event) {
const reply = await askClaude(event.text);
await slack.chat.postMessage({
channel: event.channel,
thread_ts: event.ts,
text: reply,
});
}
app.listen(3000);
The important detail here is res.sendStatus(200) happening before askClaude resolves. If you await Claude before responding to Slack, Slack will time out and retry the same event, and you'll end up processing it multiple times.
Step 3: Call Claude
The askClaude function is just a call to the Messages API. If you're calling Anthropic directly, that means managing an API key, request signing, and your own retry/error handling. If you're using SubToAPI, the same call works over HTTPS with a sub_live_ key and standard Bearer auth:
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-5",
max_tokens: 1024,
messages: [{ role: "user", content: text.replace(/<@\w+>/, "").trim() }],
}),
});
const data = await res.json();
return data.content[0].text;
}
Note the .replace(/<@\w+>/, "") — Slack includes the bot's user ID mention (<@U12345>) at the start of the message text, so you need to strip it before sending the text to Claude.
This setup is useful if you're building an internal Slack bot on a team plan and don't want every developer holding a raw Anthropic key. With SubToAPI's application keys you can issue one key per bot or per environment, see usage per key in the dashboard, and rotate a compromised key without breaking your whole Slack integration. Check /docs/quickstart for the auth flow and /docs/messages for the full request schema.
Step 4: Handle threads and context
Slack conversations are threaded, and Claude has no memory between requests — each call is stateless. If you want the bot to remember earlier messages in a thread, you need to fetch the thread history from Slack (conversations.replies) and pass it into the messages array yourself:
const history = await slack.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,
}));
This is the same pattern as any chat app — you're reconstructing conversation state on every turn because the API itself doesn't store it.
Step 5: Streaming (optional)
Slack messages can be edited after posting, which lets you fake streaming: post an initial "thinking…" message, then use chat.update to rewrite it as tokens arrive. This is a nice UX touch for longer responses but adds complexity — most teams skip it for internal bots and only bother once response length starts feeling slow. If you do want it, /docs/streaming covers how streamed responses are structured on the SubToAPI side.
Common mistakes to avoid
- Not stripping the bot mention from the message text before sending it to Claude — you'll get replies that reference
<@U12345>literally. - Awaiting Claude before acknowledging Slack — causes duplicate event processing.
- Ignoring
bot_idon incoming messages — without this check your bot can end up replying to itself in a loop. - Hardcoding the Anthropic key in the same process as your Slack token — separate credentials make key rotation and auditing much easier later.
Wrapping up
The core integration is small: an events endpoint, a call to Claude, and a chat.postMessage call back. Most of the real work is in message formatting, thread context, and making sure you acknowledge Slack fast enough. If you'd rather not manage Anthropic credentials directly across your bot and other internal tools, /signup gets you a SubToAPI key in a couple of minutes, and /pricing has the plan breakdown if you're running this for a team.
questions
Do I need an Anthropic account to build a Slack bot with Claude? Yes, you need access to Claude's API somehow — either directly through Anthropic or through a proxy service like SubToAPI that issues its own API keys against your existing access.
Can the Slack bot remember previous messages in a thread? Not automatically. Claude is stateless per request, so you need to fetch the thread history from Slack's conversations.replies API and include it in the messages array on each call.
Why does my bot reply late or not at all sometimes? Usually because your server is awaiting the Claude response before acknowledging Slack's event, which causes a timeout and retry. Always send a 200 response first, then process the Claude call asynchronously.