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,/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/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"
}]
}
}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 /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.13, 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.
Recommended workflow:
- Branch from
dev, the active integration branch (masteris the default branch). - Run
uv syncand verify the app starts locally. - Run
uv run pytest tests/ -v. - Open a pull request against
dev.
Open a pull request or file an issue.
The MIT License (MIT)