← Blog

Claude API Slack Bot Integration Tutorial

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

Claude API Slack Bot Integration Tutorial

If you're searching for this, you probably want one of two things: a bot that answers questions in a Slack channel using Claude, or a way to pipe Slack messages into Claude and post the response back as a thread reply. Both use the same core pattern — a Slack app with event subscriptions, a small server that receives those events, a call to the Claude API, and a message posted back through Slack's Web API.

This tutorial walks through that pattern end to end with working code, using Node.js and the Slack Bolt SDK. You can swap Bolt for any Slack client library — the Claude integration part stays the same.

What you'll build

A Slack bot that:

Prerequisites

Step 1: Create the Slack app

Go to api.slack.com/apps → Create New App → From scratch. Give it a name and pick your workspace.

Under OAuth & Permissions, add these bot token scopes:

Install the app to your workspace and copy the Bot User OAuth Token (xoxb-...).

Under Event Subscriptions, enable events and subscribe to app_mention. If you don't have a public HTTPS endpoint yet, use Socket Mode instead — it avoids exposing a public URL during development. Enable Socket Mode and generate an App-Level Token (xapp-...).

Step 2: Scaffold the bot server

mkdir claude-slack-bot && cd claude-slack-bot
npm init -y
npm install @slack/bolt node-fetch
// index.js
const { App } = require("@slack/bolt");
const fetch = require("node-fetch");

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

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

  const claudeReply = await askClaude(userText);

  await client.chat.postMessage({
    channel: event.channel,
    thread_ts: event.thread_ts || event.ts,
    text: claudeReply,
  });
});

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",
      max_tokens: 500,
      messages: [{ role: "user", content: prompt }],
    }),
  });
  const data = await res.json();
  return data.content?.[0]?.text ?? "Sorry, I couldn't generate a reply.";
}

(async () => {
  await app.start();
  console.log("Slack bot running");
})();

This uses SubToAPI's /v1/messages endpoint as a drop-in Claude backend — if you'd rather call Anthropic's API directly, the request shape is nearly identical. The full request and response schema is in the messages docs; the quickstart covers authentication setup if you're starting from zero.

Run it:

SLACK_BOT_TOKEN=xoxb-... \
SLACK_APP_TOKEN=xapp-... \
SUBTOAPI_KEY=sub_live_... \
node index.js

Invite the bot to a channel, mention it, and you should get a threaded reply within a couple of seconds.

Step 3: Handle Slack's ack timeout

Slack expects an acknowledgment within 3 seconds, but Claude generations — especially longer ones — can take longer than that. If you're on HTTP Events (not Socket Mode), ack immediately and process asynchronously:

app.event("app_mention", async ({ event, client, ack }) => {
  if (ack) await ack();
  // ... rest of the handler
});

With Socket Mode this is less of an issue since Bolt handles acking for you, but it's still good practice to post a quick "Thinking…" placeholder message and edit it once Claude responds, so users aren't staring at silence.

Step 4: Stream the response for long replies

For longer answers, streaming tokens into an editable Slack message feels much more responsive than waiting for the full completion. Slack doesn't support true token-by-token streaming in a message, but you can batch updates every few hundred milliseconds:

let buffer = "";
let lastUpdate = Date.now();

// inside your streaming handler, on each chunk:
buffer += chunk;
if (Date.now() - lastUpdate > 800) {
  await client.chat.update({ channel, ts: messageTs, text: buffer });
  lastUpdate = Date.now();
}

SubToAPI's /v1/messages endpoint supports stream: true with server-sent events — see the streaming docs for the event format if you want to implement this properly instead of polling.

Step 5: Add thread context (optional but recommended)

A bot that only sees the single mentioned message feels dumb in ongoing conversations. Fetch the thread history and include it as prior messages:

const history = await 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,
}));

Pass messages as the array in your Claude request instead of a single-item array.

Step 6: Add tool use for real actions

If your bot should do things — look up a ticket, query a database, post to a calendar — use Claude's tool use feature rather than trying to parse intent yourself. Define tool schemas, let Claude decide when to call them, and execute the actual Slack or API action in your handler. The tool use docs cover the request/response format for defining and handling tool calls.

Step 7: Deploy

For production, swap Socket Mode for an HTTP endpoint (needed if you want multiple instances or want to avoid the Socket Mode connection limits), deploy on any Node host, and set your Slack app's Event Subscriptions URL to that endpoint. Store your Slack tokens and Claude API key as environment secrets, not in code.

If you're managing this bot across a team, a service with per-key usage metadata and seat-based access — like SubToAPI's Team plan — makes it easier to see which bot or environment is consuming tokens without digging through raw API logs.

Questions

Does this work with Slack's Socket Mode or only HTTP events? Both. Socket Mode is simpler for local development since it doesn't need a public URL; HTTP events are better for production deployments with multiple instances.

Can the bot remember conversation history across messages? Yes — fetch the thread's prior messages with conversations.replies and pass them as the messages array in your Claude request, mapping bot messages to the assistant role.

Do I need an Anthropic API key directly, or can I use a proxy? Either works. A proxy like SubToAPI gives you a standard HTTPS API key (sub_live_...), streaming, and usage tracking without changing how your bot code is structured — see the quickstart to get a key via signup.

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 →