← Blog

Claude API Slack Bot: Step by Step Setup Guide

2026-10-23 · 4 min read · SubToAPI Team

Building a Slack bot with the Claude API means connecting Slack's Events API to a backend that calls Claude and posts the response back into a channel or thread. This guide walks through every step: creating the Slack app, setting up event subscriptions, writing the handler code, and calling Claude to generate replies.

The whole thing is roughly 150 lines of code and runs on any server or serverless function that can receive HTTPS webhooks. You don't need a framework — just a web server, a Slack bot token, and a way to call Claude.

What you need before starting

Step 1: Create the Slack app

  1. Go to api.slack.com/apps and click Create New App → From scratch.
  2. Name it (e.g. "Claude Assistant") and pick your workspace.
  3. Under OAuth & Permissions, add these bot token scopes:
  1. Install the app to your workspace. Copy the Bot User OAuth Token (xoxb-...).

Step 2: Enable event subscriptions

  1. Under Event Subscriptions, toggle it on.
  2. Set the Request URL to your server endpoint, e.g. https://yourserver.com/slack/events. Slack will send a verification challenge here first — your server needs to echo it back.
  3. Subscribe to the bot event app_mention (fires when someone @mentions your bot) and optionally message.channels if you want it to respond to all messages in channels it's invited to.
  4. Save changes and reinstall the app if prompted.

Step 3: Set up the server to verify Slack requests

Slack signs every request with a signing secret. Verify it before processing anything — this prevents spoofed requests.

import crypto from "crypto";

function verifySlackRequest(req, signingSecret) {
  const timestamp = req.headers["x-slack-request-timestamp"];
  const sig = req.headers["x-slack-signature"];
  const baseString = `v0:${timestamp}:${req.rawBody}`;
  const hmac = crypto
    .createHmac("sha256", signingSecret)
    .update(baseString)
    .digest("hex");
  const expected = `v0=${hmac}`;
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

Handle the URL verification challenge first:

app.post("/slack/events", (req, res) => {
  if (req.body.type === "url_verification") {
    return res.send(req.body.challenge);
  }
  // ... rest of handling below
});

Step 4: Handle the mention event and call Claude

When app_mention fires, Slack sends the message text (including the mention), the channel, and a thread timestamp. Strip the mention, send the text to Claude, and post the reply back.

app.post("/slack/events", async (req, res) => {
  res.sendStatus(200); // ack immediately, Slack requires a fast response

  const event = req.body.event;
  if (event?.type !== "app_mention") return;

  const text = event.text.replace(/<@[^>]+>/, "").trim();

  const claudeRes = 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 claudeRes.json();
  const reply = data.content[0].text;

  await fetch("https://slack.com/api/chat.postMessage", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SLACK_BOT_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      channel: event.channel,
      thread_ts: event.thread_ts || event.ts,
      text: reply,
    }),
  });
});

Note the pattern: ack Slack's request immediately (within 3 seconds, or Slack retries and you get duplicate replies), then do the Claude call and post the result asynchronously.

If you're using SubToAPI instead of calling Anthropic directly, you get a standard HTTPS endpoint at /docs/messages with the same request shape, plus usage metadata per request so you can track exactly how much each Slack bot call costs without digging through logs.

Step 5: Keep conversation context in threads

Slack threads map naturally to Claude conversations. Store the message history per thread_ts (in memory for testing, Redis or a database for production) and pass the full history as the messages array on each call instead of just the latest message. This lets the bot remember earlier turns in the same thread.

const threadHistory = threadStore.get(event.thread_ts) || [];
threadHistory.push({ role: "user", content: text });

const data = await callClaude(threadHistory);
threadHistory.push({ role: "assistant", content: data.content[0].text });
threadStore.set(event.thread_ts, threadHistory);

Step 6: Deploy and test

Deploy your handler (Vercel, Railway, a small VPS, whatever you already use), point the Slack Request URL at the live endpoint, and invite the bot to a channel with /invite @YourBotName. Mention it and confirm you get a reply within a few seconds.

For production, consider:

questions

Do I need a paid Slack plan to run a Claude bot? No. The Events API, bot tokens, and chat.postMessage all work on Slack's free tier. You only need a paid plan for features unrelated to bots, like extended message history for human users.

Why does my bot reply twice to the same mention? Slack retries the event if your server doesn't return a 200 response within 3 seconds. Always acknowledge the request immediately and do the Claude call afterward, as shown in Step 4.

Can I use this same setup for Microsoft Teams or Discord? The Claude-calling logic is identical — only the webhook format and message-posting API differ. Swap Slack's Events API and chat.postMessage for the equivalent endpoints in Teams or Discord's bot framework.

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 →