Claude API Discord Bot Tutorial: Build One Step by Step
Building a Discord bot powered by Claude means connecting Discord's gateway (for receiving messages) to Claude's Messages API (for generating replies), then wiring the response back into a channel or thread. This tutorial walks through the whole pipeline: bot setup, message handling, conversation memory per channel, and deployment, using Node.js and discord.js.
If you already have Anthropic API access, you can call Claude directly. If you don't — or you want a shared key across a team without managing individual Anthropic billing — you can point the same code at a SubToAPI key instead, since it speaks the same Messages API format. Either way, the Discord-side code below doesn't change.
What You'll Need
- A Discord application + bot token from the Discord Developer Portal
- Node.js 18+
- An API key that can call Claude's Messages API (either directly from Anthropic, or a
sub_live_...key from /signup) discord.jsv14
Install dependencies:
npm install discord.js dotenv node-fetch
Step 1: Create the Discord Bot
In the Developer Portal, create a new application, add a Bot user, and enable the Message Content Intent under Privileged Gateway Intents — without it, your bot can't read message text. Invite the bot to your server with the bot scope and Send Messages + Read Message History permissions.
Store your tokens in a .env file:
DISCORD_TOKEN=your_discord_bot_token
CLAUDE_API_KEY=sub_live_your_key_here
CLAUDE_API_URL=https://api.subtoapi.app/v1/messages
If you're calling Anthropic directly instead, swap CLAUDE_API_URL for https://api.anthropic.com/v1/messages and adjust the auth header format accordingly.
Step 2: Basic Bot Skeleton
import { Client, GatewayIntentBits, Partials } from "discord.js";
import "dotenv/config";
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
partials: [Partials.Channel],
});
client.once("ready", () => {
console.log(`Logged in as ${client.user.tag}`);
});
client.login(process.env.DISCORD_TOKEN);
Run it once to confirm the login works before adding Claude.
Step 3: Call the Messages API
Write a small helper that sends a conversation to Claude and returns the text:
async function askClaude(messages) {
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: 1024,
system: "You are a helpful, concise Discord bot. Keep replies under 1500 characters.",
messages,
}),
});
if (!res.ok) {
throw new Error(`API error ${res.status}: ${await res.text()}`);
}
const data = await res.json();
return data.content[0].text;
}
The system prompt is worth tuning specifically for Discord: Claude's default answers can run long, and Discord messages cap at 2000 characters. Telling the model to stay concise avoids truncation issues before you even get to splitting logic.
Step 4: Handle Incoming Messages
Listen for mentions or a command prefix, avoid replying to bots (including itself), and send the response back:
client.on("messageCreate", async (message) => {
if (message.author.bot) return;
if (!message.mentions.has(client.user)) return;
const prompt = message.content.replace(/<@!?\d+>/g, "").trim();
if (!prompt) return;
await message.channel.sendTyping();
try {
const reply = await askClaude([{ role: "user", content: prompt }]);
await message.reply(reply.slice(0, 2000));
} catch (err) {
console.error(err);
await message.reply("Something went wrong talking to Claude.");
}
});
sendTyping() shows the "bot is typing" indicator, which matters for UX since a Claude response can take a few seconds.
Step 5: Add Per-Channel Conversation Memory
A stateless bot forgets everything between messages, which feels broken in a chat context. Keep a short rolling history per channel in memory (or swap for Redis/a database in production):
const histories = new Map();
const MAX_TURNS = 10;
function getHistory(channelId) {
if (!histories.has(channelId)) histories.set(channelId, []);
return histories.get(channelId);
}
client.on("messageCreate", async (message) => {
if (message.author.bot) return;
if (!message.mentions.has(client.user)) return;
const prompt = message.content.replace(/<@!?\d+>/g, "").trim();
if (!prompt) return;
const history = getHistory(message.channelId);
history.push({ role: "user", content: prompt });
await message.channel.sendTyping();
try {
const reply = await askClaude(history.slice(-MAX_TURNS));
history.push({ role: "assistant", content: reply });
await message.reply(reply.slice(0, 2000));
} catch (err) {
console.error(err);
await message.reply("Something went wrong talking to Claude.");
}
});
This trims history to the last MAX_TURNS messages, which caps both token usage and cost per request.
Step 6: Handling Long Replies
For answers that exceed 2000 characters, split into chunks instead of silently truncating:
function chunkMessage(text, limit = 2000) {
const chunks = [];
for (let i = 0; i < text.length; i += limit) {
chunks.push(text.slice(i, i + limit));
}
return chunks;
}
for (const chunk of chunkMessage(reply)) {
await message.channel.send(chunk);
}
Streaming Responses (Optional)
Discord doesn't support token-by-token message editing well at scale (rate limits kick in fast if you edit on every token), but you can stream Claude's response and edit the message every few hundred milliseconds instead of waiting for the full completion. See /docs/streaming for the SSE event format if you want to implement this.
Deployment Notes
Deploy the bot as a long-running Node process — a small VPS, Railway, Fly.io, or a container on any host that supports persistent connections (Discord bots use WebSockets, so serverless functions with short execution limits won't work well). Set your environment variables in the host's secrets manager rather than committing .env.
If multiple people on your team need to build or maintain bots against the same Claude access, a shared dashboard with per-app keys is easier to manage than individual Anthropic accounts. SubToAPI issues separate sub_live_... keys per bot or environment, tracks usage per key, and supports team seats — see /pricing for plan details and /docs/quickstart to get a key running in minutes.
Full Request Reference
For the complete set of parameters (system prompts, tool use, stop sequences, temperature), check /docs/messages. Tool use in particular is useful for Discord bots that need to look up data — for example a bot that can query a database or hit an external API mid-conversation — and the request/response shape is documented at /docs/tools.
questions
Do I need an Anthropic account to build a Claude Discord bot? You need a valid API key that speaks the Messages API format — either directly from Anthropic or through a proxy like SubToAPI, which issues its own sub_live_... keys backed by Claude access.
Why does my bot not respond to messages? The most common cause is a missing Message Content Intent — enable it in the Discord Developer Portal and make sure GatewayIntentBits.MessageContent is included in your client's intents array.
Can the bot remember earlier messages in a conversation? Yes, by storing a rolling history per channel or user and sending it back as the messages array on each request — just cap the length to control token usage and cost.