Skip to main content

How to Build a Discord Bot with Webhooks: Complete Guide with Examples

Discord bots can use webhooks to handle slash commands, buttons, and interactions - no persistent WebSocket connection required. This serverless approach is perfect for command-based bots that don't need to monitor messages in real-time.

What are Discord Interactions? When users trigger slash commands (/command), click buttons, or use select menus, Discord sends these events to your webhook endpoint instead of requiring a WebSocket gateway connection.

The problem? Traditional Discord bots require maintaining a persistent WebSocket connection, which means running a server 24/7 even when idle.

The solution? Use Discord's Interactions Endpoint URL to receive events via webhooks. Your endpoint only runs when users interact with your bot - perfect for serverless.

Prerequisitesโ€‹

Before you begin, sign up for a free Codehooks.io account and install the CLI:

npm install -g codehooks

Quick Deploy with Templateโ€‹

The fastest way to get started is using the Codehooks Discord bot template:

coho create mydiscordbot --template webhook-discord-minimal

You'll see output like:

Creating project: mydiscordbot
Project created successfully!

Your API endpoints:
https://mydiscordbot-a1b2.api.codehooks.io/dev

Then install, deploy, and configure:

cd mydiscordbot
npm install
coho deploy
coho set-env DISCORD_PUBLIC_KEY your-public-key-here --encrypted
Project: mydiscordbot-a1b2  Space: dev
Deployed Codehook successfully! ๐Ÿ™Œ
Finding your API endpoint

Run coho info to see your API endpoints. Your interactions URL will be: https://mydiscordbot-a1b2.api.codehooks.io/dev/interactions

Custom Implementationโ€‹

If you prefer to build from scratch, create a Discord interaction handler:

import { app } from 'codehooks-js';
import crypto from 'crypto';

const DISCORD_PUBLIC_KEY = process.env.DISCORD_PUBLIC_KEY;

// Bypass API key auth for Discord endpoint (Discord uses Ed25519 signatures)
app.auth('/interactions', (req, res, next) => next());

// Verify Discord Ed25519 signature
function verifyDiscordSignature(req) {
const signature = req.headers['x-signature-ed25519'];
const timestamp = req.headers['x-signature-timestamp'];

if (!signature || !timestamp || !DISCORD_PUBLIC_KEY) {
return false;
}

try {
const message = timestamp + req.rawBody;
return crypto.verify(
null,
Buffer.from(message),
{
key: Buffer.from(DISCORD_PUBLIC_KEY, 'hex'),
format: 'der',
type: 'spki'
},
Buffer.from(signature, 'hex')
);
} catch (err) {
console.error('Signature verification failed:', err.message);
return false;
}
}

// Discord interactions endpoint
app.post('/interactions', async (req, res) => {
// Verify the request is from Discord
if (!verifyDiscordSignature(req)) {
return res.status(401).send('Invalid signature');
}

const interaction = req.body;

// Type 1: PING - Discord verifies your endpoint
if (interaction.type === 1) {
return res.json({ type: 1 }); // PONG
}

// Type 2: Application Command (slash commands)
if (interaction.type === 2) {
const commandName = interaction.data.name;
const userId = interaction.member?.user?.id || interaction.user?.id;

console.log(`Command /${commandName} from user ${userId}`);

switch (commandName) {
case 'ping':
return res.json({
type: 4, // CHANNEL_MESSAGE_WITH_SOURCE
data: {
content: '๐Ÿ“ Pong! Bot is running on Codehooks.io'
}
});

case 'hello':
return res.json({
type: 4,
data: {
content: `Hello <@${userId}>! ๐Ÿ‘‹`
}
});

case 'info':
return res.json({
type: 4,
data: {
embeds: [{
title: 'Bot Information',
color: 0x5865F2, // Discord blurple
fields: [
{ name: 'Platform', value: 'Codehooks.io', inline: true },
{ name: 'Type', value: 'HTTP Interactions', inline: true },
{ name: 'Latency', value: 'Serverless', inline: true }
],
footer: { text: 'Built with Codehooks.io' }
}]
}
});

default:
return res.json({
type: 4,
data: {
content: `Unknown command: /${commandName}`
}
});
}
}

// Type 3: Message Component (buttons, select menus)
if (interaction.type === 3) {
const customId = interaction.data.custom_id;

console.log(`Button clicked: ${customId}`);

return res.json({
type: 4,
data: {
content: `You clicked: ${customId}`,
flags: 64 // Ephemeral (only visible to user)
}
});
}

// Type 5: Modal Submit
if (interaction.type === 5) {
const modalId = interaction.data.custom_id;
const fields = interaction.data.components;

console.log(`Modal submitted: ${modalId}`);

return res.json({
type: 4,
data: {
content: 'Form submitted successfully!',
flags: 64
}
});
}

res.status(400).json({ error: 'Unknown interaction type' });
});

export default app.init();

Set Up Your Discord Applicationโ€‹

1. Create a Discord Applicationโ€‹

  1. Go to the Discord Developer Portal
  2. Click New Application
  3. Enter a name and create

2. Get Your Public Keyโ€‹

  1. Go to General Information
  2. Copy the Public Key (you'll need this for signature verification)
  3. Set it in Codehooks:
coho set-env DISCORD_PUBLIC_KEY your-public-key-here

3. Set the Interactions Endpoint URLโ€‹

  1. In the Developer Portal, go to General Information
  2. Enter your Interactions Endpoint URL: https://mydiscordbot-a1b2.api.codehooks.io/dev/interactions
  3. Click Save Changes

Discord will send a PING request to verify your endpoint. If signature verification is working, it will save successfully.

4. Register Slash Commandsโ€‹

Register your commands using the Discord API:

// Run this once to register commands (not part of your bot)
const DISCORD_APP_ID = 'your-application-id';
const DISCORD_BOT_TOKEN = 'your-bot-token';

const commands = [
{
name: 'ping',
description: 'Check if the bot is running'
},
{
name: 'hello',
description: 'Get a friendly greeting'
},
{
name: 'info',
description: 'Get bot information'
}
];

// Register global commands
await fetch(`https://discord.com/api/v10/applications/${DISCORD_APP_ID}/commands`, {
method: 'PUT',
headers: {
'Authorization': `Bot ${DISCORD_BOT_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(commands)
});

console.log('Commands registered!');

5. Invite the Bot to Your Serverโ€‹

  1. Go to OAuth2 โ†’ URL Generator
  2. Select scopes: applications.commands
  3. Copy the URL and open it to invite the bot

Interaction Typesโ€‹

TypeNameDescription
1PINGDiscord verifying your endpoint
2APPLICATION_COMMANDSlash commands
3MESSAGE_COMPONENTButtons, select menus
4APPLICATION_COMMAND_AUTOCOMPLETEAutocomplete suggestions
5MODAL_SUBMITModal form submissions

Response Typesโ€‹

TypeNameDescription
1PONGResponse to PING
4CHANNEL_MESSAGE_WITH_SOURCESend a message
5DEFERRED_CHANNEL_MESSAGE_WITH_SOURCEAcknowledge, send message later
6DEFERRED_UPDATE_MESSAGEAcknowledge component, edit later
7UPDATE_MESSAGEEdit the original message
8APPLICATION_COMMAND_AUTOCOMPLETE_RESULTAutocomplete results
9MODALOpen a modal dialog

Handling Buttons and Select Menusโ€‹

Send interactive components and handle clicks:

// Command that sends a button
case 'button-demo':
return res.json({
type: 4,
data: {
content: 'Click a button:',
components: [{
type: 1, // Action Row
components: [
{
type: 2, // Button
style: 1, // Primary (blue)
label: 'Click Me',
custom_id: 'button_click'
},
{
type: 2,
style: 4, // Danger (red)
label: 'Delete',
custom_id: 'button_delete'
}
]
}]
}
});

// Handle button clicks (type 3)
if (interaction.type === 3) {
const customId = interaction.data.custom_id;

if (customId === 'button_click') {
return res.json({
type: 7, // UPDATE_MESSAGE
data: {
content: 'โœ… Button was clicked!',
components: [] // Remove buttons
}
});
}

if (customId === 'button_delete') {
return res.json({
type: 7,
data: {
content: '๐Ÿ—‘๏ธ Deleted!',
components: []
}
});
}
}

Deferred Responses for Long Operationsโ€‹

For operations taking more than 3 seconds, defer the response:

import { Datastore } from 'codehooks-js';

case 'slow-command':
// Acknowledge immediately (user sees "Bot is thinking...")
res.json({
type: 5 // DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE
});

// Do slow work
const result = await doSlowOperation();

// Send follow-up message
const webhookUrl = `https://discord.com/api/v10/webhooks/${interaction.application_id}/${interaction.token}/messages/@original`;

await fetch(webhookUrl, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: `Operation complete: ${result}`
})
});

return;
3-Second Timeout

Discord requires an initial response within 3 seconds. For longer operations, use deferred responses (type 5). You then have 15 minutes to send the actual response.

Ed25519 Signature Verificationโ€‹

Discord signs all interaction payloads with Ed25519. You must verify signatures:

import crypto from 'crypto';

function verifyDiscordSignature(req) {
const signature = req.headers['x-signature-ed25519'];
const timestamp = req.headers['x-signature-timestamp'];
const publicKey = process.env.DISCORD_PUBLIC_KEY;

if (!signature || !timestamp || !publicKey) {
return false;
}

try {
// Concatenate timestamp + raw body
const message = timestamp + req.rawBody;

// Verify using Ed25519
return crypto.verify(
null,
Buffer.from(message),
{
key: Buffer.from(publicKey, 'hex'),
format: 'der',
type: 'spki'
},
Buffer.from(signature, 'hex')
);
} catch (err) {
return false;
}
}

Important: Discord periodically sends invalid signatures to test your verification. If you fail, Discord will disable your endpoint and notify you.

HTTP Bots vs Gateway Botsโ€‹

FeatureHTTP (Interactions)Gateway (WebSocket)
ConnectionOn-demandAlways connected
HostingServerless friendlyRequires persistent server
Slash commandsโœ… Yesโœ… Yes
Buttons/Menusโœ… Yesโœ… Yes
Read messagesโŒ Noโœ… Yes
Presence updatesโŒ Noโœ… Yes
VoiceโŒ Noโœ… Yes
ReactionsโŒ Noโœ… Yes
CostPay per use24/7 server costs

Use HTTP Interactions when:

  • Building command-based bots
  • Using serverless platforms
  • Bot doesn't need to monitor messages

Use Gateway when:

  • Bot needs to read/react to messages
  • Building moderation bots
  • Need presence or voice features

Why Use Codehooks.io for Discord Bots?โ€‹

FeatureDIY SetupCodehooks.io
Setup timeHours5 minutes
Server managementConfigure & maintainServerless
Signature verificationImplement Ed25519Built-in support
Database for bot dataSet up separatelyBuilt-in
ScalingManual configurationAutomatic
Cost24/7 server costsPay per interaction

Discord Bot FAQ

Common questions about building Discord bots with HTTP interactions

What's the difference between HTTP interactions and gateway bots?
HTTP/Webhook bots receive interactions via POST requests to your endpoint - perfect for serverless, only runs when needed. Gateway bots maintain a persistent WebSocket connection to Discord - required for reading messages, reactions, presence, and voice. Choose HTTP for command-based bots, Gateway for full-featured bots.
How do I verify Discord interaction signatures?
Discord sends x-signature-ed25519 and x-signature-timestamp headers. Concatenate the timestamp with the raw request body, then verify using Ed25519 with your app's Public Key (found in Discord Developer Portal). Discord periodically tests your verification - failing will disable your endpoint.
Why is Discord rejecting my Interactions Endpoint URL?
Common causes: (1) Signature verification not implemented or failing, (2) Not responding to PING (type 1) with PONG (type 1), (3) Response takes more than 3 seconds, (4) Endpoint not publicly accessible. Check your logs and ensure you're using the raw request body for verification.
How do I register slash commands?
Use Discord's REST API: PUT to /applications/{app_id}/commands with an array of command objects. Global commands take up to an hour to propagate; guild commands are instant. You need the applications.commands scope and a bot token for registration.
What if my command takes longer than 3 seconds?
Use a deferred response: immediately return type 5 (DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE), which shows 'Bot is thinking...'. Then send your actual response within 15 minutes using PATCH /webhooks/{app_id}/{token}/messages/@original.
Can my HTTP bot read messages in channels?
No. HTTP interaction bots can only respond to direct interactions (slash commands, buttons, select menus, modals). To read messages, react to messages, or monitor channels, you need a gateway bot with WebSocket connection. Consider what features you actually need before choosing.
How do I send ephemeral (private) messages?
Add flags: 64 to your response data. Ephemeral messages are only visible to the user who triggered the interaction. Example: { type: 4, data: { content: 'Only you can see this', flags: 64 } }
Can I use both HTTP interactions and a gateway bot?
No, they're mutually exclusive per application. Once you set an Interactions Endpoint URL, all interactions go there instead of the gateway. To switch back, remove the URL from your app settings. You can create separate applications if you need both approaches.