Claude API Slack Bot Integration Example
What you'll build
This article walks through a working example of a Slack bot powered by the Claude API — the kind of integration people usually mean when they search for "claude api slack bot integration example." You'll get a bot that responds to mentions (@claude) and a slash command (/ask), sends the message to Claude, and posts the reply back into the channel.
The stack is deliberately simple: Node.js, Slack's Bolt.js framework, and an HTTPS call to a Claude-compatible endpoint. The same pattern works whether you call Anthropic's API directly or route through a proxy like SubToAPI for key management and usage tracking — the Slack-side code doesn't change.
Prerequisites
- A Slack workspace where you can install a custom app
- Node.js 18+
- An API key for Claude (Anthropic key, or a
sub_live_...key from SubToAPI) @slack/boltinstalled via npm
npm install @slack/bolt node-fetch
Step 1: Create the Slack app
In the Slack API dashboard, create a new app and enable:
- Event Subscriptions → subscribe to
app_mention - Slash Commands → add
/ask - Bot Token Scopes →
chat:write,app_mentions:read,commands
Install the app to your workspace and grab the Bot User OAuth Token and Signing Secret.
Step 2: Set up the Bolt app
const { App } = require('@slack/bolt');
const fetch = require('node-fetch');
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
socketMode: false,
});
async function askClaude(message) {
const res = await fetch('https://api.subtoapi.app/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 500,
messages: [{ role: 'user', content: message }],
}),
});
const data = await res.json();
return data.content[0].text;
}
This example calls /v1/messages on SubToAPI, which mirrors the standard Claude Messages API shape — see /docs/messages for the full request/response reference. If you're calling Anthropic directly instead, swap the URL and header but keep the rest identical.
Step 3: Handle mentions
app.event('app_mention', async ({ event, say }) => {
const text = event.text.replace(/<@[^>]+>/, '').trim();
const reply = await askClaude(text);
await say({ text: reply, thread_ts: event.ts });
});
Replying in a thread (thread_ts) keeps channels readable, which matters once more than a couple of people start using the bot.
Step 4: Handle the slash command
app.command('/ask', async ({ command, ack, respond }) => {
await ack();
const reply = await askClaude(command.text);
await respond({ text: reply, response_type: 'in_channel' });
});
Start the app:
(async () => {
await app.start(process.env.PORT || 3000);
console.log('Slack bot running');
})();
Slash commands have a 3-second acknowledgment window, so ack() must fire immediately — the actual Claude call happens afterward and the result is sent via respond().
Handling latency and timeouts
Claude responses for longer prompts can take several seconds. Two practical fixes:
- Acknowledge fast, respond later. Already covered above —
ack()thenrespond()asynchronously. - Stream and edit the message. Slack supports updating a message via
chat.update. You can post a placeholder ("Thinking...") immediately, then stream tokens from Claude and periodically update the message text. This avoids the perception of a frozen bot. If you want the mechanics of token-by-token handling, that's covered separately — see/docs/streamingfor how streaming responses are structured on SubToAPI.
app.event('app_mention', async ({ event, client }) => {
const placeholder = await client.chat.postMessage({
channel: event.channel,
thread_ts: event.ts,
text: 'Thinking...',
});
const reply = await askClaude(event.text);
await client.chat.update({
channel: event.channel,
ts: placeholder.ts,
text: reply,
});
});
Adding conversation memory
A single-turn bot forgets context between messages, which feels broken in a thread. The simplest fix is to pass the thread history back as messages:
async function askClaudeWithHistory(history) {
const res = await fetch('https://api.subtoapi.app/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 500,
messages: history, // [{role: 'user', content}, {role: 'assistant', content}, ...]
}),
});
const data = await res.json();
return data.content[0].text;
}
Fetch thread replies with conversations.replies, map them to role: 'user' or role: 'assistant' based on whether the sender is the bot, and pass the array in. Cap it at the last 10–15 messages to control token usage.
Why route through SubToAPI instead of calling Anthropic directly
For a single internal bot, calling Anthropic directly is fine. It gets more useful once you have multiple bots, environments, or teammates:
- Each Slack bot (staging, production, a side project) can get its own
sub_live_...key instead of sharing one raw Anthropic key across codebases - Usage per key shows up in one dashboard, so you can see which bot is burning tokens
- Team members can be added as seats without handing out the underlying Anthropic credential
Setup takes the same five minutes as swapping an API URL. Start from /signup, grab a key, and check /docs/quickstart for the minimal request format. Plans are listed at /pricing (Solo €9, Team €19/seat, Scale €49/seat), with a free trial to test the bot before committing.
Deploying
For production, run the bot behind a process manager (PM2, systemd) or as a container on a small VM/PaaS. If you're not using Socket Mode, you'll need a public HTTPS endpoint for Slack's Events API to reach — a reverse proxy with TLS (Caddy, nginx) in front of the Node process is the usual setup.
questions
Do I need Socket Mode or HTTP Events for a Claude Slack bot? Either works. Socket Mode avoids exposing a public endpoint and is easier for internal tools; HTTP Events is more standard for production deployments behind your own domain.
How do I stop the bot from exceeding Slack's response time limits? Acknowledge slash commands within 3 seconds using ack(), then send the actual Claude reply asynchronously via respond() or a follow-up chat.postMessage call.
Can tool use (function calling) work inside a Slack bot? Yes — pass tools in the same request body you send to the Messages endpoint. See /docs/tools for the schema; the Slack-side handling is unchanged, you just parse tool_use blocks from the response before replying.