Installation
# npm
npm install @disqua/sdk
# pnpm
pnpm add @disqua/sdk
# yarn
yarn add @disqua/sdk
Requires Node.js 18+ and TypeScript 5.0+. The SDK ships with full type definitions — no @types package needed.
| Package | Description | Version |
|---|---|---|
| @disqua/sdk | Main SDK — client, events, slash commands, Block Kit | 1.x |
| @disqua/sdk/express | Express middleware for slash command endpoints | 1.x |
| @disqua/sdk/fastify | Fastify plugin for slash command endpoints | 1.x |
Create a bot
First, create a Bot integration in your workspace (Settings → Integrations → Bots → New Bot). You'll get a bot token — treat it like a password.
import { DisquaClient } from '@disqua/sdk';
const client = new DisquaClient({
token: process.env.DISQUA_BOT_TOKEN!, // bot token from workspace settings
workspaceId: process.env.DISQUA_WORKSPACE_ID!,
});
// Connect and start listening for events
await client.connect();
console.log('Bot connected and ready!');
// Graceful shutdown
process.on('SIGTERM', () => client.disconnect());
client.connect() establishes a WebSocket connection to wss://api.disqua.com/ws with automatic reconnect and heartbeat management.
Event handlers
Subscribe to workspace events using client.on(). All event payloads are fully typed.
import { DisquaClient, MessageCreatedEvent } from '@disqua/sdk';
const client = new DisquaClient({ token: process.env.DISQUA_BOT_TOKEN!, workspaceId: '...' });
// Respond to messages that mention the bot
client.on('message.created', async (event: MessageCreatedEvent) => {
const { message, channel, author } = event;
// Ignore messages from bots to prevent loops
if (author.isBot) return;
// React when someone says "hello" to the bot
if (message.content.toLowerCase().includes('hello')) {
await client.messages.send(channel.id, {
content: `Hello, **${author.displayName}**! How can I help you?`,
});
}
});
// Listen for new members joining the workspace
client.on('member.joined', async (event) => {
const { user, workspace } = event;
// Find the #general channel
const channels = await client.channels.list(workspace.id);
const general = channels.find(c => c.slug === 'general');
if (general) {
await client.messages.send(general.id, {
content: `Welcome to the team, **${user.displayName}**! 🎉`,
});
}
});
// React to reactions
client.on('reaction.added', async (event) => {
if (event.emoji === '🚀') {
// Someone added a rocket — trigger your CI pipeline, etc.
console.log(`Rocket reaction by ${event.userId} on message ${event.messageId}`);
}
});
await client.connect();
Available events
| Event | TypeScript type | Description |
|---|---|---|
| message.created | MessageCreatedEvent | New message in any subscribed channel |
| message.updated | MessageUpdatedEvent | Message edited |
| message.deleted | MessageDeletedEvent | Message deleted |
| reaction.added | ReactionEvent | Reaction added to a message |
| reaction.removed | ReactionEvent | Reaction removed |
| member.joined | MemberJoinedEvent | New member joined workspace |
| member.left | MemberLeftEvent | Member left workspace |
| channel.created | ChannelCreatedEvent | New channel created |
| channel.archived | ChannelArchivedEvent | Channel archived |
| slash_command | SlashCommandEvent | User invoked a slash command |
Slash commands
Slash commands let users interact with your bot by typing /commandname [args] in the message composer. Register them in Workspace Settings, then handle them in your bot.
Slash command payloads are sent to your Request URL (configured in Settings). You must respond with a JSON payload within 3 seconds, or Disqua shows a timeout error to the user.
import express from 'express';
import { createSlashCommandHandler, SlashCommandContext } from '@disqua/sdk/express';
const app = express();
const handler = createSlashCommandHandler({
signingSecret: process.env.DISQUA_SIGNING_SECRET!,
commands: {
// /giphy <query> — post a gif
giphy: async (ctx: SlashCommandContext) => {
const query = ctx.args.join(' ');
const gifUrl = await fetchGif(query); // your own logic
return ctx.respond({
content: `Here's a GIF for "${query}":`,
attachments: [{ type: 'image', url: gifUrl }],
responseType: 'in_channel', // visible to everyone
});
},
// /remind @user in 1h <message>
remind: async (ctx: SlashCommandContext) => {
// Parse args, schedule a job, etc.
return ctx.respond({
content: `Reminder set!`,
responseType: 'ephemeral', // visible only to caller
});
},
// /standup — starts an interactive standup
standup: async (ctx: SlashCommandContext) => {
return ctx.respond({
content: "Let's do a standup! Fill in your update:",
blocks: [
{
type: 'input',
blockId: 'standup_input',
label: 'What did you work on yesterday?',
element: { type: 'plain_text_input', actionId: 'yesterday' },
},
{
type: 'input',
blockId: 'standup_today',
label: 'What are you working on today?',
element: { type: 'plain_text_input', actionId: 'today' },
},
{
type: 'actions',
elements: [
{ type: 'button', text: 'Submit', actionId: 'submit_standup', style: 'primary' },
],
},
],
responseType: 'ephemeral',
});
},
},
});
app.post('/slack/commands', handler);
app.listen(3000);
SlashCommandContext properties
| Property | Type | Description |
|---|---|---|
| command | string | Command name without slash, e.g. "giphy" |
| text | string | Raw argument string after the command |
| args | string[] | Arguments split by whitespace |
| userId | string | UUID of the user who invoked the command |
| channelId | string | UUID of the channel where it was invoked |
| workspaceId | string | UUID of the workspace |
| respond() | function | Send a response (immediate or deferred) |
| ack() | function | Acknowledge within 3s, respond later via ctx.respond() |
Sending messages
// Simple text message (supports markdown)
await client.messages.send(channelId, {
content: 'Hello **world**! Visit https://disqua.com',
});
// Reply in a thread
await client.messages.send(channelId, {
content: 'This is a thread reply.',
threadId: parentMessageId,
});
// Message with file attachment (use presigned upload flow first)
await client.messages.send(channelId, {
content: 'Here is your report:',
attachmentIds: [uploadedFileId],
});
// Update an existing message (bot's own messages only)
await client.messages.update(messageId, {
content: 'Updated content',
});
// Delete a message (bot's own messages only)
await client.messages.delete(messageId);
// Add a reaction
await client.reactions.add(messageId, '👍');
// List recent messages in a channel
const { data: messages } = await client.messages.list(channelId, {
limit: 50,
});
Block Kit — interactive messages
Block Kit lets you compose rich, interactive messages with buttons, inputs, select menus, and more. Blocks are rendered natively in the Disqua client.
import { Blocks, Elements } from '@disqua/sdk';
await client.messages.send(channelId, {
content: 'Deployment approval required:',
blocks: [
Blocks.Section({
text: '*Deploy v2.4.1 to production?*\nService: `api` • Branch: `main` • Author: @jane',
}),
Blocks.Divider(),
Blocks.Section({
text: '*Changes:*\n• Fix rate limiting bug\n• Update dependency versions\n• Add new /health endpoint',
}),
Blocks.Actions({
elements: [
Elements.Button({
text: 'Approve',
actionId: 'deploy_approve',
style: 'primary',
value: JSON.stringify({ deployId: '123', version: '2.4.1' }),
confirm: {
title: 'Confirm deployment',
text: 'This will deploy to production immediately.',
confirmText: 'Deploy now',
denyText: 'Cancel',
},
}),
Elements.Button({
text: 'Reject',
actionId: 'deploy_reject',
style: 'danger',
}),
Elements.Button({
text: 'View diff',
actionId: 'deploy_diff',
url: 'https://github.com/your-org/repo/compare/v2.4.0...v2.4.1',
}),
],
}),
],
});
// Handle button clicks
client.on('block_action', async (event) => {
if (event.actionId === 'deploy_approve') {
const { deployId } = JSON.parse(event.value);
await triggerDeployment(deployId);
// Update the message to show approval
await client.messages.update(event.messageId, {
content: `Deployment approved by **${event.user.displayName}**`,
blocks: [
Blocks.Section({ text: 'Deployment to production is in progress...' }),
],
});
}
if (event.actionId === 'deploy_reject') {
await client.messages.update(event.messageId, {
content: `Deployment rejected by **${event.user.displayName}**`,
blocks: [],
});
}
});
Available block types
Blocks.Section
Markdown text, with optional accessory (image, button, select)
Blocks.Actions
Row of interactive elements (buttons, select menus)
Blocks.Input
Form input — text, multiline, date picker, user select
Blocks.Header
Large heading text
Blocks.Divider
Horizontal rule separator
Blocks.Image
Inline image with alt text and optional title
SDK API reference
All methods return Promises. Errors throw a typed DisquaError with code, message, and statusCode.
| Namespace | Methods |
|---|---|
| client.messages | send(), list(), get(), update(), delete() |
| client.channels | list(), get(), create(), update(), archive(), addMember(), removeMember() |
| client.users | list(), get(), me() |
| client.reactions | add(), remove() |
| client.files | upload(), get(), list(), delete() |
| client.on() | Subscribe to a WS event |
| client.off() | Unsubscribe from a WS event |
| client.connect() | Establish WS connection |
| client.disconnect() | Gracefully close WS connection |
Deployment
A basic Disqua bot is a long-running Node.js process. Deploy it anywhere you run Node.js.
{
"scripts": {
"build": "tsc",
"start": "node dist/bot.js",
"dev": "tsx watch src/bot.ts"
}
}
DISQUA_BOT_TOKEN=dqb_...
DISQUA_WORKSPACE_ID=01HQ6Z...
DISQUA_SIGNING_SECRET=... # for slash commands
PM2 (VPS)
pm2 start dist/bot.js --name my-bot
Railway / Render
Push to GitHub, set env vars, deploy. Zero config.
Serverless (slash only)
Deploy just the HTTP handler to any serverless platform. No WS needed for slash commands.
Ready to build your bot?
Create a free Disqua account, set up a bot integration, and deploy in minutes.