← Blog

Claude API Discord Bot Integration Tutorial

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

What you need to build a Claude-powered Discord bot

A Claude API Discord bot integration requires three pieces: a Discord application with a bot token, a Node.js (or Python) process that listens for Discord events and calls Claude, and a way to keep message history per channel so conversations feel coherent. This tutorial covers all three, using discord.js and a Claude-compatible HTTPS endpoint, with working code you can drop into a bot today.

The core pattern is simple: Discord sends your bot a messageCreate event, you forward the message content (plus recent history) to Claude, and you post the response back to the channel. The hard parts are handling Discord's reply-length limits, streaming long responses without hitting rate limits, and managing API keys safely if more than one person maintains the bot. We'll handle each of those below.

Step 1: Create the Discord application

  1. Go to the Discord Developer Portal and create a new application.
  2. Under Bot, create a bot user and copy the token — this is your DISCORD_BOT_TOKEN.
  3. Under OAuth2 > URL Generator, select the bot scope and permissions Send Messages, Read Message History, Use Slash Commands. Use the generated URL to invite the bot to your server.
  4. Enable the Message Content Intent under Bot settings, otherwise message.content will be empty in events.

Step 2: Set up the bot project

mkdir claude-discord-bot && cd claude-discord-bot
npm init -y
npm install discord.js node-fetch dotenv

Create a .env file:

DISCORD_BOT_TOKEN=your_discord_token
CLAUDE_API_KEY=your_api_key
CLAUDE_API_URL=https://api.subtoapi.app/v1/messages

Using an HTTPS proxy endpoint like this instead of raw provider credentials means you can rotate or revoke the key from a dashboard without redeploying the bot, and you get usage metadata per request — useful once more than one person is poking the bot. If you're starting from scratch, SubToAPI gives you that endpoint plus a scoped sub_live_... key from your existing Claude access; see /docs/quickstart.

Step 3: Listen for messages and call Claude

import { Client, GatewayIntentBits } from "discord.js";
import fetch from "node-fetch";
import "dotenv/config";

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

const history = new Map(); // channelId -> messages[]

client.on("messageCreate", async (message) => {
  if (message.author.bot) return;
  if (!message.mentions.has(client.user)) return;

  const channelId = message.channelId;
  const prior = history.get(channelId) || [];
  const userText = message.content.replace(/<@!?\d+>/, "").trim();

  const messages = [...prior, { role: "user", content: userText }];

  const res = await fetch(process.env.CLAUDE_API_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.CLAUDE_API_KEY}`,
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-5",
      max_tokens: 800,
      messages,
    }),
  });

  const data = await res.json();
  const reply = data.content?.[0]?.text || "Something went wrong.";

  history.set(channelId, [
    ...messages,
    { role: "assistant", content: reply },
  ].slice(-10)); // keep last 10 turns

  await sendChunked(message.channel, reply);
});

client.login(process.env.DISCORD_BOT_TOKEN);

Full request/response format for /v1/messages is documented at /docs/messages.

Step 4: Handle Discord's 2000-character limit

Claude responses often exceed Discord's message length cap. Split on paragraph boundaries rather than truncating mid-sentence:

async function sendChunked(channel, text) {
  const limit = 1900;
  if (text.length <= limit) {
    await channel.send(text);
    return;
  }
  const chunks = [];
  let remaining = text;
  while (remaining.length > limit) {
    let cut = remaining.lastIndexOf("\n", limit);
    if (cut === -1) cut = limit;
    chunks.push(remaining.slice(0, cut));
    remaining = remaining.slice(cut);
  }
  chunks.push(remaining);
  for (const chunk of chunks) await channel.send(chunk);
}

Step 5: Stream responses for a "typing" feel

For longer answers, streaming lets you edit a single Discord message as tokens arrive instead of waiting for the full response, which keeps the bot feeling responsive. Use message.channel.sendTyping() while the request is pending, and if your endpoint supports server-sent events, buffer chunks and update the message every ~500ms (Discord rate-limits message edits, so don't update on every token). See /docs/streaming for the event format if you're using SubToAPI's streaming mode.

Step 6: Add a slash command instead of mention-triggering

Mention-based triggers work for small servers, but /ask slash commands scale better across channels and give you structured input validation. Register a command with name: "ask" and an prompt option, then call the same Claude request logic inside the interactionCreate handler, replying with interaction.editReply() after deferring with interaction.deferReply() — Claude responses rarely complete within Discord's 3-second interaction window, so deferring is required.

Step 7: Rate limits and cost control

Discord bots in active servers can generate bursts of requests. Two practical guards:

If multiple developers maintain the bot or you run it across several servers, a per-key usage dashboard makes it much easier to spot a runaway channel before it burns through your budget. SubToAPI's pricing includes Solo, Team, and Scale tiers with seat-based access if you want separate keys per environment or per bot instance.

Step 8: Deploy

Package the bot as a long-running Node process (not a serverless function — Discord's gateway connection needs to stay open). A small VPS, a pm2-managed process, or a container on Fly.io/Render all work well. Store DISCORD_BOT_TOKEN and your Claude API key as environment variables, never in source control.

questions

Do I need a paid Claude plan to run a Discord bot? You need API access with usage-based or subscription billing tied to a key you can call over HTTPS. If you already have Claude access and want to turn it into a callable API key without separate billing setup, see /signup.

Why does my bot reply with empty content? Usually the Message Content Intent is disabled in the Discord Developer Portal, or the mention regex isn't stripping the bot's ID correctly — log message.content before sending it to verify.

Can I run the same bot across multiple Discord servers? Yes — keep history keyed by channelId (not globally) so conversations in different servers don't bleed into each other, and consider separate API keys per server if you need isolated usage tracking.

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 →