@msgly/discord
v1.4.0
Published
Discord (HTTP Interactions) adapter for Msgly
Maintainers
Readme
@msgly/discord
Discord HTTP Interactions adapter for Msgly. Receive slash commands and button clicks, send replies through the unified hub. Zero classes, works in Node 20.13+, Bun, Deno, Cloudflare Workers, and Edge runtimes.
📖 Docs & channel reference: https://ayushjain070401.github.io/msgly/
Install
npm install @msgly/core @msgly/discordHow Discord fits Msgly
Discord is the only Msgly channel that's not message-DM-shaped. Bots receive events via two paths:
- Gateway (WebSocket) — every message in every channel the bot can see.
- HTTP Interactions — slash commands and button/select-menu clicks only.
This adapter implements HTTP Interactions because it fits the webhook model Msgly is built around. If you need to react to every free-form message in a channel (rather than to slash commands), you'll want a Gateway client alongside Msgly. PRs for a Gateway adapter are welcome.
Quick start
import express from 'express';
import { createHub } from '@msgly/core';
import { createDiscordAdapter } from '@msgly/discord';
const hub = createHub();
hub.register(
createDiscordAdapter({
applicationId: process.env.DISCORD_APPLICATION_ID!,
botToken: process.env.DISCORD_BOT_TOKEN!,
publicKey: process.env.DISCORD_PUBLIC_KEY!,
}),
);
await hub.connect({ throwOnFailure: true });
hub.on('message', async (msg) => {
if (msg.content.type === 'text') {
await hub.send({
channel: 'discord',
account: msg.account,
contact: msg.contact,
content: { type: 'text', text: `You said: ${msg.content.text}` },
// Pass the interaction token through so the reply edits the deferred
// response inline (no "thinking..." flicker on the user's screen).
metadata: { interactionToken: msg.metadata?.interactionToken },
});
}
});
const app = express();
app.use(express.json({ verify: (req, _r, buf) => ((req as any).rawBody = new Uint8Array(buf)) }));
const handlers = hub.createWebhookHandler();
app.post('/webhook/:channel', handlers.post);
app.listen(3000);Config
interface DiscordConfig {
/** Application ID — General Information tab of your Discord app. */
applicationId: string;
/** Bot token from the Bot tab. */
botToken: string;
/** Public Key from the General Information tab (hex, 64 chars). */
publicKey: string;
/** Override the API base. Defaults to https://discord.com/api. */
apiBase?: string;
/** API version. Defaults to v10. */
apiVersion?: string;
}Setup (15 minutes)
1. Create an application. Visit discord.com/developers/applications → New Application. Give it a name.
2. Copy three values from the dashboard:
- General Information tab:
- Application ID →
DISCORD_APPLICATION_ID - Public Key →
DISCORD_PUBLIC_KEY
- Application ID →
- Bot tab:
- Click Reset Token → copy immediately (Discord only shows it once) →
DISCORD_BOT_TOKEN
- Click Reset Token → copy immediately (Discord only shows it once) →
3. Register the interactions endpoint. Back on the General Information tab, find the Interactions Endpoint URL field and paste your public URL:
<PUBLIC_URL>/webhook/discordClick Save Changes. Discord will immediately PING your endpoint; if your server is running, the adapter responds with a PONG and Discord accepts the URL. If you see "validation failed", your server isn't running, the URL is wrong, or the public key is mismatched.
4. Register a slash command. This is a one-time HTTP call. The simplest way is via curl:
APP_ID="your_application_id"
TOKEN="your_bot_token"
curl -X POST \
-H "Authorization: Bot $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"echo","description":"Echo what you type","options":[{"name":"msg","description":"the message","type":3,"required":true}]}' \
"https://discord.com/api/v10/applications/$APP_ID/commands"(Global commands can take up to an hour to propagate. For instant testing, use guild-scoped commands at /applications/$APP_ID/guilds/$GUILD_ID/commands.)
5. Invite the bot to a server. OAuth2 → URL Generator → scopes: bot, applications.commands. Open the generated URL, pick a server, authorize.
6. Test. In any channel of that server, type /echo msg:hello. Your bot replaces the "thinking..." placeholder with You said: /echo msg=hello.
Inbound shape
Msgly normalizes Discord interactions to text messages:
| Discord interaction | content.text |
| -------------------------- | ------------------------------------- |
| /echo msg:hi slash cmd | /echo msg=hi |
| Button click custom_id=x | x |
| PING (type 1) | (no inbound message — ACK'd silently) |
Each inbound message exposes metadata.interactionToken, metadata.interactionId, metadata.userId, and metadata.guildId (when applicable) so you can route, audit, or reply.
Capabilities
| Feature | Supported | | ------------- | --------- | | text | ✓ | | image | ✓ (URL) | | video | ✓ (URL) | | audio | ✓ (URL) | | file | ✓ (URL) | | location | ✓ (as map link) | | buttons | ✓ | | quick replies | — | | reactions | — | | typing | — | | templates | — |
Media is sent by passing a public URL — Discord auto-embeds it in the message. Native multipart attachment upload is not yet implemented.
Reply path: deferred + followup
Discord enforces a 3-second deadline on every interaction response. Msgly's webhook handler immediately ACKs with {type: 5} (commands) or {type: 6} (components), which shows users a "thinking..." placeholder. When you later call hub.send with metadata.interactionToken, the adapter PATCHes the original deferred response — the placeholder is replaced inline, no second message.
If you call hub.send without metadata.interactionToken, the adapter falls back to POST /channels/{channel_id}/messages using the bot token (works for any channel the bot has been invited to).
Sending examples
Text reply to a slash command
await hub.send({
channel: 'discord',
account: msg.account,
contact: msg.contact,
content: { type: 'text', text: 'hello!' },
metadata: { interactionToken: msg.metadata?.interactionToken },
});Buttons
await hub.send({
channel: 'discord',
account, contact,
content: {
type: 'interactive',
text: 'Pick one:',
buttons: [
{ id: 'yes', label: 'Yes' },
{ id: 'no', label: 'No' },
],
},
metadata: { interactionToken: msg.metadata?.interactionToken },
});When the user taps a button, your hub.on('message', ...) handler receives a text message whose content.text equals the button's id ("yes" or "no").
Unsolicited message to a channel
await hub.send({
channel: 'discord',
account: { channel: 'discord', channelAccountId: process.env.DISCORD_APPLICATION_ID! },
contact: { channel: 'discord', channelUserId: 'CHANNEL_ID_HERE' },
content: { type: 'text', text: 'announcement!' },
});The bot must be a member of the channel for this to succeed.
Runtime requirements
Ed25519 signature verification uses WebCrypto. This is native in:
- Node 20.13+ (released May 2024)
- Bun (all recent versions)
- Deno (all recent versions)
- Cloudflare Workers, Vercel Edge, Netlify Edge
- Modern browsers (Chrome 113+, Firefox 130+, Safari 17+)
Node 20.12 and older lack subtle.verify for Ed25519 — the verification call will throw. Upgrade to Node 20.13+ (or 22.x).
Common pitfalls
- "Interactions Endpoint URL: validation failed": your server isn't running, isn't reachable at the URL you entered, or the
DISCORD_PUBLIC_KEYdoesn't match the app's actual public key. The adapter logs the verification error if you wirehub.on('error', ...). - "Unknown interaction" error when calling
hub.send: the deferred ack deadline (15 min) elapsed, or you're using a staleinteractionToken. Tokens are single-use for@originalPATCH followed by additional followup POSTs. - Slash command doesn't appear in the Discord client: global commands propagate slowly (up to 1 hour). For development, register against a specific guild — those are instant.
- "401 Unauthorized" on send: your bot token was reset in the dashboard. Re-copy and update
DISCORD_BOT_TOKEN.
Documentation
Full setup walkthrough and multi-channel usage: https://github.com/AyushJain070401/msgly
License
MIT
