Prefer to chat? Query the API with natural language using our AI Skill →

Give your AI tool live Cito endpoint context before you write integration code.

Open AI Skill

Public docs assistant

Tell Cito API what you are building.

Get endpoint recommendations, exact requests, code snippets, and a free-key CTA without digging through every page.

Guides/Building a Discord Bot

Building a Discord Bot

Real-time match notifications and player lookups

In this guide, we'll build a Discord bot that monitors live Fortnite matches and posts updates to a channel. The bot will also respond to slash commands for player lookups.

What you'll learn:
  • • Setting up a Discord bot with discord.js
  • • Polling Cito API for live matches
  • • Using webhooks for real-time updates
  • • Creating slash commands for player lookups

Prerequisites

  • Node.js 18+ installed
  • A Discord account and server
  • A Cito API key (get one free)

Step 1: Project Setup

mkdir fortnite-bot && cd fortnite-bot
npm init -y
npm install discord.js dotenv

Create a .env file with your credentials:

.env
DISCORD_TOKEN=your_discord_bot_token
CITO_API_KEY=cito_live_your_key_here
CHANNEL_ID=your_channel_id

Step 2: Basic Bot Structure

index.js
require('dotenv').config();
const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');

const client = new Client({
  intents: [GatewayIntentBits.Guilds]
});

async function citoGet(path) {
  const response = await fetch(`https://api.citoapi.com/api/v1${path}`, {
    headers: { 'x-api-key': process.env.CITO_API_KEY }
  });

  if (!response.ok) throw new Error(`Cito API error: ${response.status}`);
  return response.json();
}

client.once('ready', () => {
  console.log(`Logged in as ${client.user.tag}`);
  startMatchMonitor();
});

client.login(process.env.DISCORD_TOKEN);

Step 3: Match Monitor (Polling)

Poll for live matches every 30 seconds and post updates:

const seenMatches = new Set();

async function startMatchMonitor() {
  const channel = client.channels.cache.get(process.env.CHANNEL_ID);

  setInterval(async () => {
    try {
      const data = await citoGet('/cod/matches/live');
      const matches = data.matches || data.data || [];

      for (const match of matches) {
        if (!seenMatches.has(match.id)) {
          seenMatches.add(match.id);

          const embed = new EmbedBuilder()
            .setColor(0x00E5CC)
            .setTitle(`LIVE: ${match.tournament || 'Call of Duty League'}`)
            .setDescription(match.status || 'Live match')
            .addFields(
              { name: 'Status', value: match.status || 'live', inline: true },
              { name: 'Match ID', value: match.id, inline: true }
            )
            .setTimestamp();

          await channel.send({ embeds: [embed] });
        }
      }
    } catch (error) {
      console.error('Error fetching matches:', error);
    }
  }, 30000); // 30 seconds
}

Step 4: Using Webhooks (Recommended)

Instead of polling, use webhooks for instant notifications:

server.js
const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

app.post('/webhook', (req, res) => {
  // Verify signature
  const signature = req.headers['x-cito-signature'];
  const expectedSig = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (signature !== expectedSig) {
    return res.status(401).send('Invalid signature');
  }

  const { event, data } = req.body;
  const channel = client.channels.cache.get(process.env.CHANNEL_ID);

  if (event === 'match.started') {
    const embed = new EmbedBuilder()
      .setColor(0x00E5CC)
      .setTitle(`Match Started: ${data.tournament}`)
      .setDescription(`${data.players_count} players`)
      .setTimestamp();

    channel.send({ embeds: [embed] });
  }

  res.sendStatus(200);
});

app.listen(3000, () => console.log('Webhook server running'));

Step 5: Slash Commands

Add player lookup commands:

const { SlashCommandBuilder } = require('discord.js');

const commands = [
  new SlashCommandBuilder()
    .setName('player')
    .setDescription('Look up a Fortnite player')
    .addStringOption(option =>
      option.setName('username')
        .setDescription('Player username')
        .setRequired(true)
    )
];

// Register commands
client.once('ready', async () => {
  await client.application.commands.set(commands);
});

// Handle interactions
client.on('interactionCreate', async (interaction) => {
  if (!interaction.isChatInputCommand()) return;

  if (interaction.commandName === 'player') {
    await interaction.deferReply();

    const username = interaction.options.getString('username');

    try {
      const { data: player } = await cito.fortnite.players.get(username);

      const embed = new EmbedBuilder()
        .setColor(0x00E5CC)
        .setTitle(player.username)
        .addFields(
          { name: 'Wins', value: player.stats.career.wins.toString(), inline: true },
          { name: 'K/D', value: player.stats.career.kd_ratio.toFixed(2), inline: true },
          { name: 'Earnings', value: `$${player.stats.competitive.earnings.toLocaleString()}`, inline: true }
        );

      await interaction.editReply({ embeds: [embed] });
    } catch (error) {
      await interaction.editReply(`Player "${username}" not found.`);
    }
  }
});