Visitar URL original
Discord | Chat SDK

Discord

Build Discord bots with slash commands, threads, embeds, and Components v2 cards. Runs on serverless with a cron-driven Gateway listener.

Create a new bot

Scaffold a Next.js bot with the Discord adapter using create-chat-sdk:

pnpm create chat-sdk@latest my-bot --adapter discord memory -y

The generated app uses the Memory state adapter for local development. To add the adapter to an existing app, start at Install.

Install

pnpm add @chat-adapter/discord @chat-adapter/state-memory

Quick start

The adapter auto-detects DISCORD_BOT_TOKEN, DISCORD_PUBLIC_KEY, DISCORD_APPLICATION_ID, and DISCORD_MENTION_ROLE_IDS from the environment.

For managed credentials and webhook verification, see Vercel Connect.

lib/bot.ts
import { Chat } from "chat";
import { createDiscordAdapter } from "@chat-adapter/discord";
import { createMemoryState } from "@chat-adapter/state-memory";

const discord = createDiscordAdapter();
export const bot = new Chat({
  userName: "mybot",
  adapters: {
    discord,
  },
  state: createMemoryState(),
});

bot.onNewMention(async (thread, message) => {
  await thread.post("Hello from Discord!");
});

The memory state adapter keeps subscriptions and locks in process memory, which suits local development. Use Redis or PostgreSQL in production.

Then create the webhook route:

app/api/webhooks/discord/route.ts
import { after } from "next/server";
import { bot } from "@/lib/bot";

export async function POST(request: Request): Promise<Response> {
  return bot.webhooks.discord(request, {
    waitUntil: (task) => after(() => task),
  });
}

Platform setup

These steps create your own Discord application and bot, which you need when you authenticate with Discord app credentials.

Create the application

  1. Go to the Discord Developer Portal and click New Application.
  2. Note the Application ID and copy the Public Key from the General Information page.

Create the bot

  1. Open Bot in the sidebar and click Reset Token to generate a bot token. Discord shows the token only once.
  2. Enable Message Content Intent, and Server Members Intent if your bot needs it.

Set the interactions endpoint

In General Information, set Interactions Endpoint URL to https://your-domain.com/api/webhooks/discord. Discord sends a PING to verify the endpoint.

Add the bot to your server

In OAuth2 then URL Generator, select the bot and applications.commands scopes, pick the permissions your bot needs (Send Messages, Send Messages in Threads, Create Public Threads, Manage Threads, Read Message History, Add Reactions, Attach Files), and use the generated URL to invite the bot.

Authentication

Vercel Connect

Use Vercel Connect to resolve the bot token and application ID from a Discord connector and verify trigger-forwarded interactions with Vercel OIDC:

lib/bot.ts
import { createDiscordAdapter } from "@chat-adapter/discord";
import { connectDiscordAdapter } from "@vercel/connect/chat";

const discord = createDiscordAdapter({
  ...connectDiscordAdapter("discord/acme-discord"),
});

Replace discord/acme-discord with your connector UID. The helper supplies botToken, applicationId, and webhookVerifier, so you don't need DISCORD_BOT_TOKEN, DISCORD_PUBLIC_KEY, or DISCORD_APPLICATION_ID in your deployment. Configure the Connect trigger to forward interactions to /api/webhooks/discord on your deployed app.

Connect OIDC verification replaces Discord's Ed25519 public-key check for forwarded interactions. Regular messages and reactions still require a Gateway connection.

Create the connector and attach it to your project with the Vercel CLI. --trigger-path registers the Discord webhook route as the trigger destination, and vercel env pull downloads a development OIDC token for local runs:

vercel link
vercel connect create discord --name acme-discord --triggers
vercel connect attach discord/acme-discord --triggers --trigger-path /api/webhooks/discord
vercel env pull

Without --trigger-branch, Connect forwards webhooks to your production deployment. See Vercel Connect for the full setup.

Discord app credentials

Set DISCORD_BOT_TOKEN, DISCORD_PUBLIC_KEY, and DISCORD_APPLICATION_ID from the application you created in Platform setup, and the adapter picks them up automatically.

Configuration

PropTypeDescription
botToken?string | (() => string | Promise<string>)
Discord bot token or resolver invoked per API call. Auto-detected from `DISCORD_BOT_TOKEN`.
publicKey?string
Application public key. Auto-detected from `DISCORD_PUBLIC_KEY`.
applicationId?string | (() => string | Promise<string>)
Discord application ID or resolver. Auto-detected from `DISCORD_APPLICATION_ID`.
webhookVerifier?(request: Request, body: string) => unknown | Promise<unknown>
Custom webhook verifier used instead of Discord's Ed25519 public key.
contentFormat?DiscordContentFormat
Render Chat SDK cards as embeds or Discord Components v2. Defaults to `DiscordContentFormat.Embeds`.
mentionRoleIds?string[]
Role IDs that should trigger mention handlers. Auto-detected from `DISCORD_MENTION_ROLE_IDS` (comma-separated).
respondToChannelIds?string[]
Parent channel IDs whose non-bot messages, including messages in child threads, trigger mention handlers without an @mention. Top-level messages use the adapter's normal per-message Discord thread. Auto-detected from `DISCORD_RESPOND_TO_CHANNEL_IDS` (comma-separated). Defaults to `[]`.
respondToGlobalMentions?boolean
Treat `@everyone`/`@here` pings as mentions of the bot. Defaults to `false`, so the bot only responds when mentioned directly (or via `mentionRoleIds`).
interactionFlags?(context) => DiscordInteractionResponseFlag.Ephemeral | undefined
Return Discord interaction flags for the initial deferred slash command response.
apiUrl?string
Override the Discord API base URL.

botToken and applicationId are required. Provide either publicKey or webhookVerifier.

Environment variables

VariableRequiredDescription
DISCORD_BOT_TOKENYes, unless using Vercel ConnectBot token.
DISCORD_PUBLIC_KEYYes, unless using Vercel Connect or a custom webhookVerifierApplication public key for Ed25519 interaction verification.
DISCORD_APPLICATION_IDYes, unless using Vercel ConnectApplication ID.
DISCORD_MENTION_ROLE_IDSNoComma-separated role IDs that trigger mention handlers.
DISCORD_RESPOND_TO_CHANNEL_IDSNoComma-separated parent channel IDs whose messages trigger mention handlers without an @mention.

HTTP Interactions vs Gateway

Discord delivers events over two channels:

  • HTTP Interactions deliver button clicks, slash commands, and verification pings. They work on serverless but do not include regular messages.
  • The Gateway WebSocket is required to receive regular messages and reactions, and it needs a persistent connection.

In serverless environments, use a cron job to keep the Gateway connection alive.

Gateway setup for serverless

app/api/discord/gateway/route.ts
import { after } from "next/server";
import { bot } from "@/lib/bot";

export const maxDuration = 800;

export async function GET(request: Request): Promise<Response> {
  const cronSecret = process.env.CRON_SECRET;
  if (!cronSecret) {
    return new Response("CRON_SECRET not configured", { status: 500 });
  }

  const authHeader = request.headers.get("authorization");
  if (authHeader !== `Bearer ${cronSecret}`) {
    return new Response("Unauthorized", { status: 401 });
  }

  const durationMs = 600 * 1000;
  const webhookUrl = `https://${process.env.VERCEL_URL}/api/webhooks/discord`;

  await bot.initialize();
  const discord = bot.getAdapter("discord");
  return discord.startGatewayListener(
    { waitUntil: (task) => after(() => task) },
    durationMs,
    undefined,
    webhookUrl
  );
}
vercel.json
{
  "crons": [
    {
      "path": "/api/discord/gateway",
      "schedule": "*/9 * * * *"
    }
  ]
}

This runs every 9 minutes, overlapping with the 10-minute listener duration to keep coverage continuous.

Role mentions

By default, only direct user mentions (@BotName) trigger onNewMention handlers. To also trigger on role mentions:

  1. Create a role in your Discord server (for example, "AI") and assign it to your bot.
  2. Copy the role ID. With Developer Mode enabled, right-click the role in server settings.
  3. Add it to mentionRoleIds:
createDiscordAdapter({
  mentionRoleIds: ["1457473602180878604"],
});

Or set DISCORD_MENTION_ROLE_IDS as a comma-separated string.

Cards

Discord cards render as embeds by default. Set contentFormat to render Chat SDK cards with Discord Components:

import {
  createDiscordAdapter,
  DiscordContentFormat,
} from "@chat-adapter/discord";

createDiscordAdapter({
  contentFormat: DiscordContentFormat.ComponentsV2,
});

When enabled, card messages use Discord's IS_COMPONENTS_V2 flag and render with containers, sections, text displays, media galleries, buttons, and string selects. Plain text messages continue to use normal Discord message content.

Discord caps a Components v2 message at 40 total components and 4000 characters across all text. When a card exceeds either limit the adapter throws a ValidationError instead of letting Discord reject the request, so reduce the number of sections, fields, actions, or images, or shorten the text on very large cards.

Interaction flags

Discord slash commands are acknowledged with DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE before your handler posts the final response. Use the interactionFlags option to set Discord interaction flags on that initial deferred response.

Return Discord's EPHEMERAL flag to make the loading state and original interaction response visible only to the user who invoked the command:

import {
  createDiscordAdapter,
  DiscordInteractionResponseFlag,
} from "@chat-adapter/discord";

createDiscordAdapter({
  interactionFlags: ({ command, interaction }) => {
    if (
      command === "/admin" ||
      interaction.member?.roles.includes("1457473602180878604")
    ) {
      return DiscordInteractionResponseFlag.Ephemeral;
    }

    return undefined;
  },
});

The callback receives the parsed command path, flattened option text, invoking user, normalized Chat SDK channel ID, and raw Discord interaction. Later calls to event.channel.post() continue through the normal Discord interaction response flow. Calls to event.channel.postEphemeral() still use the normal Chat SDK fallback behavior.

Discord thread channel names

Call discord.setThreadTitle(thread.id, title) to rename an existing Discord thread channel. The bot needs the Manage Threads permission.

Inbound attachments

Incoming attachments expose a lazy fetchData() that downloads from Discord's CDN anonymously. Downloads refuse private and internal addresses, including after redirects, are limited to 25 MB, and time out after 30 seconds.

Feature support

Messaging

FeatureSupported
Post message
Message replies
Edit message
Delete message
File uploads
StreamingPost+Edit
Scheduled messages

Rich content

FeatureSupported
Card formatEmbeds / Components
Buttons
Link buttons
Select menusComponents
TablesGFM
Charts
Fields
Images in cards
Modals

Conversations

FeatureSupported
Slash commands
Mentions
Add reactions
Remove reactions
Typing indicator
Mark as read
Message edit events
Message delete events
DMs
Ephemeral messages
User lookup
Parent subject
Native client
Custom API endpoint

Message history

FeatureSupported
Fetch messages
Fetch single message
Fetch thread info
Fetch channel messages
List threads
Fetch channel info
Post channel message

Resources

See all guides and templates on the resources page.

More adapters