Self-hostable • Snapshot-backed • Authenticated

Gaming Stats API — Valorant

Version: 3.8.3

A reusable API for tracked Riot IDs that refreshes stats through Apify, enriches profile data through HenrikDev and Valorant API, stores snapshots on disk, and serves clean cached responses to your app, site, or dashboard.

This project is built for people who want stable stats responses, predictable hosting, and a simple setup without adding a database on day one.

Tracked players

  • Spider31415#6921

Only configured Riot IDs are available. Unknown users return 404 User not tracked.

Authentication

All http://api.aniketraj.me/custom/valorant/stats routes require X-API-Key.

X-API-Key: your-api-key

API keys are mandatory by design so accidental public deployments do not fail open.

Refresh model

Snapshots refresh every 48h by default.

Built-in scheduler: enabled

Tracker refresh command: npm run refresh:snapshots

Henrik profile/rank refresh command: npm run refresh:profiles

Henrik refreshes are separate so you can update account/card/title/rank data without rerunning the slower tracker.gg scrape.

Request Shape

Endpoint: POST /stats/:username
Sample path: POST /stats/Spider31415%236921

{
  "playlist": "competitive",
  "modules": {
    "profile": {},
    "rank": {},
    "agents": {},
    "maps": { "playlist": "unrated", "limit": 3 },
    "totalPlaytime": {}
  }
}
Rules: top-level playlist must be competitive or unrated. modules is required. Each module config must be an object. profile is not playlist-scoped, rank is Henrik-backed and returned from the competitive branch for compatibility, and totalPlaytime always comes from shared data.

Examples

Use these as copy-paste starters. Every example below is shown in both cURL and JavaScript.

Player profile

Profile returns account level, region, player card assets, and title display text.

curl -X POST 'http://api.aniketraj.me/custom/valorant/stats/Spider31415%236921' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: your-api-key' \
  -d '{
    "modules": {
      "profile": {}
    }
  }'
const response = await fetch('http://api.aniketraj.me/custom/valorant/stats/Spider31415%236921', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'your-api-key'
  },
  body: JSON.stringify({
    modules: {
      profile: {}
    }
  })
});

const data = await response.json();

Basic request: competitive agents

Use the top-level playlist when all requested modules come from the same playlist.

curl -X POST 'http://api.aniketraj.me/custom/valorant/stats/Spider31415%236921' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: your-api-key' \
  -d '{
    "playlist": "competitive",
    "modules": {
      "agents": {}
    }
  }'
const response = await fetch('http://api.aniketraj.me/custom/valorant/stats/Spider31415%236921', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'your-api-key'
  },
  body: JSON.stringify({
    playlist: 'competitive',
    modules: {
      agents: {}
    }
  })
});

const data = await response.json();

Multiple modules in one request

You can request profile, rank, agents, maps, and shared total playtime together.

curl -X POST 'http://api.aniketraj.me/custom/valorant/stats/Spider31415%236921' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: your-api-key' \
  -d '{
    "playlist": "competitive",
    "modules": {
      "profile": {},
      "rank": {},
      "agents": {},
      "maps": {},
      "totalPlaytime": {}
    }
  }'
const response = await fetch('http://api.aniketraj.me/custom/valorant/stats/Spider31415%236921', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'your-api-key'
  },
  body: JSON.stringify({
    playlist: 'competitive',
    modules: {
      profile: {},
      rank: {},
      agents: {},
      maps: {},
      totalPlaytime: {}
    }
  })
});

const data = await response.json();

Mixed playlists in one request

Override the top-level playlist per module when you want to mix competitive and unrated data.

curl -X POST 'http://api.aniketraj.me/custom/valorant/stats/Spider31415%236921' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: your-api-key' \
  -d '{
    "playlist": "competitive",
    "modules": {
      "agents": { "playlist": "unrated" },
      "maps": { "playlist": "competitive" },
      "totalPlaytime": {}
    }
  }'
const response = await fetch('http://api.aniketraj.me/custom/valorant/stats/Spider31415%236921', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'your-api-key'
  },
  body: JSON.stringify({
    playlist: 'competitive',
    modules: {
      agents: { playlist: 'unrated' },
      maps: { playlist: 'competitive' },
      totalPlaytime: {}
    }
  })
});

const data = await response.json();

Trim array results with limit

Limit only affects array modules such as agents and maps.

curl -X POST 'http://api.aniketraj.me/custom/valorant/stats/Spider31415%236921' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: your-api-key' \
  -d '{
    "playlist": "competitive",
    "modules": {
      "agents": { "limit": 3 },
      "maps": { "limit": 5 }
    }
  }'
const response = await fetch('http://api.aniketraj.me/custom/valorant/stats/Spider31415%236921', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'your-api-key'
  },
  body: JSON.stringify({
    playlist: 'competitive',
    modules: {
      agents: { limit: 3 },
      maps: { limit: 5 }
    }
  })
});

const data = await response.json();

Use unrated as the default playlist

This keeps the payload shorter when most modules should use unrated data.

curl -X POST 'http://api.aniketraj.me/custom/valorant/stats/Spider31415%236921' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: your-api-key' \
  -d '{
    "playlist": "unrated",
    "modules": {
      "agents": {},
      "maps": {},
      "rank": {},
      "totalPlaytime": {}
    }
  }'
const response = await fetch('http://api.aniketraj.me/custom/valorant/stats/Spider31415%236921', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'your-api-key'
  },
  body: JSON.stringify({
    playlist: 'unrated',
    modules: {
      agents: {},
      maps: {},
      rank: {},
      totalPlaytime: {}
    }
  })
});

const data = await response.json();

Response Shape

{
  "username": "Spider31415#6921",
  "playlist": "competitive",
  "cachedAt": "2026-05-19T10:00:00.000Z",
  "nextRefreshAt": "2026-05-21T10:00:00.000Z",
  "status": "ok",
  "sources": {
    "henrik": {
      "status": "ok",
      "lastRefreshedAt": "2026-05-20T12:15:00.000Z"
    }
  },
  "data": {
    "profile": {
      "accountLevel": 514,
      "region": "ap",
      "card": {
        "id": "67d7faa4-4b40-e16a-1c54-0f9b41919849",
        "name": "VCT x SEN Card",
        "displayIcon": "https://...",
        "smallArt": "https://...",
        "wideArt": "https://...",
        "largeArt": "https://..."
      },
      "title": {
        "id": "ae54c1ce-42b9-3dc1-5e91-6c9e9161b01a",
        "name": "Gnarly Title",
        "displayText": "Gnarly"
      }
    },
    "agents": [
      {
        "agent": "Omen",
        "role": "Controller",
        "timePlayed": "58 hrs",
        "matches": "98",
        "winRate": "53.1%",
        "kd": "1.05",
        "adr": "143.3",
        "acs": "222.3",
        "ddDelta": "+8",
        "hsPercent": "17.8%",
        "kast": "71.7%",
        "icon": "https://...",
        "displayIconSmall": "https://...",
        "portrait": "https://...",
        "portraitV2": "https://...",
        "bustPortrait": "https://...",
        "killfeedPortrait": "https://...",
        "minimapPortrait": "https://...",
        "background": "https://...",
        "homeScreenPromoTileImage": "https://...",
        "roleIcon": "https://...",
        "abilityIcons": [{ "slot": "Ability1", "name": "Ability", "icon": "https://..." }]
      }
    ]
  }
}

Error Behavior

StatusWhen it happens
400Malformed username encoding, invalid playlist, invalid module name, invalid module config, or invalid limit
401Missing or invalid API key
404User not tracked, snapshot missing, or requested cached module unavailable

Profile Visibility Requirement

For this API to track a player, that player’s tracker.gg profile needs to be public. If the profile stays private, the refresh job will not be able to collect the data your API serves.

Based on current Tracker Network support guidance, the most reliable flow is:

  1. Open an incognito/private browser window.
  2. Sign in to the correct Riot account, and if needed the correct Tracker Network account.
  3. Open your own Valorant profile page on tracker.gg.
  4. Check the box that says I acknowledge signing in makes my profile public to all users.
  5. Click Sign in with Riot.
  6. Enter credentials manually instead of relying on browser auto sign-in.
  7. Finish the sign-in flow and return to your now-public profile page.
Common gotchas: multiple Riot accounts in one browser session, browser auto sign-in choosing the wrong account, and assuming that signing into your own account will reveal someone else’s private profile.

Operational Notes

Contact

If you are using this project, forking it, or want to contribute back, feel free to reach out.