Claude API Slack App Integration Guide
If you want to put Claude inside Slack — answering questions in a channel, summarizing threads, or responding to a slash command — you need three pieces working together: a Slack app with the right permissions, a small backend that receives Slack events, and a call out to Claude's API to generate the response. This guide walks through all three, from Slack app creation to a working /ask command that streams Claude's answer back into a thread.
The short version: create a Slack app with Events API or slash commands enabled, stand up an HTTPS endpoint (Express, Cloudflare Workers, whatever you prefer) that verifies Slack's signature, forward the user's message to Claude, and post the reply back using Slack's chat.postMessage API. The rest of this article fills in the details that usually trip people up — verification, threading, rate limits, and streaming.
Setting up the Slack app
- Go to api.slack.com/apps and create a new app "from scratch."
- Under OAuth & Permissions, add these bot token scopes:
chat:writecommands(if using a slash command)app_mentions:read(if you want the bot to respond to @mentions)
- Install the app to your workspace and copy the Bot User OAuth Token (starts with
xoxb-). - Under Slash Commands, create
/askand point its request URL to your backend, e.g.https://yourapp.com/slack/command. - Under Event Subscriptions, enable events if you want mention-based responses, and subscribe to
app_mention. - Copy the Signing Secret — you'll use it to verify every incoming request.
Slack requires your endpoint to respond within 3 seconds, and it will retry if you don't. For anything involving an LLM call, you need to acknowledge immediately and send the real response asynchronously.
Verifying requests from Slack
Every request from Slack includes a signature you must verify before processing it, otherwise anyone can POST to your endpoint and spoof messages.
import crypto from "crypto";
function verifySlackSignature(req, signingSecret) {
const timestamp = req.headers["x-slack-request-timestamp"];
const sig = req.headers["x-slack-signature"];
const base = `v0:${timestamp}:${req.rawBody}`;
const hmac = crypto
.createHmac("sha256", signingSecret)
.update(base)
.digest("hex");
const expected = `v0=${hmac}`;
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
Make sure your framework exposes the raw request body — most JSON body parsers strip it before you can hash it, which is the most common bug in Slack integrations.
Handling the slash command: ack fast, respond later
app.post("/slack/command", async (req, res) => {
if (!verifySlackSignature(req, process.env.SLACK_SIGNING_SECRET)) {
return res.status(401).send("invalid signature");
}
const { text, response_url, user_id, channel_id } = req.body;
// Acknowledge immediately
res.status(200).send();
// Process async and post back via response_url or chat.postMessage
const answer = await askClaude(text);
await fetch(response_url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: answer, response_type: "in_channel" }),
});
});
The response_url Slack gives you is valid for about 30 minutes, so you have plenty of room to call Claude and even retry on failure.
Calling Claude
This is where SubToAPI simplifies things. If you already have Claude access and don't want to manage separate Anthropic billing, keys, and usage tracking just for this one integration, SubToAPI gives you an HTTPS API key (sub_live_...) you drop straight into your Slack backend:
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-opus-4-1",
max_tokens: 500,
messages: [{ role: "user", content: prompt }],
}),
});
const data = await res.json();
return data.content[0].text;
}
This is a standard request/response call — see /docs/messages for the full parameter list. If you'd rather use raw Anthropic credentials and manage keys yourself, the shape of the request is nearly identical; SubToAPI mainly adds unified billing, team seats, and usage metadata on top so you're not reconciling separate invoices for a Slack bot versus everything else you build on Claude.
Streaming into a thread
Slack doesn't support token-by-token streaming in a single message the way a chat UI does, but you can simulate progressive updates by editing a message as new chunks arrive. If you want true streaming for a different surface (like a web dashboard tied to the same Slack workflow), see /docs/streaming for how SubToAPI exposes server-sent events over the same /v1/messages endpoint with "stream": true.
For Slack specifically, a practical pattern is:
let message = await client.chat.postMessage({
channel: channel_id,
thread_ts: thread_ts,
text: "Thinking…",
});
// buffer stream chunks, then edit every ~500ms
await client.chat.update({
channel: channel_id,
ts: message.ts,
text: accumulatedText,
});
This gives users a sense of progress without hitting Slack's rate limits on chat.update (roughly one call per second per channel is safe).
Threading and context
Keep conversations coherent by tracking thread_ts and feeding prior messages back into Claude as conversation history. Store the last N messages per thread in Redis or a simple key-value table, and pass them as the messages array on each call — Claude has no memory between requests, so the thread history has to be reconstructed and sent every time.
Handling tool calls from Slack
If your bot needs to look something up (search a knowledge base, hit an internal API, query a database) before answering, use Claude's tool use feature rather than trying to parse free text. Define the tool schema, send it with the request, and when Claude returns a tool call, execute it on your side and send the result back in a follow-up message. See /docs/tools for the schema format and multi-turn tool flow.
Production checklist
- Verify every request's signature, including retries
- Acknowledge within 3 seconds, process async
- Store and replay thread history for context
- Rate-limit per-user to avoid runaway costs from chatty channels
- Log token usage per workspace if you're billing internally — SubToAPI's dashboard gives you this per-key automatically
Getting from zero to a working bot is mostly Slack boilerplate; the Claude call itself is a handful of lines. Start with /docs/quickstart if you're setting up your API key for the first time, and check /pricing if you want predictable per-seat costs instead of raw token billing.
questions
Does Slack support streaming responses from an LLM? Not natively in a single message. You simulate it by posting a placeholder message and editing it periodically as content arrives from the API, respecting Slack's roughly 1-request-per-second rate limit on chat.update.
How do I keep conversation context across a Slack thread? Claude is stateless between requests, so you need to store each thread's message history (keyed by thread_ts) and resend it as the messages array on every subsequent call.
Can I use my existing Claude access instead of a separate Anthropic account for the Slack bot? Yes — SubToAPI issues standard sub_live_... API keys backed by your existing Claude access, so a Slack integration can use the same billing and dashboard as your other apps instead of a separate account.