How to Build a Claude-Powered Slackbot in Node.js
A Claude-powered Slackbot is a Slack app that listens for messages or mentions, forwards them to Claude, and posts the response back into the channel. The core pieces are a Slack app with Events API or Socket Mode enabled, a small server that handles incoming events, and an API call to Claude that turns the user's message into a reply. This guide walks through all three, with working code you can deploy today.
Most tutorials stop at "call Claude and post the message." That works for a demo, but a real Slackbot also needs to handle threading, rate limits, retries when Claude is slow, and a way to track usage per team or channel. We'll cover the minimal version first, then the parts that make it production-ready.
What You Need Before Starting
- A Slack workspace where you can create and install apps
- Node.js 18+ and a public HTTPS endpoint (ngrok works for local testing, a real host for production)
- A way to call Claude — either directly through Anthropic's API or through a hosted API gateway like SubToAPI, which wraps Claude access behind a standard HTTPS endpoint with streaming and usage tracking built in
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:
chat:write,app_mentions:read,channels:history(addim:historyif you want DMs) - Event Subscriptions: enable events, point the Request URL at your server's
/slack/eventsendpoint, and subscribe toapp_mention(andmessage.imfor direct messages) - Install the app to your workspace and copy the Bot User OAuth Token (
xoxb-...) and the Signing Secret
Slack will send a verification challenge to your Request URL the first time you set it up — your server needs to respond to that before events start flowing.
Step 2: Set Up the Server
A minimal Express server that verifies Slack's signature, acknowledges events immediately, and processes the message asynchronously:
import express from "express";
import { WebClient } from "@slack/web-api";
import crypto from "crypto";
const app = express();
app.use(express.raw({ type: "application/json" }));
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
function verifySlackRequest(req) {
const timestamp = req.headers["x-slack-request-timestamp"];
const sig = req.headers["x-slack-signature"];
const base = `v0:${timestamp}:${req.body}`;
const hmac = crypto
.createHmac("sha256", process.env.SLACK_SIGNING_SECRET)
.update(base)
.digest("hex");
return `v0=${hmac}` === sig;
}
app.post("/slack/events", async (req, res) => {
if (!verifySlackRequest(req)) return res.status(401).end();
const body = JSON.parse(req.body);
if (body.type === "url_verification") {
return res.json({ challenge: body.challenge });
}
res.status(200).end(); // ack immediately, Slack times out after 3s
const event = body.event;
if (event?.type === "app_mention") {
handleMention(event).catch(console.error);
}
});
app.listen(3000, () => console.log("Slackbot listening on :3000"));
Acknowledging within 3 seconds matters — Slack will retry (and sometimes disable) your app if you're slow to respond, so the Claude call has to happen after you've already sent the 200.
Step 3: Call Claude and Reply in Thread
Strip the bot mention out of the text, send it to Claude, and post the reply in the same thread so the channel stays readable:
async function handleMention(event) {
const text = event.text.replace(/<@[^>]+>/, "").trim();
const response = 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 }],
}),
});
const data = await response.json();
const reply = data.content[0].text;
await slack.chat.postMessage({
channel: event.channel,
thread_ts: event.thread_ts || event.ts,
text: reply,
});
}
This is the full loop: Slack event in, Claude call, Slack reply out. If you're calling Claude directly through Anthropic's SDK instead, the request/response shape is nearly identical — swap the endpoint and auth header.
Handling Longer Responses with Streaming
Claude responses longer than a few sentences can take several seconds to generate, which feels slow in a chat context. Instead of waiting for the full response, you can stream tokens and edit the Slack message progressively using chat.update:
let buffer = "";
const posted = await slack.chat.postMessage({
channel: event.channel,
thread_ts: event.thread_ts || event.ts,
text: "Thinking...",
});
// pseudo-loop over streamed chunks
for await (const chunk of streamFromClaude(text)) {
buffer += chunk;
await slack.chat.update({
channel: event.channel,
ts: posted.ts,
text: buffer,
});
}
In practice, don't call chat.update on every token — Slack rate-limits message edits. Batch updates every 500ms–1s or every N characters. See /docs/streaming for details on consuming streamed Claude output.
Adding Context and Memory
Slack threads are a natural fit for multi-turn conversations. Store the message history per thread_ts (in Redis, a database, or even an in-memory Map for small bots) and pass the full list of prior messages to Claude on each turn, same as you would with the /docs/messages format. Keep a cap on how many turns you include to control token usage and latency.
Production Considerations
- Rate limits: Slack limits
chat.postMessageto roughly one call per second per channel — batch streaming updates accordingly - Retries: wrap the Claude call in a retry with backoff for transient network errors
- Per-workspace API keys: if you're distributing the bot across multiple Slack workspaces or clients, issue a separate application key per workspace so you can track usage and revoke access independently — this is exactly what SubToAPI's
sub_live_keys are designed for - Tool use: if your bot needs to look things up (ticket status, docs search, calendar), Claude's tool calling lets you define functions it can invoke mid-conversation — see /docs/tools
Why Use an API Gateway Instead of a Raw Anthropic Key
Dropping a single Anthropic key directly into a Slackbot works for a solo project, but it gets messy once more than one person or workspace uses the bot: you can't see which team is burning through tokens, and revoking access means rotating the key for everyone. SubToAPI issues scoped application keys, exposes usage metadata per key, and handles streaming and tool calls through a standard HTTPS interface — see /docs/quickstart to get one running in minutes, and /pricing for plan details.
Testing Locally
Use ngrok (ngrok http 3000) to expose your local server, paste the forwarding URL into Slack's Event Subscriptions page, and mention your bot in a channel it's been invited to (/invite @yourbot). Watch your server logs — most early bugs are signature verification mismatches or forgetting to re-stringify the raw body.
questions
Do I need Socket Mode or the Events API? Either works. Events API requires a public HTTPS URL and is simpler to reason about; Socket Mode uses a WebSocket connection and avoids exposing a public endpoint, which is convenient for local development or restrictive network environments.
How do I keep the bot from responding to every message in a channel? Subscribe only to app_mention events (plus message.im for DMs) rather than message.channels, so the bot only triggers when explicitly mentioned or messaged directly.
Can the bot handle file uploads or images? Yes — Slack events include file metadata, and Claude's vision-capable models accept image content in the same /docs/messages request format. You'll need to download the file from Slack's URL (with your bot token) before passing it to Claude.