How to Integrate Claude API with a Slack Bot
Integrating Claude with a Slack bot means wiring three pieces together: a Slack app that listens for messages or mentions, a backend endpoint that forwards those messages to Claude, and a reply path that posts Claude's response back into the channel. The whole thing can run on a single small server or serverless function — there's no need for a heavy framework.
This guide walks through the exact steps: creating the Slack app, setting up event subscriptions, calling Claude from your handler, and dealing with the practical issues that trip people up — Slack's 3-second ack window, streaming long replies, threading, and keeping API credentials out of your codebase.
Step 1: Create the Slack App
Go to api.slack.com/apps and create a new app "from scratch." You'll need:
- Bot Token Scopes:
app_mentions:read,chat:write, andchannels:historyif you want the bot to read channel context. - Event Subscriptions: subscribe to
app_mention(bot is @-mentioned) ormessage.channels(bot reads every message in channels it's invited to). - Interactivity (optional): enable if you want buttons or slash command responses.
Install the app to your workspace and copy the Bot User OAuth Token (xoxb-...) and the Signing Secret — you'll need both in your backend.
Step 2: Set Up Your Event Handler
Slack's Events API sends an HTTP POST to a URL you control whenever a subscribed event fires. Using Slack's official Bolt framework for Node.js keeps boilerplate minimal:
const { App } = require('@slack/bolt');
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
socketMode: false,
});
app.event('app_mention', async ({ event, say }) => {
const userText = event.text.replace(/<@[^>]+>/, '').trim();
const reply = await askClaude(userText);
await say({ text: reply, thread_ts: event.ts });
});
app.start(process.env.PORT || 3000);
The critical detail here: Slack expects an HTTP 200 within 3 seconds of sending the event, or it will retry the request (and you'll get duplicate replies). Bolt handles the ack automatically, but your Claude call still needs to happen asynchronously if it's slow — don't block the event loop waiting on a long completion before responding.
Step 3: Call Claude from the Handler
This is where you decide how to reach Claude. You can call the Anthropic API directly, or go through a gateway like SubToAPI that turns your existing Claude access into a standard HTTPS endpoint with its own API key, usage metadata, and dashboard — useful if multiple apps or team members need access without each one managing its own credentials.
async function askClaude(prompt) {
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: prompt }],
}),
});
const data = await res.json();
return data.content[0].text;
}
The request/response shape follows Anthropic's Messages API format, so if you're already familiar with it, there's nothing new to learn — see /docs/messages for the full schema and /docs/quickstart if you're starting from zero.
Handling Long Responses with Streaming
Slack messages that arrive all at once after a 10-second wait feel sluggish. A better pattern is to post an initial placeholder message, then edit it progressively as Claude streams tokens back:
app.event('app_mention', async ({ event, client }) => {
const userText = event.text.replace(/<@[^>]+>/, '').trim();
const placeholder = await client.chat.postMessage({
channel: event.channel,
thread_ts: event.ts,
text: 'Thinking…',
});
let buffer = '';
await streamClaude(userText, async (chunk) => {
buffer += chunk;
await client.chat.update({
channel: event.channel,
ts: placeholder.ts,
text: buffer,
});
});
});
In practice, editing on every token is too chatty for Slack's rate limits — batch updates every 300–500ms or every N characters instead. If you're using SubToAPI, streaming works through the same Messages endpoint with "stream": true; see /docs/streaming for the event format.
Threading, Context, and Multi-Turn Conversations
Slack threads map naturally onto Claude conversations. Use thread_ts as your conversation key and store a rolling message history (user + assistant turns) keyed to that thread — in Redis, a database, or even an in-memory map for low-traffic bots. Trim or summarize older turns once you approach your model's context window, and always send the full accumulated history as the messages array on each call, since Claude's API is stateless between requests.
Tool Use for Richer Bots
If your Slack bot needs to do more than chat — look up a ticket, query a database, post to another system — Claude's tool use lets you define functions the model can call mid-conversation, then you execute them and return results. This is useful for Slack bots that act as a front end to internal tooling. See /docs/tools for the request format and multi-step tool call handling.
Securing the Integration
- Verify Slack's signing secret on every incoming request to confirm it actually came from Slack.
- Keep your Claude or SubToAPI key server-side only — never expose it in client-side code or logs.
- If multiple teammates build against the same Claude access, a shared API key with per-key usage tracking (available on SubToAPI's Team and Scale plans, see /pricing) avoids everyone minting personal credentials.
Deployment Checklist
- [ ] Slack app created with correct scopes and event subscriptions
- [ ] Signing secret verification in place
- [ ] Claude (or SubToAPI) key stored as an environment variable, not hardcoded
- [ ] 3-second ack handled before any long-running Claude call
- [ ] Thread-based conversation history stored and trimmed
- [ ] Streaming or chunked updates for long replies
Start with the mention-and-reply flow above, get it working end to end, then layer in streaming, threading, and tools once the basics are solid.
Questions
Do I need a server, or can this run serverless? Either works. Slack just needs an HTTPS endpoint that responds within 3 seconds — AWS Lambda, Vercel functions, or a small always-on Node server all satisfy that, as long as you handle the Claude call asynchronously if it's slow.
Can I use Claude's API directly instead of a gateway like SubToAPI? Yes — the integration pattern is identical either way, since SubToAPI exposes the same Messages API shape (see /docs/messages). A gateway mainly helps when you want application-scoped keys, usage visibility, and team seats without each developer holding separate credentials.
How do I avoid duplicate replies from Slack retries? Always return a 200 response immediately after receiving the event, before calling Claude, and do the actual model call and reply asynchronously. If you can't avoid a slow synchronous path, track event IDs you've already processed and ignore repeats.