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! ๐
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โ
- Go to the Discord Developer Portal
- Click New Application
- Enter a name and create
2. Get Your Public Keyโ
- Go to General Information
- Copy the Public Key (you'll need this for signature verification)
- Set it in Codehooks:
coho set-env DISCORD_PUBLIC_KEY your-public-key-here
3. Set the Interactions Endpoint URLโ
- In the Developer Portal, go to General Information
- Enter your Interactions Endpoint URL:
https://mydiscordbot-a1b2.api.codehooks.io/dev/interactions - 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โ
- Go to OAuth2 โ URL Generator
- Select scopes:
applications.commands - Copy the URL and open it to invite the bot
Interaction Typesโ
| Type | Name | Description |
|---|---|---|
| 1 | PING | Discord verifying your endpoint |
| 2 | APPLICATION_COMMAND | Slash commands |
| 3 | MESSAGE_COMPONENT | Buttons, select menus |
| 4 | APPLICATION_COMMAND_AUTOCOMPLETE | Autocomplete suggestions |
| 5 | MODAL_SUBMIT | Modal form submissions |
Response Typesโ
| Type | Name | Description |
|---|---|---|
| 1 | PONG | Response to PING |
| 4 | CHANNEL_MESSAGE_WITH_SOURCE | Send a message |
| 5 | DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE | Acknowledge, send message later |
| 6 | DEFERRED_UPDATE_MESSAGE | Acknowledge component, edit later |
| 7 | UPDATE_MESSAGE | Edit the original message |
| 8 | APPLICATION_COMMAND_AUTOCOMPLETE_RESULT | Autocomplete results |
| 9 | MODAL | Open 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;
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โ
| Feature | HTTP (Interactions) | Gateway (WebSocket) |
|---|---|---|
| Connection | On-demand | Always connected |
| Hosting | Serverless friendly | Requires persistent server |
| Slash commands | โ Yes | โ Yes |
| Buttons/Menus | โ Yes | โ Yes |
| Read messages | โ No | โ Yes |
| Presence updates | โ No | โ Yes |
| Voice | โ No | โ Yes |
| Reactions | โ No | โ Yes |
| Cost | Pay per use | 24/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?โ
| Feature | DIY Setup | Codehooks.io |
|---|---|---|
| Setup time | Hours | 5 minutes |
| Server management | Configure & maintain | Serverless |
| Signature verification | Implement Ed25519 | Built-in support |
| Database for bot data | Set up separately | Built-in |
| Scaling | Manual configuration | Automatic |
| Cost | 24/7 server costs | Pay per interaction |
Related Resourcesโ
- Discord Bot Template - Ready-to-deploy template
- Discord Developer Portal
- Discord Interactions Documentation
- Codehooks.io Quick Start
Discord Bot FAQ
Common questions about building Discord bots with HTTP interactions
What's the difference between HTTP interactions and gateway bots?
How do I verify Discord interaction signatures?
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?
How do I register slash commands?
/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?
PATCH /webhooks/{app_id}/{token}/messages/@original.Can my HTTP bot read messages in channels?
How do I send ephemeral (private) messages?
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 } }