HLTVEvery HLTV API guide

HLTV data in JavaScript, without the scraper.

There is no official HLTV API for JavaScript. The hltv npm package works by downloading hltv.org pages, and scripted requests now get a Cloudflare 403 "Just a moment..." page instead. In Node.js 18+, fetch from a CS2 API: results, HLTV-style ratings, rankings and live scores are one call each, with a free key.

  • Node 18+ fetch
  • TypeScript types
  • 500 free requests / month
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-2399131",      "eventName": "Stake Pulse Beat III",      "bestOf": 3,      "team1Name": "Ninjas in Pyjamas",      "team2Name": "Betclic",      "score": {        "team1": 2,        "team2": 1      }    },    {      "id": "cs2-match-2399130",      "eventName": "Stake Pulse Beat III",      "bestOf": 3,      "team1Name": "Inner Circle",      "team2Name": "Sangal",      "score": {        "team1": 2,        "team2": 0      }    },    "__more__"  ]}

Seamless Integration with our MCP Server

Give Claude, Cursor, and ChatGPT the CS2 data. One command writes the MCP config; agents call live scoreboard, team map stat, opening duel, and transfer tools instead of inventing match IDs, and your key stays on your machine.

MCPInstall the Cito MCP server
Claude
Cursor
OpenAI
Python
Cito API
Next.js
Discord
Slack
GitHub

Why does the hltv npm package fail?

It is a scraper: each function requests an hltv.org page and parses the HTML. On 7 October 2026 a scripted request to hltv.org got 403 Forbidden with cf-mitigated: challenge, a Cloudflare check that only a real browser passes, so the parser receives a challenge page instead of data.

How to detect the challenge in your own code: HLTV scraper alternative and the 403 fix.

  • Latest release: 3.5.0, published 20 September 2023; about 1,000 downloads a week (npm, read 7 October 2026).
  • When challenged it throws: Error: Access denied | www.hltv.org used Cloudflare to restrict access.
  • Its live scorebot connection has an open report of 403s since January 2025.
  • HLTV's terms of use, effective 27 January 2025, prohibit scraping the website (clause 2.2).

Step 1: How do I call the API from Node.js?

Node 18 and later ship fetch, so there is nothing to install. Keep the key on the server in an environment variable and send it in the x-api-key header; never put it in browser code.

Node.js 18+: cito.mjs
const API = "https://api.citoapi.com/api/v1/cs2"

export async function get(path, params = {}) {
  const url = new URL(API + path)
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v))
  const res = await fetch(url, { headers: { "x-api-key": process.env.CITO_API_KEY } })
  if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`)
  return res.json()
}

Step 2: How do I get results and upcoming matches?

GET /cs2/matches/results returns finished matches newest first; GET /cs2/matches/upcoming returns the schedule. Both carry team names, the event and the format.

Node.js
import { get } from "./cito.mjs"

const { data: results } = await get("/matches/results", { limit: 10 })
for (const m of results) console.log(`${m.eventName}: ${m.team1Name} ${m.team1Score}-${m.team2Score} ${m.team2Name}`)

const { data: upcoming } = await get("/matches/upcoming", { limit: 5 })
for (const m of upcoming) console.log(m.startsAt, m.team1Name, "vs", m.team2Name)

Step 3: How do I type the responses in TypeScript?

Declare the fields you use. Every response has success, data and meta; lists add paging fields to meta. The shapes below match production responses.

TypeScript
type Envelope<T> = { success: boolean; data: T; meta?: Record<string, unknown> }

type RankingRow = { rank: number; teamName: string; points: number; change: number | null; region: string | null }
type MapLine = { matchId: string; kills: number; deaths: number; adr: number | null; kast: number | null; rating: number | null }

const API = "https://api.citoapi.com/api/v1/cs2"

async function get<T>(path: string): Promise<T> {
  const res = await fetch(API + path, { headers: { "x-api-key": process.env.CITO_API_KEY ?? "" } })
  if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`)
  return ((await res.json()) as Envelope<T>).data
}

const top10 = await get<RankingRow[]>("/rankings/world?limit=10")
const lines = await get<MapLine[]>("/players/zywoo/matches?limit=5")
console.log(top10.map((t) => `${t.rank}. ${t.teamName}`).join("\n"), lines.length)

Step 4: How do I get live scores pushed instead of polling?

Open the Server-Sent Events stream at /cs2/live/stream and the server pushes score changes as they happen. Every plan can use it; it counts one request when it connects and one per 15 seconds open.

Node.js 18+: read the SSE stream
const res = await fetch("https://api.citoapi.com/api/v1/cs2/live/stream", {
  headers: { "x-api-key": process.env.CITO_API_KEY, accept: "text/event-stream" },
})
const decoder = new TextDecoder()
let buffer = ""
for await (const chunk of res.body) {
  buffer += decoder.decode(chunk, { stream: true })
  let i
  while ((i = buffer.indexOf("\n\n")) >= 0) {
    const event = buffer.slice(0, i)
    buffer = buffer.slice(i + 2)
    const data = event.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5)).join("")
    if (data) console.log(JSON.parse(data))
  }
}

Can I call it from the browser?

Not directly with your key: anything in browser code is public. Put a small server route in front (a Next.js route handler, an Express endpoint or a Cloudflare Worker) that adds the key and caches responses for a few seconds.

Endpoints you can ship with

Every endpoint the Node.js examples call.

Live matches

Poll the live list for every match in progress, open one match for its round, timer, bomb and players, or keep an SSE stream open and get each update pushed.

View docs
  • GET/api/v1/cs2/live

    Matches in progress: series score, map in play, map score, current round.

  • GET/api/v1/cs2/live/{matchId}/state

    One live match: score, round, round timer, bomb status and players.

  • GET/api/v1/cs2/live/{matchId}/scoreboard

    Series score, current map and round, and each player's kills, deaths, money and equipment.

  • GET/api/v1/cs2/live/{matchId}/rounds

    Round-by-round results so far: winner, side and how each round ended.

  • GET/api/v1/cs2/live/stream

    Server-Sent Events push of every live update. Every plan; counts like polling.

  • GET/api/v1/cs2/live/ws

    WebSocket with a room per match. Scale and Enterprise.

FAQs

HLTV and CS2 data in JavaScript

Can't find what you're looking for? Contact our customer support team

Read the CS2 docs

Build in JavaScript: Discord bots

Create a free key and run the examples above.