← Blog

How to Build a Claude-Powered Slackbot in Node.js

2026-10-29 · 5 min read · SubToAPI Team

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

Step 1: Create the Slack App

Go to api.slack.com/apps and create a new app "from scratch." You'll need:

  1. Bot Token Scopes: chat:write, app_mentions:read, channels:history (add im:history if you want DMs)
  2. Event Subscriptions: enable events, point the Request URL at your server's /slack/events endpoint, and subscribe to app_mention (and message.im for direct messages)
  3. 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

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.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →