krapio

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

HostUsed byReply shape
https://gapi.svc.krunker.ioeverything unless noted{"data": ..., "error": null}, unwrapped by krapio
https://api.krunker.ioranked.*{"data": ...} without an error field on success, unwrapped by krapio
StatusWhen
401 invalid tokenbad signature or expired; Access token required on api.krunker.io without a header
403 insufficient permissions, not authorizeda 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:

FieldTypeDescription
playerobject
player.idstring
player.namestring
player.dataunknown[]

Wallet

me.player.wallet()

Kr plus every studio currency.

Response Fields:

FieldTypeDescription
currenciesobject[]
currencies[].idnumber
currencies[].namestring
currencies[].symbolstring
currencies[].amountnumber
spending_tiernumber

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:

FieldTypeDescription
profileobject
profile.scorenumber
profile.fundsnumber
profile.killsnumber
profile.deathsnumber
profile.gamesnumber
profile.winsnumber

Usernames

me.player.usernames()

The active name, the reserved names and the rename cooldown.

Response Fields:

FieldTypeDescription
activestring
namesobject[]
names[].namestring
names[].statestring
names[].leave_release_secsnumber
cooldown_secsnumber

Email Preferences

me.player.emailPreferences()

{chosen, promotional}.

Response Fields:

FieldTypeDescription
chosenboolean
promotionalboolean

Discord Status

me.player.discordStatus()

{configured, linked, id?, username?, globalName?, avatar?, linkedAt?, guildUrl?, inGuild?}, the optional fields only while linked.

Response Fields:

FieldTypeDescription
configuredboolean
linkedboolean
idstring
usernamestring
globalNamestring
avatarstring
linkedAtnumber
guildUrlstring
inGuildboolean

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.

NameTypeDescription
item_idintone skin index
itemsint listseveral skin indices
sortstringlatest, price_asc, price_desc or popular
cursor_idintthe last uid of the previous page
limitint1 to 100
rarities, weapons, types, wearsint listcomma separated filters
seasonint
paintedboolsent as 1 when true
searchTermstring

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:

NameTypeLocationRequiredDescription
item_idintegerqueryno
itemsinteger[]queryno
sort"latest" | "price_asc" | "price_desc" | "popular"queryno
cursor_idintegerqueryno
limitintegerqueryno
raritiesinteger[]queryno
weaponsinteger[]queryno
typesinteger[]queryno
wearsinteger[]queryno
seasonintegerqueryno
painted1querynosent as 1 when `painted` is set
searchTermstringqueryno

Response Fields:

FieldTypeDescription
listingsobject[]
listings[].uidnumber
listings[].marketidnumber
listings[].skinidnumberthe item instance
listings[].skinindexnumber
listings[].fundsnumberasking price in kr
listings[].seller_idnumber
listings[].seller_namestring
listings[].listed_timenumberunix ms
listings[].listing_countnumberhow many of this skin the seller has listed
has_moreboolean
next_cursor_idnumber or nullthe 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:

FieldTypeDescription
inumber
cntnumber
sidnumber
stnumber

Sales

me.market.sales()

The player's own open listings, [] when there are none.

Response Fields:

FieldTypeDescription
uidnumber
marketidnumber
skinidnumberthe item instance
skinindexnumber
fundsnumberasking price in kr
listed_timenumberunix 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:

NameTypeLocationRequiredDescription
sort"date_asc" | "date_desc" | "amount_asc" | "amount_desc" | "item_asc" | "item_desc"queryno
cursorintegerqueryno
limitintegerqueryno

Response Fields:

FieldTypeDescription
historyobject[]
history[].marketidnumber
history[].typestring
history[].datestring
history[].skinindexnumber
history[].amountnumber
history[].other_party_idnumber
history[].other_party_namestring
history[].statusstring
has_moreboolean
next_cursornumber or nullthe 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:

FieldTypeDescription
peak_mmrnumber 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:

NameTypeLocationRequiredDescription
offsetintegerqueryno
limitintegerqueryno
include_open0querynosent as 0 when `includeOpen` is set

Response Fields:

FieldTypeDescription
openobject[]
open[].tridnumber
open[].sksellobject[]items offered
open[].sksell[].indnumber
open[].skbuyobject[]items asked for
open[].skbuy[].indnumber
open[].csellobject[]
open[].csell[].cidnumber
open[].csell[].amtnumber
open[].cbuyobject[]
open[].cbuy[].cidnumber
open[].cbuy[].amtnumber
open[].plnumberid of the player who made the offer, their items are sksell
open[].buynumberid of the player the offer went to, their items are skbuy
open[].buyerstringname of the player the offer went to
open[].playerstringname of the player who made the offer
open[].messagestring
open[].dtestringwhen the offer was made, "YYYY-MM-DD HH:MM:SS" utc; a completed trade keeps it
completedobject[]
completed[].tridnumber
completed[].sksellobject[]items offered
completed[].sksell[].indnumber
completed[].skbuyobject[]items asked for
completed[].skbuy[].indnumber
completed[].csellobject[]
completed[].csell[].cidnumber
completed[].csell[].amtnumber
completed[].cbuyobject[]
completed[].cbuy[].cidnumber
completed[].cbuy[].amtnumber
completed[].plnumberid of the player who made the offer, their items are sksell
completed[].buynumberid of the player the offer went to, their items are skbuy
completed[].buyerstringname of the player the offer went to
completed[].playerstringname of the player who made the offer
completed[].messagestring
completed[].dtestringwhen the offer was made, "YYYY-MM-DD HH:MM:SS" utc; a completed trade keeps it
completed_has_moreboolean

Trade Inventories

me.trades.inventory(playerId)

Both inventories side by side: {my_inventory, their_inventory}, each {i, cnt, sid, st}[].

Parameters:

NameTypeLocationRequiredDescription
playerIdintegerpathyes

Response Fields:

FieldTypeDescription
my_inventoryobject[]
my_inventory[].inumber
my_inventory[].cntnumber
my_inventory[].sidnumber
my_inventory[].stnumber
their_inventoryobject[]
their_inventory[].inumber
their_inventory[].cntnumber
their_inventory[].sidnumber
their_inventory[].stnumber

me.trades.playerSearch(playerName)

One player by exact name: {player_id, player_name, player_score}.

Parameters:

NameTypeLocationRequiredDescription
namestringqueryyes

Response Fields:

FieldTypeDescription
player_idnumber
player_namestring
player_scorenumber

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, ...).

RequestBodyReply dataThe 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 indexthe 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/acceptnone{funds}/trades/history?include_open=0 about a second later, with the trade under completed
POST /trades/:id/cancelnone{success: true}nothing right away
POST /trades/decline-allnonenot 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:

FieldTypeDescription
skinsobject[]
skins[].idnumber
skins[].skin_idnumber
skins[].pricenumber
skins[].max_purchasenumber
skins[].purchase_countnumber

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:

FieldTypeDescription
errorstring

Wristband

me.shop.wristband()

The player's KrunkCup wristband skin and whether it was claimed: {skin_index, claimed, style, instance_id}.

Response Fields:

FieldTypeDescription
skin_indexnumber
claimedboolean
stylenumber
instance_idnumber

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:

NameTypeLocationRequiredDescription
pageintegerqueryno
page_sizeintegerqueryno
typeintegerqueryno
weaponintegerqueryno
searchstringqueryno

Response Fields:

FieldTypeDescription
skinsobject[]
skins[].skinIdnumber
skins[].namestring
skins[].typenumber
skins[].weaponnumber
skins[].raritynumber
skins[].itemConfigobject
skins[].playerIdnumber
skins[].createdAtstring
skins[].updatedAtstring
totalnumber
pagenumber
pageSizenumber

GET /skins/{skinId}

Parameters:

NameTypeLocationRequiredDescription
skinIdintegerpathyes

Response Fields:

FieldTypeDescription
skinIdnumber
namestring
typenumber
weaponnumber
raritynumber
itemConfigobject
playerIdnumber
createdAtstring
updatedAtstring

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:

FieldTypeDescription
clan_idsnumber[] or null

Map Versions

me.creator.mapVersions(mapId)

The version history of an own map, empty for a map that is not yours.

Parameters:

NameTypeLocationRequiredDescription
mapIdintegerpathyes

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:

NameTypeLocationRequiredDescription
mapIdintegerpathyes
playerstringqueryyes

Studios

me.creator.studios(), .studioInvites(), .studio(id), .studioMonetization(id), .studioAnalytics(id), .studioSkins(id)

GET /studios/{studioId}

Parameters:

NameTypeLocationRequiredDescription
studioIdintegerpathyes

GET /studios/{studioId}/monetization

Parameters:

NameTypeLocationRequiredDescription
studioIdintegerpathyes

GET /studios/{studioId}/analytics

Parameters:

NameTypeLocationRequiredDescription
studioIdintegerpathyes

GET /studios/{studioId}/skins

Parameters:

NameTypeLocationRequiredDescription
studioIdintegerpathyes

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:

NameTypeLocationRequiredDescription
playerNamestringpathyes

GET /players/{playerName}/reports

Parameters:

NameTypeLocationRequiredDescription
playerNamestringpathyes

GET /kpd/demos

Parameters:

NameTypeLocationRequiredDescription
offintegerqueryno
qstringqueryno

GET /kpd/demos/{demoId}

Parameters:

NameTypeLocationRequiredDescription
demoIdstringpathyes
ctstringqueryno

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:

NameTypeLocationRequiredDescription
seasonintegerpathyes
limitintegerqueryno
offsetintegerqueryno

Response Fields:

FieldTypeDescription
dataobjectGET api.krunker.io/leaderboards/rankings/season/:season[/region/:region]
data.leaderboardobject[]
data.leaderboard[].positionnumber
data.leaderboard[].profileobjecta player's ranked card in the season rankings
data.totalPagesnumber

GET /leaderboards/rankings/season/{season}/region/{region}

Parameters:

NameTypeLocationRequiredDescription
seasonintegerpathyes
regionintegerpathyes
limitintegerqueryno
offsetintegerqueryno

Response Fields:

FieldTypeDescription
dataobjectGET api.krunker.io/leaderboards/rankings/season/:season[/region/:region]
data.leaderboardobject[]
data.leaderboard[].positionnumber
data.leaderboard[].profileobjecta player's ranked card in the season rankings
data.totalPagesnumber

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:

NameTypeLocationRequiredDescription
seasonintegerpathyes

Response Fields:

FieldTypeDescription
dataobjectGET api.krunker.io/player-profile/me/season/:season[/region/:region], the only place with a season accuracy total
data.profileobject
data.profile.player_idnumber
data.profile.seasonnumber
data.profile.ranknumber
data.profile.munumber
data.profile.sigmanumber
data.profile.conservative_ratingnumber
data.profile.winsnumber
data.profile.lossesnumber
data.profile.total_matchesnumber
data.profile.total_killsnumber
data.profile.total_deathsnumber
data.profile.total_assistsnumber
data.profile.total_scorenumber
data.profile.total_objective_scorenumber
data.profile.total_accuracynumber
data.profile.total_damage_donenumber
data.profile.total_time_playednumber
data.profile.highest_kill_countnumber
data.profile.highest_damage_in_matchnumber
data.profile.player_scorenumber
data.profile.levelnumber
data.profile.last_milestone_claimednumber
data.profile.player_namestring
data.milestonesobject[]
data.milestones[].idnumber
data.milestones[].matchesnumber
data.milestones[].rewardnumber

GET /player-profile/me/season/{season}/region/{region}

Parameters:

NameTypeLocationRequiredDescription
seasonintegerpathyes
regionintegerpathyes

Response Fields:

FieldTypeDescription
dataobjectGET api.krunker.io/player-profile/me/season/:season[/region/:region], the only place with a season accuracy total
data.profileobject
data.profile.player_idnumber
data.profile.seasonnumber
data.profile.ranknumber
data.profile.munumber
data.profile.sigmanumber
data.profile.conservative_ratingnumber
data.profile.winsnumber
data.profile.lossesnumber
data.profile.total_matchesnumber
data.profile.total_killsnumber
data.profile.total_deathsnumber
data.profile.total_assistsnumber
data.profile.total_scorenumber
data.profile.total_objective_scorenumber
data.profile.total_accuracynumber
data.profile.total_damage_donenumber
data.profile.total_time_playednumber
data.profile.highest_kill_countnumber
data.profile.highest_damage_in_matchnumber
data.profile.player_scorenumber
data.profile.levelnumber
data.profile.last_milestone_claimednumber
data.profile.player_namestring
data.milestonesobject[]
data.milestones[].idnumber
data.milestones[].matchesnumber
data.milestones[].rewardnumber

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:

NameTypeLocationRequiredDescription
seasonintegerpathyes
modeinteger or "undefined"pathyes`undefined` stands for every value
rankedboolean or "all"pathyesundefined = all; `all` stands for every value
limitintegerqueryno
offsetintegerqueryno

Response Fields:

FieldTypeDescription
dataobjectGET api.krunker.io/match-history/me/..., paged
data.matchHistoryobject
data.matchHistory.matchesunknown[]
data.matchHistory.totalPagesnumber

GET /match-history/me/season/{season}/gameMode/{mode}/isRanked/{ranked}/region/{region}

Parameters:

NameTypeLocationRequiredDescription
seasonintegerpathyes
modeinteger or "undefined"pathyes`undefined` stands for every value
rankedboolean or "all"pathyesundefined = all; `all` stands for every value
regionintegerpathyes
limitintegerqueryno
offsetintegerqueryno

Response Fields:

FieldTypeDescription
dataobjectGET api.krunker.io/match-history/me/..., paged
data.matchHistoryobject
data.matchHistory.matchesunknown[]
data.matchHistory.totalPagesnumber

Connection

me.ranked.connection()

The ranked lobby the player is bound to: {serverUrl}, null without one.

Response Fields:

FieldTypeDescription
dataobjectGET api.krunker.io/ranked/me/connection
data.serverUrlstring or null

Daily Spin

me.ranked.dailySpin()

The daily spin meter: {score, maxScore, stage, timeRemaining, claimed}, timeRemaining in seconds.

Response Fields:

FieldTypeDescription
dataobjectGET api.krunker.io/daily-spin/progress
data.scorenumber
data.maxScorenumber
data.stagenumber
data.timeRemainingnumberseconds
data.claimedboolean

On this page