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
{ "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 serverThe error
HTTP/1.1 403 Forbidden
Server: cloudflare
cf-mitigated: challenge
<title>Just a moment...</title>
Enable JavaScript and cookies to continueWhat 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 see | What it means |
|---|---|
| HTTP 403 Forbidden | The request was stopped at Cloudflare, not served by HLTV. |
| Response header cf-mitigated: challenge | Cloudflare 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 access | The hltv npm package's error when it receives the challenge page. |
| CloudflareChallengeError: Detected a Cloudflare version 2 challenge | Python 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.
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.textexport 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()
}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 job | Endpoint |
|---|---|
| Results page | GET /cs2/matches/results |
| Upcoming matches | GET /cs2/matches/upcoming |
| Live scores | GET /cs2/matches/live (or the /cs2/live/stream SSE stream) |
| Player stats page | GET /cs2/players/{slug}/stats and /cs2/players/{slug}/matches |
| Top players list | GET /cs2/leaderboards/rating |
| Team ranking | GET /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.
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.
- GET
/api/v1/cs2/liveMatches in progress: series score, map in play, map score, current round.
- GET
/api/v1/cs2/live/{matchId}/stateOne live match: score, round, round timer, bomb status and players.
- GET
/api/v1/cs2/live/{matchId}/scoreboardSeries score, current map and round, and each player's kills, deaths, money and equipment.
- GET
/api/v1/cs2/live/{matchId}/roundsRound-by-round results so far: winner, side and how each round ended.
- GET
/api/v1/cs2/live/streamServer-Sent Events push of every live update. Every plan; counts like polling.
- GET
/api/v1/cs2/live/wsWebSocket with a room per match. Scale and Enterprise.
Results
Finished matches with the series score and winner, every map and its score, and the player lines behind them.
- GET
/api/v1/cs2/matches/recentFinished 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}/mapsEach map: name, score, winner, and the team that picked it.
- GET
/api/v1/cs2/matches/{matchId}/player-statsPer-player stats for each map: kills, deaths, ADR, KAST and rating.
- GET
/api/v1/cs2/teams/{teamIdOrSlug}/resultsOne team's finished matches, newest first.
- GET
/api/v1/cs2/events/{eventIdOrSlug}/resultsFinal placements and prize money of a tournament.
Schedule
What is on next: upcoming matches soonest first, today's slate, one team's fixtures, and the tournament calendar.
- GET
/api/v1/cs2/matches/upcomingScheduled matches, soonest first.
- GET
/api/v1/cs2/matches/todayMatches scheduled, live or finished today (UTC).
- GET
/api/v1/cs2/teams/{teamIdOrSlug}/upcomingOne team's scheduled matches.
- GET
/api/v1/cs2/events/upcomingScheduled tournaments, soonest first.
- GET
/api/v1/cs2/events/calendarTournaments in a date range, with from and to.
- GET
/api/v1/cs2/matches/{matchId}/streamsBroadcast links for a match, when known.
Player stats
Rating, ADR, KAST and K/D for every map a player has played, rolled up into career, yearly and recent-form views.
- GET
/api/v1/cs2/players/{playerIdOrSlug}Nickname, real name, photo, country, team, role and headline stats.
- GET
/api/v1/cs2/players/{playerIdOrSlug}/statsCareer totals plus a stat line for every map played.
- GET
/api/v1/cs2/players/{playerIdOrSlug}/seasonsA player's stats by year.
- GET
/api/v1/cs2/players/{playerIdOrSlug}/formThe most recent maps with kills, deaths, ADR, KAST and rating.
- GET
/api/v1/cs2/leaderboards/{metric}Top players by rating, ADR, KAST, K/D, kills or opening kills.
- GET
/api/v1/cs2/players/search?q={name}Find a player by nickname or real name.
Teams & rosters
Team profiles with the current roster, every past lineup, player moves between teams, and how each team plays each map.
- GET
/api/v1/cs2/teams/{teamIdOrSlug}Name, logo, country, region, world ranking and current roster.
- GET
/api/v1/cs2/teams/{teamIdOrSlug}/roster-historyEvery player who has played for the team, with start and end dates.
- GET
/api/v1/cs2/teams/{teamIdOrSlug}/map-statsPer-map win rate, rounds and pick/ban tendencies.
- GET
/api/v1/cs2/teams/{teamIdOrSlug}/vs/{opponentIdOrSlug}Record and past matches between two teams.
- GET
/api/v1/cs2/roster-changes/recentPlayers joining and leaving teams, newest first.
- GET
/api/v1/cs2/transfers/recentPlayer moves between teams, newest first.
Rankings
The world team ranking and the Valve Regional Standings, with points and movement, plus one table per region.
- GET
/api/v1/cs2/rankings/worldThe latest world team ranking.
- GET
/api/v1/cs2/rankings/vrsThe latest Valve Regional Standings (VRS).
- GET
/api/v1/cs2/rankings/region/{region}One region, with each team's world and regional rank.
- GET
/api/v1/cs2/rankingsOne row per team, with points and change since the previous ranking.
Skin prices
The current price of a skin on each marketplace, its daily price history, sell listings across markets, and the week's biggest movers.
- GET
/api/v1/cs2/skins?q={name}Search skins by name, weapon, rarity or price range.
- GET
/api/v1/cs2/skins/{skinIdOrSlug}/pricesThe current price on each marketplace, cheapest first, with offer counts.
- GET
/api/v1/cs2/skins/{skinIdOrSlug}/historyDaily cheapest price across marketplaces, years back on long-listed skins.
- GET
/api/v1/cs2/skins/{skinIdOrSlug}/order-bookSell listings per marketplace with counts, and the lowest ask.
- GET
/api/v1/cs2/skins/trendingBiggest gainers and losers over 1 day to 1 year, with sparklines.
- GET
/api/v1/cs2/skins/{skinIdOrSlug}One skin: weapon, rarity, collection, image and price summary.
Tournaments
Tournaments with dates, prize pool, tier and format, plus every match, the standings and the bracket.
- GET
/api/v1/cs2/events/liveTournaments being played now.
- GET
/api/v1/cs2/events/{eventIdOrSlug}Dates, location, prize pool, tier, teams and format.
- GET
/api/v1/cs2/events/{eventIdOrSlug}/matchesEvery match of the tournament.
- GET
/api/v1/cs2/events/{eventIdOrSlug}/standingsTeams with seed, final placement and prize.
- GET
/api/v1/cs2/events/{eventIdOrSlug}/bracketBracket, groups, play-in and Swiss, each slot with teams, scores and winner.
FAQs
HLTV 403, Cloudflare and scraping
Can't find what you're looking for? Contact our customer support team
Replace the scraper
- HLTV APIIs there an HLTV API? No official one. What to use instead.
- HLTV API alternativeEvery way to get HLTV-style CS2 data, compared.
- HLTV API in PythonResults, player stats, rankings and live scores in Python.
- HLTV API in JavaScriptHLTV-style CS2 data in Node.js and TypeScript.
- CS2 results APISeries and map scores for every finished match.
- CS2 player stats APIRating, ADR, and KAST for every map played.
- Free CS2 APIWhat a free key includes, with every limit stated.
- CS2 API docsEvery endpoint, its parameters, and a real response.