← Blog

How to Build a Slack Bot with Claude

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

Building a Slack bot with Claude means connecting Slack's Events API or Bolt SDK to a Claude-powered backend that reads messages, generates a response, and posts it back to the channel. The core pieces are: a Slack app with the right scopes and event subscriptions, a small server that receives Slack events, a call to Claude to generate the reply, and a way to post that reply back through Slack's Web API.

This guide walks through the whole flow — from creating the Slack app to handling threads, streaming responses, and keeping your bot responsive under Slack's tight timeout windows.

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, and channels:history (or im:history for DMs) depending on where the bot should respond.
  2. Event subscriptions: subscribe to app_mention if you only want the bot to respond when tagged, or message.channels if you want it listening to everything in channels it's invited to.
  3. Socket Mode or HTTP events: Socket Mode avoids exposing a public URL during development, which is the fastest way to iterate locally.

Install the app to your workspace and grab the Bot User OAuth Token and Signing Secret — you'll need both.

Step 2: Set up the Bolt server

Bolt handles signature verification, retries, and event parsing so you don't have to write that boilerplate yourself.

import { App } from "@slack/bolt";

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  socketMode: true,
  appToken: process.env.SLACK_APP_TOKEN,
});

app.event("app_mention", async ({ event, say }) => {
  const reply = await askClaude(event.text, event.thread_ts || event.ts);
  await say({ text: reply, thread_ts: event.thread_ts || event.ts });
});

app.start();

Note the say call posts into the same thread the mention came from — this keeps conversations organized instead of flooding the main channel.

Step 3: Call Claude for the response

This is the function that turns the Slack message into a Claude request. If you're using SubToAPI, the request is a plain HTTPS call with your sub_live_ key — no SDK installation or Anthropic account setup required:

async function askClaude(userText, threadId) {
  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",
      max_tokens: 500,
      messages: [
        { role: "user", content: userText }
      ],
    }),
  });

  const data = await response.json();
  return data.content[0].text;
}

Check the quickstart and messages endpoint docs for the full request and response shape, including how usage metadata is returned so you can track cost per Slack workspace or per team if you're running the bot for multiple customers.

Step 4: Handle threads and conversation history

Slack threads map naturally to Claude's messages array. Instead of sending only the latest message, pull the thread's history with conversations.replies and pass it as alternating user/assistant turns:

const history = await app.client.conversations.replies({
  channel: event.channel,
  ts: event.thread_ts || event.ts,
});

const messages = history.messages.map((m) => ({
  role: m.bot_id ? "assistant" : "user",
  content: m.text,
}));

This keeps the bot aware of earlier context in a thread without you building your own session store — Slack is already storing the conversation for you.

Step 5: Stream responses to avoid timeouts

Slack's Events API expects an acknowledgment within 3 seconds, but a full Claude response can take longer, especially with longer prompts or larger max_tokens. The standard pattern is to acknowledge the event immediately, then post an initial "thinking" message you edit as tokens arrive:

const placeholder = await say({ text: "Thinking…", thread_ts: threadTs });

// stream from Claude, then update:
await app.client.chat.update({
  channel: event.channel,
  ts: placeholder.ts,
  text: finalText,
});

SubToAPI supports streaming responses over server-sent events — see the streaming docs for the exact event format if you want token-by-token updates instead of a single edit at the end.

Step 6: Add tool use for real actions

A bot that only chats is limited. If you want it to look up a ticket, query an internal API, or post a follow-up task, Claude's tool use lets you define functions the model can call during its response, and you execute them server-side before returning the final answer to Slack. The tools documentation covers the request format for defining tool schemas and handling the tool-call turn.

Step 7: Deploy and manage access

Once it works locally, deploy the server to any host that can run a long-lived Node process (Socket Mode keeps a persistent connection, so serverless functions need HTTP mode instead). For managing Claude access across a team building or maintaining the bot, SubToAPI's pricing plans include per-seat API keys — useful if multiple engineers need their own key for testing without sharing one production credential, and you can see usage broken down by key in the dashboard.

Start with a free trial to generate a key and test the full Slack flow end-to-end before going to production.

questions

Does the bot need to use Socket Mode or HTTP events? Socket Mode is easier for local development because it doesn't require a public URL, but production deployments often use HTTP events behind a load balancer for easier scaling and observability.

How do I keep Claude's responses within Slack's message length limits? Slack truncates messages around 40,000 characters, but for readability keep max_tokens modest (300–800) and split longer answers into a thread reply rather than one giant block.

Can the same bot work in DMs and channels? Yes — subscribe to both app_mention and message.im events, and branch your handler logic based on the channel type (event.channel_type === "im").

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 →