https://vlrggapi.vercel.app/is currently down because it exceeded free-tier limits. Please self-host the API for now, or wait while I look into another solution.
An Unofficial REST API for vlr.gg, a site for Valorant Esports match and news coverage.
Built by Andre Saddler
If this API helps your projects, you can support ongoing maintenance and development on Ko-fi.
- Public base URL:
https://vlrggapi.vercel.app— currently down, see the notice above - Local base URL:
http://127.0.0.1:3001(see Installation) - Interactive docs:
/ - Version info:
/version
curl "http://127.0.0.1:3001/v2/news"
curl "http://127.0.0.1:3001/v2/match?q=live_score"
curl "http://127.0.0.1:3001/v2/player?id=9×pan=all"- V2-first API - consistent response envelopes, validation, and per-endpoint caching
- Backwards compatibility - original unversioned endpoints are still available
- Deep match coverage - detailed match, player, team, and event match endpoints
- Operational guardrails - async HTTP, rate limiting, and bounded expensive scrapes
New endpoints provide deep coverage of individual matches, players, and teams:
- Match details — per-map player stats (K/D/A, ACS, rating), round-by-round data, kill matrix, economy breakdown, head-to-head history, map veto data
- Player profiles — agent stats (17 metrics), current/past teams, event placements, total winnings, match history
- Team profiles — roster with roles/captain status, VLR rating/rank, event placements, total winnings, match history, roster transactions
- Event matches — full match list for any event with scores and VOD links
- Event detail — event info with prize pool breakdown, participating team rosters, and group/stage standings tables
- Search — cross-entity search for teams, players, and events by name or keyword
- Standardized responses — all V2 endpoints return
{"status": "success", "data": {...}} - Input validation — invalid parameters return HTTP 400 with clear error messages
- Per-endpoint caching — in-memory TTL cache reduces load on vlr.gg
- Async HTTP — scrapers use
httpxfor non-blocking I/O
Both the original and V2 endpoints coexist. The original endpoints (/news, /match, etc.) are preserved unchanged for backwards compatibility. V2 endpoints (/v2/news, /v2/match, etc.) are the recommended API going forward.
| Feature | Original | V2 |
|---|---|---|
| Response shape | Varies per endpoint | Consistent {"status": "success", "data": {...}} |
| Input validation | HTTP 400 on most routes (/match and /events are looser) |
HTTP 400 on invalid params |
| Caching | Shared in-memory TTL cache | Per-endpoint TTL cache |
Interactive Swagger docs are available at /.
- Recommended base path - use
/v2for new integrations - Current version endpoint -
GET /versionreturns the current API version and default API - Rate limit - tiered sliding-window limits, applied per client IP.
200/min(/health,/version),60/min(/news,/events,/event/{id},/events/matches,/rankings,/search, and/matchwithq=upcomingorq=live_score),20/min(/match/details,/match/logs,/stats,/player,/team, and/matchwithq=resultsorq=upcoming_extended). The/v2prefix shares each tier with its unversioned route. Exceeding a tier returns HTTP 429 withRetry-After,X-RateLimit-Limit, andX-RateLimit-Remainingheaders. Limits are per worker process, so a multi-worker deployment enforces them independently in each worker. - Error handling - V2 returns HTTP 400 for invalid input and propagates upstream failures with HTTP error codes
- Deployment targets - Vercel for the hosted API, Docker for containerized self-hosting
| Route | Query Params | Cache |
|---|---|---|
GET /v2/news |
— | 10 min |
GET /v2/match |
q (upcoming/upcoming_extended/live_score/results); pagination params num_pages, from_page, to_page apply to upcoming_extended/results only; also max_retries, request_delay, timeout |
30s–5 min |
GET /v2/match/details |
match_id |
5 min |
GET /v2/match/logs |
match_id, game_id (optional, defaults to the first played map) |
5 min |
GET /v2/rankings |
region |
1 hr |
GET /v2/stats |
region, timespan, event_id |
30 min |
GET /v2/events |
q (upcoming/completed/live), page |
30 min |
GET /v2/event/{id} |
event_id (path), stage |
30 min |
GET /v2/events/matches |
event_id |
10 min |
GET /v2/search |
q |
5 min |
GET /v2/player |
id, q (profile/matches), timespan, page |
30 min / 10 min |
GET /v2/team |
id, q (profile/matches/transactions/stats/roster/schedule), page |
30 min / 10 min / 1 hr / 10 min / 30 min / 5 min |
GET /v2/health |
— | none |
See section below for full descriptions and response examples.
All examples are collapsed — click to expand.
Params: none | Cache: 10 min
GET /v2/news
Response
{
"status": "success",
"data": {
"status": 200,
"segments": [
{
"title": "Article title",
"description": "Article summary",
"date": "April 23, 2024",
"author": "author_name",
"url_path": "https://vlr.gg/..."
}
]
}
}Params: q (required: upcoming/upcoming_extended/live_score/results), num_pages, from_page, to_page, max_retries, request_delay, timeout
Cache: 30s (live_score), 5min (upcoming, upcoming_extended), 60s (results)
The pagination params (num_pages, from_page, to_page) apply to upcoming_extended and results only. Passing any of them with q=upcoming or q=live_score returns HTTP 400, since those read a single fixed page. For the paginated queries the page window is capped at 20 pages, max_retries at 3, and timeout at 45 seconds.
GET /v2/match?q=upcoming
Response (upcoming)
{
"status": "success",
"data": {
"status": 200,
"segments": [
{
"team1": "G2 Esports", "team2": "Leviatán",
"flag1": "flag_us", "flag2": "flag_cl",
"time_until_match": "51m from now",
"match_series": "Regular Season: Week 3",
"match_event": "Champions Tour 2024: Americas Stage 1",
"unix_timestamp": "2024-04-24 21:00:00",
"match_page": "https://www.vlr.gg/...",
"match_id": "312784"
}
]
}
}Params: region (required — see Region Codes) | Cache: 1 hr
GET /v2/rankings?region=na
Response
{
"status": "success",
"data": {
"status": 200,
"segments": [
{
"rank": "1", "team": "Sentinels", "country": "United States",
"last_played": "22h ago", "record": "7-3", "earnings": "$295,500",
"logo": "//owcdn.net/img/..."
}
]
}
}Params: region (required), timespan (required: 30/60/90/all), event_id (optional — numeric event ID, filters the stats to a single event) | Cache: 30 min
Regions (the /stats page taxonomy, distinct from /rankings): all, americas, emea, pacific, china, intl. Deprecated aliases are still accepted and normalized: na/br → americas, eu → emea, ap/kr/jp/oce → pacific, cn → china.
GET /v2/stats?region=americas×pan=30
Response
{
"status": "success",
"data": {
"status": 200,
"segments": [
{
"player": "player_name", "org": "ORG",
"rating": "1.18", "average_combat_score": "235.2",
"kill_deaths": "1.19", "kill_assists_survived_traded": "72%",
"average_damage_per_round": "158.4", "kills_per_round": "0.81",
"assists_per_round": "0.29", "first_kills_per_round": "0.19",
"first_deaths_per_round": "0.13", "headshot_percentage": "26%",
"clutch_success_percentage": "28%", "clutch_attempts": "9/57"
}
]
}
}Browse events — use to discover event IDs for detail/matches lookups.
Params: q (optional: upcoming/completed/live), page | Cache: 30 min
GET /v2/events?q=upcoming
GET /v2/events?q=live
Response
{
"status": "success",
"data": {
"status": 200,
"segments": [
{
"title": "VCT 2026: Pacific Stage 2", "event_id": "2776", "status": "completed",
"prize": "$250,000", "dates": "Jul 15—Sep 6", "region": "kr",
"thumb": "https://owcdn.net/img/...",
"url_path": "https://www.vlr.gg/event/2776/vct-2026-pacific-stage-2"
}
]
}
}Params: match_id (required) | Cache: 5 min (30s for live)
GET /v2/match/details?match_id=595657
Response
{
"status": "success",
"data": {
"match_id": "595657",
"event": { "name": "Champions Tour 2024: Americas Stage 1", "series": "Regular Season: Week 3" },
"map_vetos": "SEN ban Breeze; C9 ban Lotus; SEN pick Ascent; C9 pick Bind; Haven remains",
"date": "April 23, 2024", "status": "completed",
"teams": [
{ "id": "2", "name": "Sentinels", "score": 2, "logo": "//owcdn.net/img/..." },
{ "id": "188", "name": "Cloud9", "score": 1, "logo": "//owcdn.net/img/..." }
],
"maps": [{
"map_name": "Ascent", "picked_by": "Sentinels", "duration": "28:41",
"score": { "team1": { "total": 13, "ct": 8, "t": 5 }, "team2": { "total": 9, "ct": 4, "t": 5 } },
"players": { "team1": [{ "name": "TenZ", "agent": "Jett", "rating": "1.32", "acs": "267", "kills": "24", "deaths": "15", "assists": "3", "kd_diff": "+9", "kast": "77%", "adr": "172.3", "hs_pct": "32%", "fk": "4", "fd": "2", "fk_diff": "+2" }], "team2": [] },
"rounds": [{ "round_num": 1, "winner": "team1", "side": "t" }]
}],
"head_to_head": [{ "event": "VCT Americas", "date": "2024/03/15", "score": "2-1", "url": "https://www.vlr.gg/..." }],
"performance": {
"kill_matrix": [{ "player": "TenZ", "kills_vs": { "opponent1": "5" } }],
"advanced_stats": [{ "player": "TenZ", "2K": "3", "3K": "1" }]
},
"economy": [{
"Team": "Sentinels", "Pistol": "1/2", "Conv.": "1/1", "Bonus": "0/1",
"On Eco <10k: vs. Full": "0/2", "On Half buy 10–18k: vs. Full": "0/3",
"On Full buy 18k+: vs. Eco": "2/2", "On Full buy 18k+: vs. Half": "1/1", "On Full buy 18k+: vs. Full": "4/8"
}]
}
}Params: match_id (required), game_id (optional — defaults to the match's first played map) | Cache: 5 min
Per-round 2D replay of one map, from the logs tab on vlr.gg. Each round carries its score, winner, attacking side and end reason, every player's buy, and one event per kill, spike plant and spike defuse — each with the acting player, damage source, spike site, and the positions and health of every player at that moment. Positions are per-event snapshots, not continuous frames.
GET /v2/match/logs?match_id=766311&game_id=285688
Response — real output for Abyss (22 rounds), trimmed to the first 2 rounds, 2 of 10 buy rows, 2 of 8 events and 3 positions per event
{
"status": "success",
"data": {
"match_id": "766311", "game_id": "285688", "map": "abyss",
"teams": {
"team1": { "name": "Akave Esports GC", "tag": "AKV.GC" },
"team2": { "name": "MIBR GC", "tag": "MIBR.GC" }
},
"rounds": [
{
"round": 1, "score": { "team1": 1, "team2": 0 }, "winner": "team1",
"attacking_side": "team1", "end_reason": "Elimination",
"economy": [
{ "player": "WIRE", "team": 1, "agent": "yoru", "credits": 50, "spent": 750, "weapon": "Ghost", "armor": "none" },
{ "player": "Ivy", "team": 1, "agent": "sage", "credits": 100, "spent": 700, "weapon": "Classic", "armor": "none" }
],
"events": [
{
"t": "0:07", "type": "kill", "player": "Tastyy", "team": 1, "victim": "bstrdd", "victim_team": 2,
"pos": { "x": 562.3, "y": 584.8 }, "from": { "x": 530.7, "y": 707.5 },
"positions": [
{ "player": "conir", "team": 2, "x": 967.4, "y": 418.8, "hp": 112 },
{ "player": "waved", "team": 2, "x": 464.8, "y": 528.5, "hp": 150 },
{ "player": "WIRE", "team": 1, "x": 180.8, "y": 642.2, "hp": 198 }
],
"source": "Headhunter"
},
{
"t": "0:17", "type": "kill", "player": "waved", "team": 2, "victim": "Tastyy", "victim_team": 1,
"pos": { "x": 240.7, "y": 751.8 }, "from": { "x": 337.2, "y": 608.7 },
"positions": [
{ "player": "conir", "team": 2, "x": 845.4, "y": 280.8, "hp": 81 },
{ "player": "waved", "team": 2, "x": 337.2, "y": 608.7, "hp": 124 },
{ "player": "WIRE", "team": 1, "x": 38, "y": 361.2, "hp": 8 }
],
"source": "Ghost"
}
]
},
{
"round": 2, "score": { "team1": 2, "team2": 0 }, "winner": "team1",
"attacking_side": "team1", "end_reason": "Elimination",
"economy": [
{ "player": "WIRE", "team": 1, "agent": "yoru", "credits": 100, "spent": 3650, "weapon": "Bulldog", "armor": "heavy" },
{ "player": "Ivy", "team": 1, "agent": "sage", "credits": 1900, "spent": 1500, "weapon": "Sheriff", "armor": "heavy" }
],
"events": [
{
"t": "0:19", "type": "kill", "player": "waved", "team": 2, "victim": "waved", "victim_team": 2,
"pos": { "x": 1149.6, "y": 749.2 },
"positions": [
{ "player": "conir", "team": 2, "x": 1105.6, "y": 659.7, "hp": 4 },
{ "player": "WIRE", "team": 1, "x": 161.7, "y": 582.9, "hp": 220 },
{ "player": "Tura", "team": 1, "x": 712.2, "y": 846, "hp": 300 }
],
"source": "Fall"
}
]
}
]
}
}Reading the payload:
armoris one ofnone,light,heavy,regen.creditsis what the player had left after the buy,spenta positive amount.sourceis vlr.gg's own label for the damage source: usually a weapon, but ability kills appear by ability name (Headhunter) and environmental deaths asFallorSpike.- A death to the environment is reported as a kill where
playerequalsvictimandsourceisFall/Spike— 11 of Abyss's 140 kills here. Do not treatplayer == victimas a parsing bug. positionsholds every player still in play at that event, with their health.posis where the event happened;fromis the shooter's position (kills only).- Coordinates are vlr.gg's per-map replay pixels, not normalised: Abyss runs to
x = 1157.5on a nominally 1024-unit map, so scale from the observed range per map rather than assuming 0–1024. - A map that was never played returns
rounds: []. A 22-round response is roughly 128 KB, dominated bypositions.
Event detail: stages, prizes, team rosters, and standings tables.
Params: event_id (path, required — from /v2/events), stage (optional — a slug from stages) | Cache: 30 min
A multi-stage event page on vlr.gg shows one stage at a time, and its prize rows and standings are that stage's (a group-stage view lists only the placements decided there). Without stage you get vlr.gg's default view, usually the current stage; stages lists every stage with its slug, and stage=<slug> returns that stage's view. An unknown slug returns 404.
GET /v2/event/2124
GET /v2/event/2124?stage=playoffs
Response
{
"status": "success",
"data": {
"segments": {
"event": {
"name": "VCT 2026: Americas Stage 1", "series": "Valorant Champions Tour 2026",
"dates": "Apr 15 - May 10, 2026", "prize": "$250,000 USD",
"location": "Los Angeles, USA", "logo": "https://owcdn.net/img/..."
},
"stages": [
{ "name": "Playoffs", "slug": "playoffs", "dates": "May 2–10", "active": false },
{ "name": "Group Stage", "slug": "group-stage", "dates": "Apr 15–27", "active": true }
],
"prizes": [
{ "placement": "1st", "amount": "$100,000", "team": { "id": "120", "name": "100 Thieves", "logo": "...", "region": "United States" } },
{ "placement": "2nd", "amount": "$60,000", "team": { "id": "2355", "name": "KRÜ Esports", "logo": "...", "region": "Chile" } }
],
"teams": [{
"id": "120", "name": "100 Thieves",
"players": [{ "id": "9", "name": "Asuna", "flag": "mod-us" }],
"qualification": "NA Circuit Points"
}],
"standings": [{
"stage": "Group Stage", "group": "Group A", "columns": ["Team", "W", "L", "T", "MAP", "RND", "Δ"],
"rows": [{ "Team": "100 Thieves", "W": "4", "L": "1", "T": "0", "MAP": "8/3", "RND": "135/102", "Δ": "+33" }]
}]
}
}
}Cross-entity search for players, teams, and events.
Params: q (required) | Cache: 5 min
GET /v2/search?q=tenz
Response
{
"status": "success",
"data": {
"segments": {
"query": "tenz",
"results": {
"players": [{ "id": "9", "name": "TenZ", "img": "https://owcdn.net/img/...", "description": "", "tag": "" }],
"teams": [{ "id": "16647", "name": "TenZ and Friends", "img": "https://...", "description": "", "tag": "(inactive)" }],
"events": []
}
}
}
}Params: id (required), q (profile/matches, default: profile), timespan (30d/60d/90d/all, default: 90d), page (1-based, default: 1) | Cache: varies
GET /v2/player?id=9&q=profile×pan=all
GET /v2/player?id=9&q=matches&page=1
Profile response
{
"status": "success",
"data": {
"info": { "name": "TenZ", "real_name": "Tyson Ngo", "avatar": "https://owcdn.net/img/...", "country": "Canada", "socials": [{ "platform": "twitter", "url": "..." }] },
"current_teams": [{ "name": "Sentinels", "tag": "SEN", "status": "Active" }],
"past_teams": [{ "name": "Cloud9", "tag": "C9", "dates": "2020–2021" }],
"agent_stats": [{ "agent": "Jett", "use_count": 150, "use_pct": "42%", "rounds": 3200, "rating": "1.15", "acs": "245.3", "kd": "1.18", "adr": "162.1", "kast": "71%", "kpr": "0.82", "apr": "0.28", "fkpr": "0.20", "fdpr": "0.14", "kills": 2624, "deaths": 2224, "assists": 896, "fk": 640, "fd": 448 }],
"event_placements": [{ "event": "Champions 2024", "placement": "1st", "prize": "$100,000", "team": "Sentinels" }],
"total_winnings": "$177,650"
}
}Matches response
{
"status": "success",
"data": {
"status": 200,
"segments": [{
"match_id": "719455",
"url": "https://www.vlr.gg/719455/...",
"event": "Twitch Rivals 2026",
"date": "2026/07/24 8:50 pm",
"team1": { "name": "Team TenZ", "tag": "Team TenZ", "logo": "" },
"team2": { "name": "Team s0m", "tag": "Team s0m", "logo": "" },
"score": "2:0",
"result": "win"
}],
"meta": { "page": 1 }
}
}Params: id (required), q (profile/matches/transactions/stats/roster/schedule, default: profile), page (1-based, default: 1) | Cache: 30 min (profile, roster), 10 min (matches, stats), 1 hr (transactions), 5 min (schedule)
GET /v2/team?id=2&q=profile
GET /v2/team?id=2&q=matches&page=1
GET /v2/team?id=2&q=transactions
GET /v2/team?id=2&q=stats
GET /v2/team?id=2&q=roster
GET /v2/team?id=2&q=schedule
Profile response
{
"status": "success",
"data": {
"info": { "name": "Sentinels", "tag": "SEN", "logo": "https://owcdn.net/img/...", "country": "United States", "socials": [{ "platform": "twitter", "url": "..." }] },
"rating": { "vlr_rating": "1850", "rank": "1", "region": "na" },
"roster": [{ "alias": "TenZ", "real_name": "Tyson Ngo", "role": "Duelist", "is_captain": false, "avatar": "..." }],
"event_placements": [{ "event": "Champions 2024", "placement": "1st", "prize": "$100,000" }],
"total_winnings": "$1,194,000"
}
}Matches response
{
"status": "success",
"data": {
"status": 200,
"segments": [{
"match_id": "729757",
"url": "https://www.vlr.gg/729757/sentinels-vs-kr-esports-...",
"event": "LR2",
"date": "2026/08/22",
"time": "7:10 pm",
"team1": { "name": "Sentinels", "tag": "SEN", "logo": "https://owcdn.net/img/..." },
"team2": { "name": "KRÜ Esports", "tag": "KRÜ", "logo": "https://owcdn.net/img/..." },
"score": "0:2",
"result": "loss"
}],
"meta": { "page": 1 }
}
}Unlike the player equivalent, each segment also carries a separate time field; the player date folds both together.
Transactions response
{
"status": "success",
"data": {
"status": 200,
"segments": [{
"date": "Gurjiwan Gill",
"action": "inactive",
"player": { "name": "Jerrwin", "id": "34057", "url": "https://www.vlr.gg/player/34057/gurjiwan-gill", "avatar": "", "country": "in" },
"role": ""
}]
}
}As with the roster's transactions list, player is a nested object and date carries a person's full name rather than a date — see the roster note above.
Stats response
{
"status": "success",
"data": {
"segments": [
{
"map": "Bind",
"games": 86,
"win_pct": "71%",
"wins": 61,
"losses": 25,
"atk_first": 54,
"def_first": 32,
"atk_rwin_pct": "60%",
"atk_rw": 575,
"atk_rl": 383,
"def_rwin_pct": "53%",
"def_rw": 458,
"def_rl": 408
}
]
}
}Roster response
{
"status": "success",
"data": {
"status": 200,
"segments": [{
"active": [{
"id": "1265", "url": "https://www.vlr.gg/player/1265/johnqt",
"alias": "johnqt", "real_name": "Mohamed Amine Ouarid",
"avatar": "https://owcdn.net/img/...", "country": "ma",
"is_captain": true, "role": "", "is_staff": false
}],
"staff": [],
"former": [],
"benched": [],
"transactions": [{
"date": "Gurjiwan Gill", "action": "inactive",
"player": { "name": "Jerrwin", "id": "34057", "url": "https://www.vlr.gg/player/34057/gurjiwan-gill", "avatar": "", "country": "in" },
"role": ""
}]
}]
}
}segments holds a single object with the roster split into active, staff, former, and benched. transactions mirrors q=transactions, but note vlr.gg renders that table's columns as name and source link rather than a date, so date carries a person's full name and role carries a URL on join/leave entries.
Schedule response
{
"status": "success",
"data": {
"status": 200,
"segments": [{
"match_id": "729757",
"url": "https://www.vlr.gg/729757/sentinels-vs-kr-esports-...",
"event": "LR2",
"date": "2026/08/22",
"time": "7:10 pm",
"eta": "",
"team1": { "name": "Sentinels", "tag": "SEN", "logo": "https://owcdn.net/img/..." },
"team2": { "name": "KRÜ Esports", "tag": "KRÜ", "logo": "https://owcdn.net/img/..." }
}]
}
}segments is a flat list of upcoming matches. eta is a countdown string when vlr.gg renders one and "" otherwise.
Params: event_id (required) | Cache: 10 min
GET /v2/events/matches?event_id=2095
Response
{
"status": "success",
"data": {
"status": 200,
"segments": [{
"match_id": "595657",
"url": "https://www.vlr.gg/595657/...",
"date": "Sun, April 24, 2024",
"time": "3:00 PM",
"unix_timestamp": "2024-04-24 19:00:00",
"status": "Completed",
"note": "",
"stage": "Playoffs",
"event_series": "Grand Final",
"team1": { "name": "Sentinels", "score": "2", "is_winner": true },
"team2": { "name": "Cloud9", "score": "1", "is_winner": false }
}]
}
}date and time are as vlr.gg rendered them, in the timezone vlr.gg picked for the requesting server. unix_timestamp is UTC, derived from a minute-precision countdown on the same page; it is "" when the page has none (e.g. an event whose every match is more than a day away or a day old).
Params: none | Cache: none
Reports local service readiness — the app itself and its shared HTTP client. It does not call vlr.gg, so it is safe to use as a container liveness probe.
GET /v2/health
Response
{
"status": "success",
"data": {
"service": { "status": "Healthy" },
"http_client": { "status": "Healthy", "status_code": null }
},
"meta": null,
"message": null
}Preserved for backwards compatibility. Most return {"data": {"status": int, "segments": [...]}}, mirroring their V2 counterparts — see V2 Endpoints for examples. Three differ:
/rankingsreturns{"status": int, "data": [...]}— the segment list is unwrapped./healthreturns the bare{"service": {...}, "http_client": {...}}object with no envelope./match/detailsomits theidfield from every entry inteams.
| Route | Query Params |
|---|---|
GET /news |
— |
GET /match |
q (upcoming/upcoming_extended/live_score/results); pagination params for upcoming_extended/results only |
GET /match/details |
match_id |
GET /match/logs |
match_id, game_id (optional) |
GET /stats |
region, timespan, event_id |
GET /rankings |
region |
GET /events |
q (upcoming/completed/live), page |
GET /events/matches |
event_id |
GET /player |
id, timespan |
GET /player/matches |
id, page |
GET /team |
id |
GET /team/matches |
id, page |
GET /team/transactions |
id |
GET /team/roster |
id |
GET /team/schedule |
id |
GET /health |
— |
/search and /event/{id} exist only under /v2 — there is no unversioned equivalent.
GET /match?q=upcoming — response example
{
"data": {
"status": 200,
"segments": [
{
"team1": "G2 Esports",
"team2": "Leviatán",
"flag1": "flag_us",
"flag2": "flag_cl",
"time_until_match": "51m from now",
"match_series": "Regular Season: Week 3",
"match_event": "Champions Tour 2024: Americas Stage 1",
"unix_timestamp": "2024-04-24 21:00:00",
"match_page": "https://www.vlr.gg/..."
}
]
}
}GET /match?q=live_score — response example
{
"data": {
"status": 200,
"segments": [
{
"team1": "Team 1",
"team2": "Team 2",
"flag1": "flag_xx",
"flag2": "flag_xx",
"team1_logo": "https://...",
"team2_logo": "https://...",
"score1": "1",
"score2": "0",
"team1_round_ct": "7",
"team1_round_t": "6",
"team2_round_ct": "5",
"team2_round_t": "4",
"map_number": "1",
"current_map": "Ascent",
"time_until_match": "LIVE",
"match_event": "Event name",
"match_series": "Series name",
"unix_timestamp": "1713996000",
"match_page": "https://www.vlr.gg/..."
}
]
}
}GET /match?q=results — response example
{
"data": {
"status": 200,
"segments": [
{
"team1": "Team Vitality",
"team2": "Gentle Mates",
"score1": "0",
"score2": "2",
"flag1": "flag_eu",
"flag2": "flag_fr",
"time_completed": "2h 44m ago",
"round_info": "Regular Season-Week 4",
"tournament_name": "Champions Tour 2024: EMEA Stage 1",
"match_page": "/318931/team-vitality-vs-gentle-mates-...",
"tournament_icon": "https://owcdn.net/img/..."
}
]
}
}GET /stats?region=americas×pan=30 — response example
{
"data": {
"status": 200,
"segments": [
{
"player": "corey",
"org": "TTR",
"rating": "1.18",
"average_combat_score": "235.2",
"kill_deaths": "1.19",
"kill_assists_survived_traded": "72%",
"average_damage_per_round": "158.4",
"kills_per_round": "0.81",
"assists_per_round": "0.29",
"first_kills_per_round": "0.19",
"first_deaths_per_round": "0.13",
"headshot_percentage": "26%",
"clutch_success_percentage": "28%",
"clutch_attempts": "9/57"
}
]
}
}GET /rankings?region=na — response example
Note: /rankings uses a different response shape than other endpoints.
{
"status": 200,
"data": [
{
"rank": "1",
"team": "Sentinels",
"country": "United States",
"last_played": "22h ago",
"last_played_team": "vs. Evil Geniuses",
"last_played_team_logo": "//owcdn.net/img/...",
"record": "7-3",
"earnings": "$295,500",
"logo": "//owcdn.net/img/..."
}
]
}GET /events?q=upcoming — response example
{
"data": {
"status": 200,
"segments": [
{
"title": "VCT 2025: Pacific Stage 2",
"status": "ongoing",
"prize": "$250,000",
"dates": "Jul 15—Aug 31",
"region": "kr",
"thumb": "https://owcdn.net/img/...",
"url_path": "https://www.vlr.gg/event/..."
}
]
}
}GET /health — response example
{
"service": {
"status": "Healthy"
},
"http_client": {
"status": "Healthy",
"status_code": null
}
}| Code | Region |
|---|---|
na |
North America |
eu |
Europe |
ap |
Asia Pacific |
la |
Latin America |
la-s |
Latin America South |
la-n |
Latin America North |
oce |
Oceania |
kr |
Korea |
mn |
MENA |
gc |
Game Changers |
br |
Brazil |
cn |
China |
jp |
Japan |
col |
Collegiate |
V2 endpoints validate input and return HTTP 400 with a detail message:
- Invalid region —
/rankingsaccepts the 14 codes listed above./statsuses its own taxonomy (all,americas,emea,pacific,china,intl) and normalizes the deprecated aliases instead of rejecting them - Invalid timespan — must be
30,60,90, orall - Invalid player timespan — must be
30d,60d,90d, orall - Invalid match query — must be
upcoming,upcoming_extended,live_score, orresults - Invalid event query — must be
upcoming,completed, orlive - Invalid player query — must be
profileormatches - Invalid team query — must be
profile,matches,transactions,stats,roster, orschedule - Invalid stage — must be a vlr.gg slug such as
group-stage(lowercase letters, digits, and hyphens) - Invalid ID —
match_id,event_id, playerid, and teamidmust be numeric - Unsupported pagination —
num_pages,from_page, andto_pageare rejected forq=upcomingandq=live_score - Excessive workload — for the paginated match queries the page window is capped at 20 pages,
max_retriesat 3, andtimeoutat 45 seconds
{
"detail": "Invalid region 'xyz'. Valid regions: ap, br, cn, col, eu, gc, jp, kr, la, la-n, la-s, mn, na, oce"
}/stats reports its own vocabulary in the same shape:
{
"detail": "Invalid region 'xyz'. Valid regions: all, americas, china, emea, intl, pacific"
}The unversioned endpoints are not uniformly unvalidated — most (/rankings, /stats, /player, /player/matches, /team, /team/matches, /team/transactions, /team/roster, /team/schedule, /match/details, /events/matches) reuse these same validators and return the same HTTP 400. Two are deliberately looser for backwards compatibility: /match answers an unknown q with HTTP 200 and {"error": "Invalid query parameter"}, and /events ignores an unknown q and returns the unfiltered event list.
V2 endpoints use an in-memory TTL cache to reduce load on vlr.gg. Cache durations per endpoint:
| Data | TTL | |---|---|---| | Live scores | 30 seconds | | Match detail (live) | 30 seconds | | Results | 60 seconds | | Upcoming matches | 5 minutes | | Match detail | 5 minutes | | Search | 5 minutes | | Team schedule | 5 minutes | | News | 10 minutes | | Player matches | 10 minutes | | Team matches | 10 minutes | | Team stats | 10 minutes | | Event matches | 10 minutes | | Stats | 30 minutes | | Events | 30 minutes | | Event detail | 30 minutes | | Player profile | 30 minutes | | Team profile | 30 minutes | | Team roster | 30 minutes | | Rankings | 1 hour | | Team transactions | 1 hour |
Original endpoints share the same in-memory TTL cache as their V2 counterparts, so the same scrape is never repeated across the two versions. /health is the exception: it is never cached, since it only reports in-process state.
- Python
3.11locally (from.python-version). CI tests3.11–3.14, and the Docker image runs Python3.14 uv
git clone https://github.com/axsddlr/vlrggapi/
cd vlrggapi
uv syncuv sync creates .venv and installs the exact versions pinned in uv.lock,
including the dev dependency group. Prefix commands with uv run to run them
inside that environment — there is nothing to activate by hand.
uv run python main.pyThe API will be available at http://127.0.0.1:3001 and the interactive docs will be at http://127.0.0.1:3001/.
curl http://127.0.0.1:3001/version
curl "http://127.0.0.1:3001/v2/health"docker compose up --builduv run pytest tests/ -vRate limiting is implemented in-house (api/utils/rate_limiter.py) with no additional dependency.
Issues and pull requests are welcome.
master is the only long-lived branch, and it is always deployed. Work on a
short-lived branch and open a pull request against master:
git switch master && git pull --ff-onlygit switch -c fix/your-slug- Run
uv syncand verify the app starts locally. - Run
uv run pytest -qanduv run ruff check .. - Rebase onto
origin/master, push, and open a pull request againstmaster.
Pull requests are squash-merged, so master gets one readable commit per change.
Do not push work in progress to master — use a draft PR if you want CI feedback
early. Branch protection, commit hygiene, and how to revert a bad change are
documented in WORKFLOW.md.
Open a pull request or file an issue.
The MIT License (MIT)