Token API
The endpoints the Krunker client reads with the player's own access token
These are the reads the game makes with the player's FRVR access token: the wallet, inventory, trades, custom skins, the live market listings, and the ranked system's own API on api.krunker.io. Nothing here is documented by Krunker. Every endpoint below was taken from the game's client code and confirmed once with a real token in October 2026, read only. All of it can change without notice.
In krapio these live under krunker.as(token).*. The token is sent as a bearer header and nothing else: never stored, never logged, never used for a write. Every write the game makes with it (purchases, listings, trades, follows, posts, studio and map management) is left out on purpose.
The token
The game gets it from FRVR.auth.getFreshAccessToken(): an RS256 JWT with the claims sub (the player's FRVR user id), iss, iat, exp and extra, valid for about 24 hours.
krapio decodes the claims without verifying the signature (Krunker does that) so a malformed or expired token throws KrunkerTokenError before any request, and exposes them as session.claims. claims.sub is the only stable key to cache anything read here under, never the token itself.
const me = krunker.as(token);
me.claims.sub; // "frvr user id"
await me.player.wallet();Hosts, envelopes and errors
| Host | Used by | Reply shape |
|---|---|---|
https://gapi.svc.krunker.io | everything unless noted | {"data": ..., "error": null}, unwrapped by krapio |
https://api.krunker.io | ranked.* | {"data": ...} without an error field on success, unwrapped by krapio |
| Status | When |
|---|---|
401 invalid token | bad signature or expired; Access token required on api.krunker.io without a header |
403 insufficient permissions, not authorized | a role gated endpoint (see Staff) |
Some replies carry their refusal inside data with a 200, see the wild offer.
The rate limit headers are the same as for the game API: 5 requests per second per endpoint on gapi.svc.krunker.io, none on api.krunker.io.
Player
The token's own account.
Me
me.player.me()
{player: {id, name, data}}, where data is the packed account blob the game decodes itself.
Response Fields:
| Field | Type | Description |
|---|---|---|
player | object | |
player.id | string | |
player.name | string | |
player.data | unknown[] |
Wallet
me.player.wallet()
Kr plus every studio currency.
Response Fields:
| Field | Type | Description |
|---|---|---|
currencies | object[] | |
currencies[].id | number | |
currencies[].name | string | |
currencies[].symbol | string | |
currencies[].amount | number | |
spending_tier | number |
Guest Profile
me.player.guestProfile()
The stats of a guest token: {profile: {score, funds, kills, deaths, games, wins}}, zeros for a registered account.
Response Fields:
| Field | Type | Description |
|---|---|---|
profile | object | |
profile.score | number | |
profile.funds | number | |
profile.kills | number | |
profile.deaths | number | |
profile.games | number | |
profile.wins | number |
Usernames
me.player.usernames()
The active name, the reserved names and the rename cooldown.
Response Fields:
| Field | Type | Description |
|---|---|---|
active | string | |
names | object[] | |
names[].name | string | |
names[].state | string | |
names[].leave_release_secs | number | |
cooldown_secs | number |
Email Preferences
me.player.emailPreferences()
{chosen, promotional}.
Response Fields:
| Field | Type | Description |
|---|---|---|
chosen | boolean | |
promotional | boolean |
Discord Status
me.player.discordStatus()
{configured, linked, id?, username?, globalName?, avatar?, linkedAt?, guildUrl?, inGuild?}, the optional fields only while linked.
Response Fields:
| Field | Type | Description |
|---|---|---|
configured | boolean | |
linked | boolean | |
id | string | |
username | string | |
globalName | string | |
avatar | string | |
linkedAt | number | |
guildUrl | string | |
inGuild | boolean |
Market
The market behind a login. Market stats and item pages answer without a token, see the game API.
Listings
me.market.listings({ item_id, items, sort, cursor_id, limit, rarities, weapons, types, wears, season, painted, searchTerm })
What the market tab shows. A token is required although the data is not about the player.
| Name | Type | Description |
|---|---|---|
| item_id | int | one skin index |
| items | int list | several skin indices |
| sort | string | latest, price_asc, price_desc or popular |
| cursor_id | int | the last uid of the previous page |
| limit | int | 1 to 100 |
| rarities, weapons, types, wears | int list | comma separated filters |
| season | int | |
| painted | bool | sent as 1 when true |
| searchTerm | string |
100 listings per page. Opening the market tab loads sort=latest; browsing by price walks sort=price_asc page by page through next_cursor_id (13 pages in 4 seconds in one recording), an item page loads item_id=&sort=price_asc.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
item_id | integer | query | no | |
items | integer[] | query | no | |
sort | "latest" | "price_asc" | "price_desc" | "popular" | query | no | |
cursor_id | integer | query | no | |
limit | integer | query | no | |
rarities | integer[] | query | no | |
weapons | integer[] | query | no | |
types | integer[] | query | no | |
wears | integer[] | query | no | |
season | integer | query | no | |
painted | 1 | query | no | sent as 1 when `painted` is set |
searchTerm | string | query | no |
Response Fields:
| Field | Type | Description |
|---|---|---|
listings | object[] | |
listings[].uid | number | |
listings[].marketid | number | |
listings[].skinid | number | the item instance |
listings[].skinindex | number | |
listings[].funds | number | asking price in kr |
listings[].seller_id | number | |
listings[].seller_name | string | |
listings[].listed_time | number | unix ms |
listings[].listing_count | number | how many of this skin the seller has listed |
has_more | boolean | |
next_cursor_id | number or null | the cursor_id of the next page |
Inventory
me.market.inventory()
The player's stacks: {i, cnt, sid, st}[] (skin index, count, instance id, state).
Response Fields:
| Field | Type | Description |
|---|---|---|
i | number | |
cnt | number | |
sid | number | |
st | number |
Sales
me.market.sales()
The player's own open listings, [] when there are none.
Response Fields:
| Field | Type | Description |
|---|---|---|
uid | number | |
marketid | number | |
skinid | number | the item instance |
skinindex | number | |
funds | number | asking price in kr |
listed_time | number | unix ms |
History
me.market.history({ sort, cursor, limit })
The player's completed buys and sells, newest first by default. sort is one of date_asc, date_desc, amount_asc, amount_desc, item_asc, item_desc; limit 1 to 100.
date is when the sale went through, a few seconds after the buyer's POST /market/purchase returned in one recording.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
sort | "date_asc" | "date_desc" | "amount_asc" | "amount_desc" | "item_asc" | "item_desc" | query | no | |
cursor | integer | query | no | |
limit | integer | query | no |
Response Fields:
| Field | Type | Description |
|---|---|---|
history | object[] | |
history[].marketid | number | |
history[].type | string | |
history[].date | string | |
history[].skinindex | number | |
history[].amount | number | |
history[].other_party_id | number | |
history[].other_party_name | string | |
history[].status | string | |
has_more | boolean | |
next_cursor | number or null | the cursor of the next page |
Black Market
me.market.blackMarket(), me.market.blackMarketRank()
The skin indices on offer, and {peak_mmr} (null without a ranked history), the player's peak MMR for the ranked tier of the black market.
GET /market/black-market/rank
Response Fields:
| Field | Type | Description |
|---|---|---|
peak_mmr | number or null |
Trades
Player to player trades. Creating, accepting and cancelling are writes and are not in krapio.
History
me.trades.history({ offset, limit, includeOpen })
Open and completed trades of the player, limit 1 to 100. krapio sends include_open=0 only when includeOpen is false.
The sides follow the offer, not the reader: in the recipient's history pl and player are still the other player. cid 1 is kr. A completed trade keeps the dte of its offer, so an open trade and the same trade completed are identical apart from the list they are in. The trading window loads this with include_open on, and again with include_open=0 about a second after an accept.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
offset | integer | query | no | |
limit | integer | query | no | |
include_open | 0 | query | no | sent as 0 when `includeOpen` is set |
Response Fields:
| Field | Type | Description |
|---|---|---|
open | object[] | |
open[].trid | number | |
open[].sksell | object[] | items offered |
open[].sksell[].ind | number | |
open[].skbuy | object[] | items asked for |
open[].skbuy[].ind | number | |
open[].csell | object[] | |
open[].csell[].cid | number | |
open[].csell[].amt | number | |
open[].cbuy | object[] | |
open[].cbuy[].cid | number | |
open[].cbuy[].amt | number | |
open[].pl | number | id of the player who made the offer, their items are sksell |
open[].buy | number | id of the player the offer went to, their items are skbuy |
open[].buyer | string | name of the player the offer went to |
open[].player | string | name of the player who made the offer |
open[].message | string | |
open[].dte | string | when the offer was made, "YYYY-MM-DD HH:MM:SS" utc; a completed trade keeps it |
completed | object[] | |
completed[].trid | number | |
completed[].sksell | object[] | items offered |
completed[].sksell[].ind | number | |
completed[].skbuy | object[] | items asked for |
completed[].skbuy[].ind | number | |
completed[].csell | object[] | |
completed[].csell[].cid | number | |
completed[].csell[].amt | number | |
completed[].cbuy | object[] | |
completed[].cbuy[].cid | number | |
completed[].cbuy[].amt | number | |
completed[].pl | number | id of the player who made the offer, their items are sksell |
completed[].buy | number | id of the player the offer went to, their items are skbuy |
completed[].buyer | string | name of the player the offer went to |
completed[].player | string | name of the player who made the offer |
completed[].message | string | |
completed[].dte | string | when the offer was made, "YYYY-MM-DD HH:MM:SS" utc; a completed trade keeps it |
completed_has_more | boolean |
Trade Inventories
me.trades.inventory(playerId)
Both inventories side by side: {my_inventory, their_inventory}, each {i, cnt, sid, st}[].
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
playerId | integer | path | yes |
Response Fields:
| Field | Type | Description |
|---|---|---|
my_inventory | object[] | |
my_inventory[].i | number | |
my_inventory[].cnt | number | |
my_inventory[].sid | number | |
my_inventory[].st | number | |
their_inventory | object[] | |
their_inventory[].i | number | |
their_inventory[].cnt | number | |
their_inventory[].sid | number | |
their_inventory[].st | number |
Player Search
me.trades.playerSearch(playerName)
One player by exact name: {player_id, player_name, player_score}.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
name | string | query | yes |
Response Fields:
| Field | Type | Description |
|---|---|---|
player_id | number | |
player_name | string | |
player_score | number |
Market and Trade Writes
What the game posts when the player buys, lists or trades. krapio never sends any of these: they are here so a client that only reads what the game already did (a DevTools observer, a proxy) knows the shapes. Recorded once each in October 2026. The reply types are exported anyway (MarketListResult, MarketPurchaseResult, ...).
| Request | Body | Reply data | The game then loads |
|---|---|---|---|
POST /market/list | {item_id, price, skin_id} (skin index, kr, instance id) | {funds, cnt, marketid}: kr left, stack left of that skin, the new listing | /market/inventory, /market/sales |
POST /market/unlist | {market_id} | {response: "SUCCESS"} | /market/inventory, /market/sales |
POST /market/purchase | {market_id} | {playerfunds, sid, mid, skn}: kr left, the bought instance, the listing's market id and skin index | the listings again, not the history |
POST /trades/create | {target_player_id, my_offer: {i, c}[], their_offer: {i, c}[], message} | {funds} | /trades/history |
POST /trades/:id/accept | none | {funds} | /trades/history?include_open=0 about a second later, with the trade under completed |
POST /trades/:id/cancel | none | {success: true} | nothing right away |
POST /trades/decline-all | none | not recorded |
The purchase reply has no price: it is the funds of that marketid in the listings the game loaded before, or amount in the buyer's market history later. Declining one incoming offer was not recorded; the game code has only /cancel and /decline-all for it.
Refusals come as 400 with {error: string} and no data. Seen: "Try again in 60 seconds" (two accepts of an offer made 9 s and 89 s before, the third after 2.5 minutes went through), "Market is busy. Please try again in a few seconds." (a second list 5 s after the first), "Try again in a few seconds" (an unlist 23 s after listing).
Shop
Per player shop state and the player's own custom skins.
Ranked Shop
me.shop.ranked()
The skins bought with ranked currency, with the player's purchase counts: {skins: {id, skin_id, price, max_purchase, purchase_count}[]}.
Response Fields:
| Field | Type | Description |
|---|---|---|
skins | object[] | |
skins[].id | number | |
skins[].skin_id | number | |
skins[].price | number | |
skins[].max_purchase | number | |
skins[].purchase_count | number |
Wild Offer
me.shop.wildOffer()
A 200 with {error: "Wild offer is not available"} inside data when there is none. krapio returns that object as is, check for an error key.
Response Fields:
| Field | Type | Description |
|---|---|---|
error | string |
Wristband
me.shop.wristband()
The player's KrunkCup wristband skin and whether it was claimed: {skin_index, claimed, style, instance_id}.
Response Fields:
| Field | Type | Description |
|---|---|---|
skin_index | number | |
claimed | boolean | |
style | number | |
instance_id | number |
Custom Skins
me.shop.customSkins({ page, page_size, type, weapon, search }), me.shop.customSkin(skinId)
The player's own skins from the skin editor, page_size 1 to 100.
GET /skins
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
page | integer | query | no | |
page_size | integer | query | no | |
type | integer | query | no | |
weapon | integer | query | no | |
search | string | query | no |
Response Fields:
| Field | Type | Description |
|---|---|---|
skins | object[] | |
skins[].skinId | number | |
skins[].name | string | |
skins[].type | number | |
skins[].weapon | number | |
skins[].rarity | number | |
skins[].itemConfig | object | |
skins[].playerId | number | |
skins[].createdAt | string | |
skins[].updatedAt | string | |
total | number | |
page | number | |
pageSize | number |
GET /skins/{skinId}
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
skinId | integer | path | yes |
Response Fields:
| Field | Type | Description |
|---|---|---|
skinId | number | |
name | string | |
type | number | |
weapon | number | |
rarity | number | |
itemConfig | object | |
playerId | number | |
createdAt | string | |
updatedAt | string |
Creator
What a creator sees of their own maps and studios, plus the player's clan applications. The studio replies are untyped in krapio (unknown), only their existence was verified.
Clan Applications
me.creator.clanApplications()
{clan_ids: number[] | null}, null without any.
Response Fields:
| Field | Type | Description |
|---|---|---|
clan_ids | number[] or null |
Map Versions
me.creator.mapVersions(mapId)
The version history of an own map, empty for a map that is not yours.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
mapId | integer | path | yes |
Map Data
me.creator.mapData(mapId, playerName)
A map's stored key value data for one player (the key value store of map scripts).
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
mapId | integer | path | yes | |
player | string | query | yes |
Studios
me.creator.studios(), .studioInvites(), .studio(id), .studioMonetization(id), .studioAnalytics(id), .studioSkins(id)
GET /studios/{studioId}
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
studioId | integer | path | yes |
GET /studios/{studioId}/monetization
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
studioId | integer | path | yes |
GET /studios/{studioId}/analytics
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
studioId | integer | path | yes |
GET /studios/{studioId}/skins
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
studioId | integer | path | yes |
Staff
Role gated reads for moderators and KPD reviewers. A normal account gets a 403 insufficient permissions (or not authorized for reports). The replies are untyped in krapio, no account with the role was available to verify them.
me.staff.chatBan(playerName), .reports(playerName), .kpdDemos({ off, q }), .kpdDemo(demoId, ct?), .kpdReviewers()
chatBan is a player's chat ban, kpdDemos the demo review queue (off is the offset, q a search), kpdDemo one demo where ct is the review ticket the game passes along.
GET /moderation/chat/{playerName}
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
playerName | string | path | yes |
GET /players/{playerName}/reports
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
playerName | string | path | yes |
GET /kpd/demos
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
off | integer | query | no | |
q | string | query | no |
GET /kpd/demos/{demoId}
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
demoId | string | path | yes | |
ct | string | query | no |
Ranked
https://api.krunker.io, the ranked system's own API. Every call answers 401 Access token required without a token, including the public leaderboard.
Season Rankings
me.ranked.leaderboard(season, { limit, offset, region })
The game reads 40 per page; limit 1 to 100, offset 0 based. Regions are the ranked region ids (2 Asia, 3 Europe, 4 North America).
GET /leaderboards/rankings/season/{season}
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
season | integer | path | yes | |
limit | integer | query | no | |
offset | integer | query | no |
Response Fields:
| Field | Type | Description |
|---|---|---|
data | object | GET api.krunker.io/leaderboards/rankings/season/:season[/region/:region] |
data.leaderboard | object[] | |
data.leaderboard[].position | number | |
data.leaderboard[].profile | object | a player's ranked card in the season rankings |
data.totalPages | number |
GET /leaderboards/rankings/season/{season}/region/{region}
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
season | integer | path | yes | |
region | integer | path | yes | |
limit | integer | query | no | |
offset | integer | query | no |
Response Fields:
| Field | Type | Description |
|---|---|---|
data | object | GET api.krunker.io/leaderboards/rankings/season/:season[/region/:region] |
data.leaderboard | object[] | |
data.leaderboard[].position | number | |
data.leaderboard[].profile | object | a player's ranked card in the season rankings |
data.totalPages | number |
Ranked Profile
me.ranked.profile(season, region?)
The player's ranked card with season totals. total_accuracy here is the only aggregate ranked accuracy anywhere in Krunker's APIs, and it is own account only.
GET /player-profile/me/season/{season}
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
season | integer | path | yes |
Response Fields:
| Field | Type | Description |
|---|---|---|
data | object | GET api.krunker.io/player-profile/me/season/:season[/region/:region], the only place with a season accuracy total |
data.profile | object | |
data.profile.player_id | number | |
data.profile.season | number | |
data.profile.rank | number | |
data.profile.mu | number | |
data.profile.sigma | number | |
data.profile.conservative_rating | number | |
data.profile.wins | number | |
data.profile.losses | number | |
data.profile.total_matches | number | |
data.profile.total_kills | number | |
data.profile.total_deaths | number | |
data.profile.total_assists | number | |
data.profile.total_score | number | |
data.profile.total_objective_score | number | |
data.profile.total_accuracy | number | |
data.profile.total_damage_done | number | |
data.profile.total_time_played | number | |
data.profile.highest_kill_count | number | |
data.profile.highest_damage_in_match | number | |
data.profile.player_score | number | |
data.profile.level | number | |
data.profile.last_milestone_claimed | number | |
data.profile.player_name | string | |
data.milestones | object[] | |
data.milestones[].id | number | |
data.milestones[].matches | number | |
data.milestones[].reward | number |
GET /player-profile/me/season/{season}/region/{region}
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
season | integer | path | yes | |
region | integer | path | yes |
Response Fields:
| Field | Type | Description |
|---|---|---|
data | object | GET api.krunker.io/player-profile/me/season/:season[/region/:region], the only place with a season accuracy total |
data.profile | object | |
data.profile.player_id | number | |
data.profile.season | number | |
data.profile.rank | number | |
data.profile.mu | number | |
data.profile.sigma | number | |
data.profile.conservative_rating | number | |
data.profile.wins | number | |
data.profile.losses | number | |
data.profile.total_matches | number | |
data.profile.total_kills | number | |
data.profile.total_deaths | number | |
data.profile.total_assists | number | |
data.profile.total_score | number | |
data.profile.total_objective_score | number | |
data.profile.total_accuracy | number | |
data.profile.total_damage_done | number | |
data.profile.total_time_played | number | |
data.profile.highest_kill_count | number | |
data.profile.highest_damage_in_match | number | |
data.profile.player_score | number | |
data.profile.level | number | |
data.profile.last_milestone_claimed | number | |
data.profile.player_name | string | |
data.milestones | object[] | |
data.milestones[].id | number | |
data.milestones[].matches | number | |
data.milestones[].reward | number |
Match History
me.ranked.matchHistory(season, { limit, offset, gameMode, isRanked, region })
:mode is a gamemode id or the literal undefined for all modes (that is what the game sends, and the API accepts it), :ranked is true, false or all. The game reads 5 per page. Returns {matchHistory: {matches, totalPages}}.
GET /match-history/me/season/{season}/gameMode/{mode}/isRanked/{ranked}
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
season | integer | path | yes | |
mode | integer or "undefined" | path | yes | `undefined` stands for every value |
ranked | boolean or "all" | path | yes | undefined = all; `all` stands for every value |
limit | integer | query | no | |
offset | integer | query | no |
Response Fields:
| Field | Type | Description |
|---|---|---|
data | object | GET api.krunker.io/match-history/me/..., paged |
data.matchHistory | object | |
data.matchHistory.matches | unknown[] | |
data.matchHistory.totalPages | number |
GET /match-history/me/season/{season}/gameMode/{mode}/isRanked/{ranked}/region/{region}
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
season | integer | path | yes | |
mode | integer or "undefined" | path | yes | `undefined` stands for every value |
ranked | boolean or "all" | path | yes | undefined = all; `all` stands for every value |
region | integer | path | yes | |
limit | integer | query | no | |
offset | integer | query | no |
Response Fields:
| Field | Type | Description |
|---|---|---|
data | object | GET api.krunker.io/match-history/me/..., paged |
data.matchHistory | object | |
data.matchHistory.matches | unknown[] | |
data.matchHistory.totalPages | number |
Connection
me.ranked.connection()
The ranked lobby the player is bound to: {serverUrl}, null without one.
Response Fields:
| Field | Type | Description |
|---|---|---|
data | object | GET api.krunker.io/ranked/me/connection |
data.serverUrl | string or null |
Daily Spin
me.ranked.dailySpin()
The daily spin meter: {score, maxScore, stage, timeRemaining, claimed}, timeRemaining in seconds.
Response Fields:
| Field | Type | Description |
|---|---|---|
data | object | GET api.krunker.io/daily-spin/progress |
data.score | number | |
data.maxScore | number | |
data.stage | number | |
data.timeRemaining | number | seconds |
data.claimed | boolean |