HLTVEvery HLTV API guide

HLTV returns 403 "Just a moment...": what happened.

hltv.org answers scripted requests with a Cloudflare challenge: 403 Forbidden, the header cf-mitigated: challenge and a "Just a moment..." page that needs JavaScript and cookies. We saw it on 7 October 2026 even with a browser user agent. It is a challenge, not data, so new headers and retries will not get through.

  • HTTP 403
  • cf-mitigated: challenge
  • 500 free requests / month
GET /cs2/matches/results?limit=2200 OK
$ curl "https://api.citoapi.com/api/v1/cs2/matches/results?limit=2" \ -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

The error

What you see
HTTP/1.1 403 Forbidden
Server: cloudflare
cf-mitigated: challenge

<title>Just a moment...</title>
Enable JavaScript and cookies to continue

What does the 403 "Just a moment..." response mean?

Cloudflare, which sits in front of hltv.org, decided your request needs a browser check before it reaches the site. Instead of the page you asked for, it sends a challenge page. Your parser then finds no tables and fails, or worse, quietly stores nothing.

What you seeWhat it means
HTTP 403 ForbiddenThe request was stopped at Cloudflare, not served by HLTV.
Response header cf-mitigated: challengeCloudflare issued a challenge. This header is the reliable signal to test for.
Page title "Just a moment..."The challenge interstitial. It needs JavaScript to run in a real browser.
"Enable JavaScript and cookies to continue"The no-JavaScript fallback text of the same challenge page.
Access denied | www.hltv.org used Cloudflare to restrict accessThe hltv npm package's error when it receives the challenge page.
CloudflareChallengeError: Detected a Cloudflare version 2 challengePython cloudscraper cannot solve the current challenge type.
Error 1015 "You are being rate limited"A Cloudflare rate limit: too many requests from your IP.
Error 1020 "Access denied"A firewall rule blocked the request outright.

Why does it happen to code that used to work?

The challenge is decided per request from signals your HTTP library cannot change by setting headers: how the TLS connection is made, whether JavaScript runs, whether the clearance cookie exists, and how your IP has behaved. A copied browser user agent does not help; on 7 October 2026 a request with a Chrome user agent got the same 403.

  • requests, httpx, urllib, fetch, axios and curl do not run JavaScript, so they can never pass a JavaScript challenge on their own.
  • Cloud and datacenter IP ranges are challenged more often than home connections, so a script that works on your laptop can fail on a server.
  • Retrying in a loop makes it worse: repeated failed requests from one IP raise its risk score and can turn a challenge into a 1015 or 1020 block.
  • Challenge solvers and headless-browser farms turn your project into an arms race with the site's protection, and HLTV's terms of use (effective 27 January 2025, clause 2.2) prohibit data mining or web scraping of the site.

How do I stop the scraper from failing silently?

Check for the challenge on every response and stop with a clear error instead of parsing it. A challenge page has status 403 and the cf-mitigated: challenge header; test both, back off, and move the job to an API that returns JSON with a key.

Python: detect the challenge and fail fast
import requests

class Challenged(Exception):
    pass

def fetch_page(url: str) -> str:
    r = requests.get(url, timeout=15)
    if r.status_code in (403, 429, 503) and (
        r.headers.get("cf-mitigated") == "challenge" or "Just a moment" in r.text[:2000]
    ):
        # A challenge page is not data: do not parse it, do not retry in a loop.
        raise Challenged(f"{url} returned a Cloudflare challenge ({r.status_code})")
    r.raise_for_status()
    return r.text
Node.js 18+: the same check
export class Challenged extends Error {}

export async function fetchPage(url) {
  const res = await fetch(url)
  const head = (await res.clone().text()).slice(0, 2000)
  if ([403, 429, 503].includes(res.status) &&
      (res.headers.get("cf-mitigated") === "challenge" || head.includes("Just a moment"))) {
    // A challenge page is not data: do not parse it, do not retry in a loop.
    throw new Challenged(`${url} returned a Cloudflare challenge (${res.status})`)
  }
  if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`)
  return res.text()
}
Python: the same job from the CS2 API
import os, requests

API = "https://api.citoapi.com/api/v1/cs2"
H = {"x-api-key": os.environ["CITO_API_KEY"]}

r = requests.get(f"{API}/matches/results", headers=H, params={"limit": 10}, timeout=10)
r.raise_for_status()
for m in r.json()["data"]:
    print(m["eventName"], "|", m["team1Name"], m["team1Score"], "-", m["team2Score"], m["team2Name"])

Which scraper jobs move to which endpoint?

HLTV scrapers almost always do one of six jobs. Each is one documented GET with a stable JSON shape, so nothing breaks when a web page is redesigned or challenged.

Scraper jobEndpoint
Results pageGET /cs2/matches/results
Upcoming matchesGET /cs2/matches/upcoming
Live scoresGET /cs2/matches/live (or the /cs2/live/stream SSE stream)
Player stats pageGET /cs2/players/{slug}/stats and /cs2/players/{slug}/matches
Top players listGET /cs2/leaderboards/rating
Team rankingGET /cs2/rankings/world and /cs2/rankings/vrs

How does the CS2 API make this impossible?

There is no web page between you and the data, so there is nothing to challenge: you send an x-api-key header and get JSON from api.citoapi.com. Limits are published per plan and every response says how many requests you have left, so a busy day slows you down predictably instead of blocking your IP. CS2 API is independent and not affiliated with HLTV.

Endpoints you can ship with

The endpoints that replace each scraper job.

Results

Finished matches with the series score and winner, every map and its score, and the player lines behind them.

View docs
  • GET/api/v1/cs2/matches/recent

    Finished matches with final series score and winner, newest first.

  • GET/api/v1/cs2/matches/{matchId}

    One match: event, teams, series score, and maps.

  • GET/api/v1/cs2/matches/{matchId}/maps

    Each map: name, score, winner, and the team that picked it.

  • GET/api/v1/cs2/matches/{matchId}/player-stats

    Per-player stats for each map: kills, deaths, ADR, KAST and rating.

  • GET/api/v1/cs2/teams/{teamIdOrSlug}/results

    One team's finished matches, newest first.

  • GET/api/v1/cs2/events/{eventIdOrSlug}/results

    Final placements and prize money of a tournament.

FAQs

HLTV 403, Cloudflare and scraping

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

Read the CS2 docs

Stop fighting Cloudflare

One key, documented JSON. Free to test.