TutorialCS2 API tutorials

Build a CS2 Discord score bot.

This bot posts a live CS2 scoreboard to a Discord channel and keeps editing the same message as the match goes: /follow picks a live match, one shared poll of GET /cs2/matches/live updates every followed scoreboard, and the final score is written when the match ends. About 90 lines of discord.js.

  • discord.js v14
  • One shared poll
  • Edits one message, no spam
GET /cs2/matches/live200 OK
$ curl "https://api.citoapi.com/api/v1/cs2/matches/live" \ -H "x-api-key: $CITO_API_KEY"
{  "success": true,  "data": [    {      "id": "cs2-match-2398974",      "eventName": "ROG JOURNEY Autumn 2026",      "bestOf": 3,      "team1Name": "Ninjas in Pyjamas",      "team2Name": "Eternal Fire",      "score": { "team1": 2, "team2": 0 }    },    {      "id": "cs2-match-2398896",      "eventName": "FOX Legacy Season 1",      "bestOf": 3,      "team1Name": "SAW",      "team2Name": "Lazer Cats",      "score": { "team1": 2, "team2": 0 }    }    ...  ]}

Step 1: What do I need before I start?

Node.js 18 or newer, a Discord application with a bot user (discord.com/developers), the bot invited to your server with the applications.commands and bot scopes, and a CS2 API key. Put the three secrets in environment variables.

Shell
npm init -y && npm i discord.js
export DISCORD_TOKEN=...      # Bot tab, Reset Token
export DISCORD_APP_ID=...     # General Information, Application ID
export CITO_API_KEY=cito_...  # citoapi.com/signup

Step 2: What is the bot's code?

One file. It registers /follow and /results, keeps a map of followed match IDs to Discord messages, and runs one poll loop that edits every followed message. When a match leaves the live list, the bot fetches its final score once and stops following it.

score-bot.mjs
import { Client, GatewayIntentBits, MessageFlags, REST, Routes, SlashCommandBuilder } from "discord.js"

const API = "https://api.citoapi.com/api/v1/cs2"
const H = { "x-api-key": process.env.CITO_API_KEY }
const POLL_MS = 60_000 // one request a minute for every followed match together
const followed = new Map() // matchId -> Discord message

const get = async (path) => (await (await fetch(API + path, { headers: H })).json()).data

const board = (m, final = false) =>
  [
    `**${m.team1Name} ${m.team1Score} - ${m.team2Score} ${m.team2Name}** (Bo${m.bestOf}) ${final ? "FINAL" : "LIVE"}`,
    final ? "" : `${m.currentMap ?? "?"}: ${m.currentMapScore?.team1 ?? 0}-${m.currentMapScore?.team2 ?? 0}, round ${m.currentRound ?? "?"}`,
    m.eventName ?? "",
  ].filter(Boolean).join("\n")

const commands = [
  new SlashCommandBuilder().setName("follow").setDescription("Post a self-updating scoreboard for a live CS2 match")
    .addStringOption((o) => o.setName("team").setDescription("Team name, e.g. Vitality").setRequired(true)),
  new SlashCommandBuilder().setName("results").setDescription("Latest finished CS2 matches"),
].map((c) => c.toJSON())

await new REST().setToken(process.env.DISCORD_TOKEN)
  .put(Routes.applicationCommands(process.env.DISCORD_APP_ID), { body: commands })

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

client.on("interactionCreate", async (i) => {
  if (!i.isChatInputCommand()) return
  if (i.commandName === "results") {
    const rows = await get("/matches/results?limit=5")
    return i.reply(rows.map((m) => `${m.team1Name} ${m.team1Score}-${m.team2Score} ${m.team2Name} · ${m.eventName}`).join("\n"))
  }
  const team = i.options.getString("team").toLowerCase()
  const match = (await get("/matches/live")).find((m) => `${m.team1Name} ${m.team2Name}`.toLowerCase().includes(team))
  if (!match) return i.reply({ content: `No live CS2 match for "${team}" right now.`, flags: MessageFlags.Ephemeral })
  await i.reply(board(match))
  followed.set(match.matchId, await i.fetchReply())
})

async function poll() {
  if (followed.size) {
    const live = new Map((await get("/matches/live")).map((m) => [m.matchId, m]))
    for (const [id, message] of followed) {
      if (live.has(id)) {
        await message.edit(board(live.get(id)))
      } else {
        await message.edit(board(await get(`/matches/${id}`), true)) // finished: final score, stop following
        followed.delete(id)
      }
    }
  }
  setTimeout(() => poll().catch(console.error), POLL_MS)
}

client.once("ready", () => {
  console.log("Logged in as", client.user.tag)
  poll()
})
client.login(process.env.DISCORD_TOKEN)

Step 3: How do I run it?

Run node score-bot.mjs, type /follow Vitality in a channel while Vitality is playing, and the bot posts the scoreboard and edits it every minute. /results lists the last five finished matches.

Shell
node score-bot.mjs

How many API requests does the bot use?

One request per poll, however many matches it follows, plus one per command and one per finished match. Polling every 60 seconds is about 43,200 requests a month if it runs non-stop, which fits the Pro plan (250,000); the free plan's 500 a month is for building and testing.

Poll intervalRequests / month (always on)Plan
60 s43,200Pro
30 s86,400Pro
15 s172,800Pro
Only while a match is followedFar fewerStarter or Pro, depending on traffic

CS2 live data refreshes every 15 seconds at the median, so polling faster than about 15 seconds returns the same score twice. Free and Starter see live data on a 60-second delay.

Can the bot get scores pushed instead of polling?

Yes. On Pro and Scale, register a webhook for cs2.score.updated and cs2.match.completed and update the message when the POST arrives; the webhook tutorial has the signature check. The SSE stream at /cs2/live/stream works on every plan too.

Frequently asked questions

Something missing? Email support@citoapi.com.

How do I make a CS2 live score Discord bot?

Use discord.js with a /follow command that posts a scoreboard message, then poll GET /cs2/matches/live once a minute and edit that message with the new score. The full code is above.

Will the bot spam the channel with messages?

No. It edits the one scoreboard message it posted, and writes the final score into it when the match ends.

Can I run the CS2 score bot on the free plan?

For testing, yes. Running it all day needs more than the free 500 requests a month; polling once a minute non-stop is about 43,200 a month, which fits Pro.

See pricing

Build CS2 live score apps

Free is for building and testing. Paid plans add commercial use, real-time live data and the full archive.