← Blog

Claude API Slack Bot Tutorial: Build It in an Afternoon

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

If you're searching for a Claude API Slack bot tutorial, you want a bot that listens in a channel, sends the message to Claude, and posts the reply back — without duplicating threads, timing out on slow responses, or leaking your API key into client-side code. This guide covers the whole path: Slack app setup, the Node.js server that bridges Slack events to Claude, handling Slack's 3-second ack window, and deploying it so it stays online.

The core challenge isn't calling an LLM — it's Slack's event model. Slack expects an HTTP 200 within 3 seconds of sending an event, but a Claude completion can take much longer, especially for longer answers. The fix is to acknowledge immediately and post the actual answer asynchronously via chat.postMessage. That pattern is the backbone of everything below.

What You're Building

A Slack bot that:

Step 1: Create the Slack App

  1. Go to api.slack.com/apps and create a new app "from scratch."
  2. Under OAuth & Permissions, add these bot scopes: app_mentions:read, chat:write, channels:history (if you want thread context).
  3. Under Event Subscriptions, enable events and subscribe to app_mention.
  4. Install the app to your workspace and copy the Bot User OAuth Token (xoxb-...) and the Signing Secret.

Slack will require a reachable HTTPS endpoint for event subscriptions, so have your server deployed (even to a temporary tunnel like ngrok) before finishing this step.

Step 2: Set Up the Server

mkdir claude-slack-bot && cd claude-slack-bot
npm init -y
npm install express @slack/bolt dotenv node-fetch

Using @slack/bolt handles signature verification and event parsing for you, which saves a lot of boilerplate compared to raw Express routes.

// index.js
require('dotenv').config();
const { App } = require('@slack/bolt');

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

app.event('app_mention', async ({ event, say }) => {
  const userText = event.text.replace(/<@[^>]+>/, '').trim();

  // Acknowledge fast with a placeholder, then edit it in place
  const placeholder = await say({
    text: 'Thinking…',
    thread_ts: event.thread_ts || event.ts,
  });

  try {
    const answer = await askClaude(userText);
    await app.client.chat.update({
      token: process.env.SLACK_BOT_TOKEN,
      channel: event.channel,
      ts: placeholder.ts,
      text: answer,
    });
  } catch (err) {
    await app.client.chat.update({
      token: process.env.SLACK_BOT_TOKEN,
      channel: event.channel,
      ts: placeholder.ts,
      text: `Sorry, something went wrong: ${err.message}`,
    });
  }
});

(async () => {
  await app.start(process.env.PORT || 3000);
  console.log('Slack bot running');
})();

The "post placeholder, then edit" pattern solves the 3-second ack problem cleanly: Bolt's event handler returns quickly, Slack is happy, and the user sees a message update once Claude replies.

Step 3: Wire Up Claude

You can call the Anthropic API directly, but if you'd rather avoid managing raw API keys, rate limits, and streaming plumbing yourself, SubToAPI gives you an HTTPS endpoint (sub_live_... keys) that wraps your existing Claude access with the same request/response shape. Either way, the integration code looks like this:

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-sonnet-4-5',
      max_tokens: 1024,
      messages: [{ role: 'user', content: prompt }],
    }),
  });

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

See /docs/quickstart and /docs/messages for the full request schema if you want to pass system prompts, temperature, or stop sequences.

Step 4: Add Thread Context (Optional but Worth It)

A bot that forgets everything after one message gets annoying fast. If the mention happens inside a thread, pull the prior messages and feed them in as conversation history:

async function getThreadHistory(client, channel, threadTs) {
  const result = await client.conversations.replies({ channel, ts: threadTs });
  return result.messages.map(m => ({
    role: m.bot_id ? 'assistant' : 'user',
    content: m.text,
  }));
}

Pass that array as messages instead of a single prompt. Keep an eye on token usage as threads grow — long-running channels can balloon fast, and trimming older turns or summarizing them is worth building in early.

Step 5: Streaming Responses (Better UX for Long Answers)

Slack doesn't support true token-by-token streaming in a channel message, but you can simulate progressive updates by editing the placeholder message every few hundred milliseconds as chunks arrive. If you're using SubToAPI, /docs/streaming documents the server-sent events format — buffer the chunks and call chat.update on an interval rather than on every token, or you'll hit Slack's rate limits on message edits.

Step 6: Deploy and Harden

questions

Do I need the official Anthropic SDK to build a Claude Slack bot? No — any HTTP client works since the API is plain JSON over HTTPS. The Bolt framework handles Slack's side; a simple fetch or axios call handles the Claude side.

How do I stop the bot from replying to itself or other bots? Check event.bot_id and ignore events where it's set, and only respond to app_mention events rather than every message in a channel to avoid loops.

Can the bot handle file uploads or images shared in Slack? Yes, if the model supports vision input — download the file via Slack's files.info with your bot token, then pass the image data in the Claude request content array alongside the text.

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 →