← Blog

Claude API Slack Bot Integration Example

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

What you'll build

This article walks through a working example of a Slack bot powered by the Claude API — the kind of integration people usually mean when they search for "claude api slack bot integration example." You'll get a bot that responds to mentions (@claude) and a slash command (/ask), sends the message to Claude, and posts the reply back into the channel.

The stack is deliberately simple: Node.js, Slack's Bolt.js framework, and an HTTPS call to a Claude-compatible endpoint. The same pattern works whether you call Anthropic's API directly or route through a proxy like SubToAPI for key management and usage tracking — the Slack-side code doesn't change.

Prerequisites

npm install @slack/bolt node-fetch

Step 1: Create the Slack app

In the Slack API dashboard, create a new app and enable:

Install the app to your workspace and grab the Bot User OAuth Token and Signing Secret.

Step 2: Set up the Bolt app

const { App } = require('@slack/bolt');
const fetch = require('node-fetch');

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

async function askClaude(message) {
  const res = await fetch('https://api.subtoapi.app/v1/messages', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
    },
    body: JSON.stringify({
      model: 'claude-3-5-sonnet-20241022',
      max_tokens: 500,
      messages: [{ role: 'user', content: message }],
    }),
  });
  const data = await res.json();
  return data.content[0].text;
}

This example calls /v1/messages on SubToAPI, which mirrors the standard Claude Messages API shape — see /docs/messages for the full request/response reference. If you're calling Anthropic directly instead, swap the URL and header but keep the rest identical.

Step 3: Handle mentions

app.event('app_mention', async ({ event, say }) => {
  const text = event.text.replace(/<@[^>]+>/, '').trim();
  const reply = await askClaude(text);
  await say({ text: reply, thread_ts: event.ts });
});

Replying in a thread (thread_ts) keeps channels readable, which matters once more than a couple of people start using the bot.

Step 4: Handle the slash command

app.command('/ask', async ({ command, ack, respond }) => {
  await ack();
  const reply = await askClaude(command.text);
  await respond({ text: reply, response_type: 'in_channel' });
});

Start the app:

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

Slash commands have a 3-second acknowledgment window, so ack() must fire immediately — the actual Claude call happens afterward and the result is sent via respond().

Handling latency and timeouts

Claude responses for longer prompts can take several seconds. Two practical fixes:

  1. Acknowledge fast, respond later. Already covered above — ack() then respond() asynchronously.
  2. Stream and edit the message. Slack supports updating a message via chat.update. You can post a placeholder ("Thinking...") immediately, then stream tokens from Claude and periodically update the message text. This avoids the perception of a frozen bot. If you want the mechanics of token-by-token handling, that's covered separately — see /docs/streaming for how streaming responses are structured on SubToAPI.
app.event('app_mention', async ({ event, client }) => {
  const placeholder = await client.chat.postMessage({
    channel: event.channel,
    thread_ts: event.ts,
    text: 'Thinking...',
  });

  const reply = await askClaude(event.text);

  await client.chat.update({
    channel: event.channel,
    ts: placeholder.ts,
    text: reply,
  });
});

Adding conversation memory

A single-turn bot forgets context between messages, which feels broken in a thread. The simplest fix is to pass the thread history back as messages:

async function askClaudeWithHistory(history) {
  const res = await fetch('https://api.subtoapi.app/v1/messages', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
    },
    body: JSON.stringify({
      model: 'claude-3-5-sonnet-20241022',
      max_tokens: 500,
      messages: history, // [{role: 'user', content}, {role: 'assistant', content}, ...]
    }),
  });
  const data = await res.json();
  return data.content[0].text;
}

Fetch thread replies with conversations.replies, map them to role: 'user' or role: 'assistant' based on whether the sender is the bot, and pass the array in. Cap it at the last 10–15 messages to control token usage.

Why route through SubToAPI instead of calling Anthropic directly

For a single internal bot, calling Anthropic directly is fine. It gets more useful once you have multiple bots, environments, or teammates:

Setup takes the same five minutes as swapping an API URL. Start from /signup, grab a key, and check /docs/quickstart for the minimal request format. Plans are listed at /pricing (Solo €9, Team €19/seat, Scale €49/seat), with a free trial to test the bot before committing.

Deploying

For production, run the bot behind a process manager (PM2, systemd) or as a container on a small VM/PaaS. If you're not using Socket Mode, you'll need a public HTTPS endpoint for Slack's Events API to reach — a reverse proxy with TLS (Caddy, nginx) in front of the Node process is the usual setup.

questions

Do I need Socket Mode or HTTP Events for a Claude Slack bot? Either works. Socket Mode avoids exposing a public endpoint and is easier for internal tools; HTTP Events is more standard for production deployments behind your own domain.

How do I stop the bot from exceeding Slack's response time limits? Acknowledge slash commands within 3 seconds using ack(), then send the actual Claude reply asynchronously via respond() or a follow-up chat.postMessage call.

Can tool use (function calling) work inside a Slack bot? Yes — pass tools in the same request body you send to the Messages endpoint. See /docs/tools for the schema; the Slack-side handling is unchanged, you just parse tool_use blocks from the response before replying.

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 →