Skip to main content

Overview

This guide walks you through building a Discord bot that connects to TierZero, enabling your team to receive alerts, interact with AI responses, and manage incidents directly within Discord channels.
The Discord bot acts as a bridge between Discord and TierZero.

Architecture

Your Discord bot will:
  • Listen to Discord events (mentions, reactions, guild joins, guild leaves)
  • Forward relevant events to TierZero via webhooks
  • Receive commands from TierZero to send messages, reactions, and updates
  • Handle authentication and rate limiting

Outbound Events

Discord → TierZero Bot mentions, reactions, server joins

Inbound Commands

TierZero → Discord Send messages, add reactions, fetch data

Prerequisites

  • Discord Developer Application and Bot Token
  • HTTPS endpoint for webhook reception
  • TierZero API key (32-character hex string)

Data Model

Core Objects

All timestamps are RFC-3339/ISO-8601 format in UTC. Field order is illustrative; implementations may reorder fields.
Represents a Discord server where your bot is installed.
{
  "id": "1111222233334444",
  "name": "My Engineering Team",
  "icon_url": "https://cdn.discordapp.com/icons/...",
  "owner_id": "9999000011112222"
}
Text or voice channels within a server. Channel types include GUILD_TEXT, GUILD_VOICE, GUILD_PUBLIC_THREAD, GUILD_PRIVATE_THREAD, and others.
{
  "id": "5555666677778888",
  "server_id": "1111222233334444",
  "name": "incidents",
  "type": "GUILD_TEXT",
  "parent_id": null,
  "archived": false,
  "created_at": "2025-07-02T18:00:00Z"
}
Fields marked with ? in the spec (like parent_id?, archived?) are optional and may be null or omitted.
Specialized channels that are treated as threads (includes all Channel fields plus thread-specific metadata).
{
  "id": "7777888899990000",
  "server_id": "1111222233334444",
  "name": "incident-discussion",
  "type": "GUILD_PUBLIC_THREAD",
  "parent_id": "5555666677778888",
  "archived": false,
  "created_at": "2025-07-02T18:30:00Z",
  "starter_message_id": "123456789012345678",
  "member_count": 8,
  "message_count": 42
}
Discord members with profile information.
{
  "id": "9999000011112222",
  "username": "alice",
  "global_name": "Alice Chen",
  "avatar_url": "https://cdn.discordapp.com/avatars/9999000011112222/abc123.png",
  "bot": false
}
Chat messages with full context and metadata.
{
  "id": "123456789012345678",
  "server_id": "1111222233334444",
  "channel_id": "5555666677778888",
  "thread_id": null,
  "author": {
    "id": "9999000011112222",
    "username": "alice",
    "global_name": "Alice Chen",
    "avatar_url": "https://cdn.discordapp.com/avatars/...",
    "bot": false
  },
  "body": "@tierzero-bot check system status",
  "timestamp": "2025-07-02T19:01:57Z",
  "mentions_bot": true
}
Emoji reactions on messages with count and metadata.
{
  "emoji": {
    "id": "123456789012345678",
    "name": "custom_emoji"
  }
}
For standard Unicode emojis:
{
  "emoji": {
    "id": null,
    "name": "👍"
  }
}

Security & Authentication

Store your API key securely and never expose it in client-side code or logs.

Transport Security

  • HTTPS 1.2+ required for all communications
  • TLS certificate validation enforced

Authentication Header

Include this header in all requests (both directions):
x-tierzero-discord-key: <32-char-hex-api-key>

Replay Protection

Each outbound event includes an idempotency_key (UUID-v4). TierZero rejects duplicates within 24 hours.

Webhook Envelope Format

All webhook calls use this consistent envelope structure:
{
  "idempotency_key": "49c3e8c1-df6d-4caf-b7c7-f9236f6b5954",
  "sent_at": "2025-07-02T18:41:11Z",
  "event": "bot_mentioned",
  "version": "1.0",
  "data": {
    // Event-specific payload
  }
}

Outbound Events (Discord → TierZero)

Your bot should send these events to TierZero when they occur in Discord:

1. Server Joined

Trigger: GUILD_CREATE - Bot added to a new Discord server
Endpoint: POST https://api.tierzero.ai/discord/webhooks/server_joined
{
  "event": "server_joined",
  "data": {
    "server": {
      "id": "1111222233334444",
      "name": "Engineering Team",
      "icon_url": "https://cdn.discordapp.com/icons/...",
      "owner_id": "9999000011112222"
    }
  }
}

2. Bot Mentioned

Trigger: MESSAGE_CREATE - User mentions your bot in a message
Endpoint: POST https://api.tierzero.ai/discord/webhooks/bot_mentioned
{
  "event": "bot_mentioned",
  "data": {
    "message": {
      "id": "123456789012345678",
      "server_id": "1111222233334444",
      "channel_id": "5555666677778888",
      "author": {
        "id": "9999000011112222",
        "username": "alice",
        "global_name": "Alice Chen",
        "bot": false
      },
      "body": "@tierzero-bot can you check latency?",
      "timestamp": "2025-07-02T19:01:57Z",
      "mentions_bot": true
    }
  }
}

3. Reaction Added

Trigger: MESSAGE_REACTION_ADD - User adds a reaction to a message
Endpoint: POST https://api.tierzero.ai/discord/webhooks/reaction_added
{
  "event": "reaction_added",
  "data": {
    "message_id": "123456789012345678",
    "user": { /* User object */ },
    "reaction": {
      "emoji": { "id": null, "name": "👍" }
    }
  }
}

4. Server Left

Trigger: GUILD_DELETE - Bot removed from server
Endpoint: POST https://api.tierzero.ai/discord/webhooks/server_left
{
  "event": "server_left",
  "data": {
    "server_id": "1111222233334444"
  }
}

Inbound Webhooks (TierZero → Discord)

Your bot must expose these webhook endpoints for TierZero to call:

Read Operations

Path: POST /webhooks/get_usersLists all users in servers where the bot is present.
// Request
{ "data": {} }

// Response
{
  "status": "OK",
  "users": [
    {
      "id": "9999000011112222",
      "username": "alice",
      "global_name": "Alice Chen",
      "bot": false
    }
  ]
}
Path: POST /webhooks/get_server_channelsLists all channels in a specific server.
// Request
{
  "data": {
    "server_id": "1111222233334444"
  }
}

// Response
{
  "status": "OK",
  "channels": [
    {
      "id": "5555666677778888",
      "server_id": "1111222233334444",
      "name": "general",
      "type": "GUILD_TEXT",
      "parent_id": null,
      "archived": false,
      "created_at": "2025-07-02T18:00:00Z"
    },
    {
      "id": "7777888899990000",
      "server_id": "1111222233334444",
      "name": "incidents",
      "type": "GUILD_TEXT",
      "parent_id": null,
      "archived": false,
      "created_at": "2025-07-02T18:30:00Z"
    }
  ]
}
Path: POST /webhooks/get_channel_messagesRetrieves messages from a specific channel.
// Request
{
  "data": {
    "channel_id": "5555666677778888",
    "limit": 50,
    "after": "123456789012345678",
    "before": "987654321098765432"
  }
}

// Response
{
  "status": "OK",
  "messages": [ /* Array of Message objects */ ]
}
Path: POST /webhooks/get_channel_threadsLists all threads in a channel.
// Request
{
  "data": {
    "channel_id": "5555666677778888",
    "include_archived": true
  }
}

// Response
{
  "status": "OK",
  "threads": [ /* Array of Thread objects */ ]
}
Path: POST /webhooks/get_thread_messagesRetrieves messages from a specific thread.
// Request
{
  "data": {
    "thread_id": "7777888899990000",
    "after": "123456789012345678",
    "limit": 100
  }
}

// Response
{
  "status": "OK",
  "messages": [ /* Array of Message objects */ ]
}

Write Operations

Path: POST /webhooks/send_messageSends a message to a channel or thread.
This single endpoint handles both channel and thread messages. For thread messages, thread_id will be included in the request data instead of channel_id.
// Request (Channel Message)
{
  "data": {
    "channel_id": "5555666677778888",
    "body": "🚨 **Alert Detected**\n\nHigh CPU usage on prod-server-01\n\nUse `/t0 investigate` for details.",
    "embeds": [
      {
        "title": "System Alert",
        "description": "CPU usage exceeded threshold",
        "color": 16711680,
        "timestamp": "2025-07-02T19:05:00Z"
      }
    ],
    "components": [
      {
        "type": 1,
        "components": [
          {
            "type": 2,
            "style": 3,
            "label": "Investigate",
            "custom_id": "investigate_alert"
          }
        ]
      }
    ]
  }
}

// Request (Thread Message)
{
  "data": {
    "thread_id": "7777888899990000",
    "body": "🔍 **Investigation Update**\n\nRoot cause identified: Database connection pool exhausted\n\nApplying fix now...",
    "embeds": [
      {
        "title": "Thread Update",
        "description": "Investigation in progress",
        "color": 16776960,
        "timestamp": "2025-07-02T19:10:00Z"
      }
    ]
  }
}

// Response
{
  "status": "OK",
  "message": {
    "id": "135792468013579246",
    "timestamp": "2025-07-02T19:05:01Z"
  }
}
Path: POST /webhooks/add_reactionAdds a reaction to a message.
// Request
{
  "data": {
    "message_id": "123456789012345678",
    "emoji": { "name": "✅" }
  }
}

// Response
{
  "status": "OK"
}
Path: POST /webhooks/edit_messageEdits a message sent by the bot.
// Request
{
  "data": {
    "message_id": "135792468013579246",
    "new_body": "✅ **Alert Resolved**\n\nCPU usage back to normal levels.",
    "embeds": [
      {
        "title": "Status: Resolved",
        "description": "System metrics have returned to normal",
        "color": 65280,
        "timestamp": "2025-07-02T19:15:00Z"
      }
    ]
  }
}

// Response
{
  "status": "OK"
}
Path: POST /webhooks/delete_messageDeletes a message sent by the bot.
// Request
{
  "data": {
    "message_id": "135792468013579246"
  }
}

// Response
{
  "status": "OK"
}

Error Handling

Your bot should handle these HTTP status codes appropriately:
CodeMeaningResponse
400Invalid request{"error": "VALIDATION_ERROR", "details": "Missing channel_id"}
401Authentication failed{"error": "UNAUTHORIZED", "details": "Invalid API key"}
404Resource not found{"error": "NOT_FOUND", "details": "Channel not found"}
429Rate limit exceeded{"error": "RATE_LIMITED", "retry_at": "2025-07-02T19:10:00Z"}
5xxServer error{"error": "INTERNAL_ERROR", "details": "Temporary failure"}
Implement exponential backoff for 5xx errors and respect the retry_at timestamp for 429 responses.

Best Practices

  1. Queue requests internally to avoid hitting limits
  2. Use the same idempotency_key for retries
  3. Implement backoff strategies for rate limit responses
  4. Cache frequently accessed data to reduce API calls

Implementation Example

Here’s a basic TypeScript implementation structure using discord.js:
import { Client, GatewayIntentBits, Message, Guild, MessageReaction, User } from 'discord.js';
import express from 'express';
import { v4 as uuidv4 } from 'uuid';

interface TierZeroWebhookEnvelope {
  idempotency_key: string;
  sent_at: string;
  event: string;
  version: string;
  data: any;
}

class TierZeroBot {
  private client: Client;
  private tierZeroApiKey: string;
  private tierZeroBaseUrl: string = 'https://api.tierzero.ai';
  private app: express.Application;

  constructor(discordToken: string, tierZeroApiKey: string, port: number = 3000) {
    this.tierZeroApiKey = tierZeroApiKey;
    
    // Initialize Discord client
    this.client = new Client({
      intents: [
        GatewayIntentBits.Guilds,
        GatewayIntentBits.GuildMessages,
        GatewayIntentBits.MessageContent,
        GatewayIntentBits.GuildMessageReactions
      ]
    });

    // Initialize Express server for webhooks
    this.app = express();
    this.app.use(express.json());
    this.setupWebhookRoutes();
    this.setupDiscordEvents();

    this.client.login(discordToken);
    this.app.listen(port, () => {
      console.log(`TierZero Discord bot webhook server running on port ${port}`);
    });
  }

  private async sendToTierZero(event: string, data: any): Promise<void> {
    const payload: TierZeroWebhookEnvelope = {
      idempotency_key: uuidv4(),
      sent_at: new Date().toISOString(),
      event,
      version: '1.0',
      data
    };

    try {
      const response = await fetch(`${this.tierZeroBaseUrl}/discord/webhooks/${event}`, {
        method: 'POST',
        headers: {
          'x-tierzero-discord-key': this.tierZeroApiKey,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(payload)
      });

      if (!response.ok) {
        console.error(`TierZero API error: ${response.status}`);
      }
    } catch (error) {
      console.error('Failed to send event to TierZero:', error);
    }
  }

  private setupDiscordEvents(): void {
    this.client.on('guildCreate', async (guild: Guild) => {
      const serverData = {
        id: guild.id,
        name: guild.name,
        icon_url: guild.iconURL(),
        owner_id: guild.ownerId
      };
      
      await this.sendToTierZero('server_joined', { server: serverData });
    });

    this.client.on('messageCreate', async (message: Message) => {
      if (message.author.bot) return;
      
      if (message.mentions.has(this.client.user!)) {
        const messageData = {
          id: message.id,
          server_id: message.guild?.id || null,
          channel_id: message.channel.id,
          thread_id: message.channel.isThread() ? message.channel.id : null,
          author: {
            id: message.author.id,
            username: message.author.username,
            global_name: message.author.globalName,
            avatar_url: message.author.avatarURL(),
            bot: message.author.bot
          },
          body: message.content,
          embeds: message.embeds,
          attachments: message.attachments.map(a => ({ url: a.url, name: a.name })),
          reactions: [],
          timestamp: message.createdAt.toISOString(),
          edited_timestamp: message.editedAt?.toISOString() || null,
          mentions_bot: true
        };
        
        await this.sendToTierZero('bot_mentioned', { message: messageData });
      }
    });

    this.client.on('messageReactionAdd', async (reaction: MessageReaction, user: User) => {
      if (user.bot) return;
      
      const reactionData = {
        message_id: reaction.message.id,
        channel_id: reaction.message.channel.id,
        thread_id: reaction.message.channel.isThread() ? reaction.message.channel.id : null,
        user: {
          id: user.id,
          username: user.username,
          global_name: user.globalName,
          avatar_url: user.avatarURL(),
          bot: user.bot
        },
        reaction: {
          emoji: {
            id: reaction.emoji.id,
            name: reaction.emoji.name
          },
          count: reaction.count,
          me: reaction.users.cache.has(this.client.user!.id)
        }
      };
      
      await this.sendToTierZero('reaction_added', reactionData);
    });
  }

  private setupWebhookRoutes(): void {
    // Middleware to verify TierZero API key
    const verifyApiKey = (req: express.Request, res: express.Response, next: express.NextFunction) => {
      const apiKey = req.headers['x-tierzero-discord-key'];
      if (apiKey !== this.tierZeroApiKey) {
        return res.status(401).json({ error: 'UNAUTHORIZED', details: 'Invalid API key' });
      }
      next();
    };

    this.app.use('/webhooks', verifyApiKey);

    // Send message endpoint
    this.app.post('/webhooks/send_message', async (req, res) => {
      try {
        const { channel_id, thread_id, body, embeds, components } = req.body.data;
        const targetId = thread_id || channel_id;
        
        const channel = await this.client.channels.fetch(targetId);
        if (!channel?.isTextBased()) {
          return res.status(404).json({ error: 'NOT_FOUND', details: 'Channel not found' });
        }

        const message = await channel.send({
          content: body,
          embeds: embeds || [],
          components: components || []
        });

        res.json({
          status: 'OK',
          message: {
            id: message.id,
            timestamp: message.createdAt.toISOString()
          }
        });
      } catch (error) {
        res.status(500).json({ error: 'INTERNAL_ERROR', details: 'Failed to send message' });
      }
    });

    // Add reaction endpoint
    this.app.post('/webhooks/add_reaction', async (req, res) => {
      try {
        const { message_id, channel_id, emoji } = req.body.data;
        
        const channel = await this.client.channels.fetch(channel_id);
        if (!channel?.isTextBased()) {
          return res.status(404).json({ error: 'NOT_FOUND', details: 'Channel not found' });
        }

        const message = await channel.messages.fetch(message_id);
        await message.react(emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name);

        res.json({ status: 'OK' });
      } catch (error) {
        res.status(500).json({ error: 'INTERNAL_ERROR', details: 'Failed to add reaction' });
      }
    });
  }
}

// Usage
const bot = new TierZeroBot(
  process.env.DISCORD_TOKEN!,
  process.env.TIERZERO_API_KEY!,
  3000
);

Troubleshooting

TierZero API Errors

Symptoms: 401, 403, or 5xx responses from TierZero Solutions:
  • Verify x-tierzero-discord-key header is correct
  • Check request payload matches documented schema
  • Ensure HTTPS is used for all requests

Support

For technical support with your Discord bot integration: