TutorialCS2 API tutorials

CS2 match alerts by webhook.

Register an HTTPS endpoint in the Cito dashboard, pick events such as cs2.match.completed or cs2.round.ended, and the API POSTs a signed JSON payload to you when they happen. Verify X-Cito-Signature (HMAC-SHA256 of the raw body), answer 2xx fast, and dedupe on X-Cito-Event-Id.

  • 16 CS2 events
  • HMAC-SHA256 signed
  • Pro and Scale plans
GET /cs2/webhooks/events200 OK
$ curl "https://api.citoapi.com/api/v1/cs2/webhooks/events" \ -H "x-api-key: $CITO_API_KEY"
{  "success": true,  "available_on_plans": [    "PRO",    "BUSINESS"  ],  "events": [    "cs2.match.started",    "cs2.round.ended",    "cs2.map.completed",    "cs2.match.completed"    ...  ],  "signature_headers": [    "X-Cito-Signature",    "X-Cito-Timestamp"  ]}

Which CS2 events can trigger a webhook?

Sixteen events cover the life of a match, from cs2.match.started through every round to cs2.match.completed, plus ranking updates. GET /cs2/webhooks/events returns the current list, the signing contract and an example payload.

StageEvents
Matchcs2.match.started, cs2.match.live_started, cs2.match.live_ended, cs2.match.completed
Mapcs2.map.started, cs2.map.completed
Roundcs2.round.started, cs2.round.ended, cs2.score.updated
Bombcs2.bomb.planted, cs2.bomb.defused, cs2.bomb.exploded
Players and statecs2.player.state_changed, cs2.live.state, cs2.live.player_stats_updated
Rankingscs2.ranking.updated

Step 1: How do I create a webhook?

Open Webhooks in the Cito dashboard, add your HTTPS URL and the CS2 events you want, and copy the webhook's signing secret into an environment variable. Webhooks are included on the Pro and Scale plans.

Dashboard: citoapi.com/dashboard/webhooks. Every delivery carries these headers: X-Cito-Signature, X-Cito-Event, X-Cito-Event-Id, X-Cito-Timestamp and X-Cito-Delivery-Attempt.

Step 2: How do I verify the signature?

Compute HMAC-SHA256 over the exact raw request body with your secret, hex-encode it, and compare it with X-Cito-Signature using a constant-time comparison. Do it before parsing the JSON; re-serialising changes the bytes and the check fails.

Node.js + Express
import crypto from "node:crypto"
import express from "express"

const app = express()
const SECRET = process.env.CITO_WEBHOOK_SECRET
const seen = new Set() // use Redis or your DB in production

app.post("/cito/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex")
  const got = req.get("X-Cito-Signature") ?? ""
  const ok = got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))
  if (!ok) return res.sendStatus(401)

  const id = req.get("X-Cito-Event-Id")
  if (seen.has(id)) return res.sendStatus(200)   // retried delivery, already handled
  seen.add(id)

  res.sendStatus(200)                            // answer first, work after
  const { event, data } = JSON.parse(req.body.toString("utf8"))
  if (event === "cs2.match.completed") notify(data)
})

function notify(data) { console.log("match completed", data) }
app.listen(3000)
Python + Flask
import hashlib, hmac, os, json
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["CITO_WEBHOOK_SECRET"].encode()

@app.post("/cito/webhook")
def cito_webhook():
    body = request.get_data()                         # raw bytes, before parsing
    expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-Cito-Signature", "")):
        abort(401)
    payload = json.loads(body)
    print(payload["event"], payload["data"])
    return "", 200

Step 3: How do I post the alert to Discord or Slack?

Inside the handler, turn the payload into a message and POST it to the channel's incoming-webhook URL. Keep that call after you have answered Cito with 200, so a slow chat API never makes the delivery look failed.

Node.js: forward cs2.match.completed to a Discord channel
async function notify(data) {
  await fetch(process.env.DISCORD_WEBHOOK_URL, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ content: "CS2 match finished: " + JSON.stringify(data) }),
  })
}

What if I am on the free plan?

Use the Server-Sent Events stream at /api/v1/cs2/live/stream, which every plan can open, or poll GET /cs2/matches/results on a timer. Both work without a public HTTPS endpoint, which also makes them easier while developing.

Frequently asked questions

Something missing? Email support@citoapi.com.

Does the CS2 API support webhooks?

Yes, on the Pro and Scale plans: 16 CS2 events, from cs2.match.started and cs2.round.ended to cs2.match.completed and cs2.ranking.updated.

How are CS2 API webhooks signed?

HMAC-SHA256 of the exact raw JSON body with your webhook secret, hex-encoded in the X-Cito-Signature header, with no prefix.

Can a webhook be delivered twice?

Treat it as possible: X-Cito-Delivery-Attempt counts attempts, and X-Cito-Event-Id is stable across them, so dedupe on the event id.

How fast do webhooks arrive?

Cito's dispatcher delivered first attempts a median 1.2 s after the event was recorded (all games, 24 hours to 3 October 2026). CS2 live data itself refreshes every 15 seconds at the median.

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.