Developer API
The documented Krunker developer API, corrected against the live service
Base URL: https://gapi.svc.krunker.io
This is the developer API documentation as provided by Krunker (FRVR), with corrections from using it. Rows marked undocumented and notes marked observed were found by calling the endpoints in October 2026 and are not part of the original document; they may change without notice. The KPD, Events and Webhooks sections at the end were added later from a second document by Krunker; they are reproduced as documented and not verified, the key this document was checked with holds neither the kpd nor an events scope.
In krapio every endpoint below lives on the root of Krunker (krunker.player.profile(...), krunker.market.skin(...)). The endpoints the game reads without a key are in game-api.md and the ones bound to a player token in token-api.md.
Developer APIs
The Developer APIs provide read-only access to public game data for third-party integrations.
observed: player, clan and map name lookups are case-insensitive (Burg and burg return the same map, named burg).
Authentication
All developer API endpoints require the X-Developer-API-Key header.
X-Developer-API-Key: <your-api-key>Error Response (403 Forbidden):
{
"error": "Not allowed."
}Rate Limiting
All endpoints are rate-limited to 240 requests per minute per API key (observed: X-RateLimit-Reset is a unix timestamp about 60 s ahead; the keyless gapi paths send their own headers, 5 per route per second with a relative reset in seconds). Some endpoints have a lower limit of their own on top (GET /api/events: 6 per minute).
Rate Limit Headers:
Every response includes headers to help you track your usage:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per window (240, or the endpoint's own limit) |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the rate limit resets |
Rate Limit Exceeded Response (429 Too Many Requests):
{
"error": "Rate limit exceeded",
"retry_after": 45
}| Field | Type | Description |
|---|---|---|
| error | string | Error message |
| retry_after | int | Seconds until the rate limit resets |
Player Endpoints
Get Player Profile
Retrieve comprehensive player statistics and profile information.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| playername | string | path | yes | The player's in-game username |
Response Fields:
| Field | Type | Description |
|---|---|---|
| player_id | int | Unique player identifier |
| player_name | string | Player's username |
| clan | string | Player's clan name (empty string if not in a clan) |
| verified | bool | Whether the player is verified |
| flag | int | Player's country flag index |
| badges | int[] | Array of badge IDs the player has earned |
| following | int | Number of players this player is following |
| followers | int | Number of players following this player |
| ranked | object[] | Array of ranked profiles for the current season, one per region (only includes regions where player has completed 6+ placement matches) |
| ranked[].region | int | Region ID |
| ranked[].mmr | int | Matchmaking rating |
| ranked[].wins | int | Ranked wins in this region |
| ranked[].losses | int | Ranked losses in this region |
| ranked[].kills | int | Total kills in ranked matches |
| ranked[].deaths | int | Total deaths in ranked matches |
| ranked[].assists | int | Total assists in ranked matches |
| ranked[].score | int | Total score in ranked matches |
| ranked[].damage_done | int | Total damage dealt in ranked matches |
| ranked[].time_played | int | Total time played in ranked matches (seconds) |
| kr | int | Currency balance |
| level | int | Calculated from score (0.03 × √score, min 1) |
| junk | float | ELO/Junk rating |
| inventory | int | Total skin value |
| score | int | Total score |
| spk | float | Score per kill |
| kills | int | Total kills |
| deaths | int | Total deaths |
| kdr | float | Kill/Death ratio |
| kpg | float | Kills per game |
| games | int | Games played |
| wins | int | Games won |
| losses | int | Games lost (games - wins) |
| assists | int | Total assists |
| melees | int | Melee kills |
| beatdowns | int | Fist kills |
| bullseyes | int | Thrown weapon kills |
| headshots | int | Headshot kills |
| legshots | int | Leg shot kills |
| wallbangs | int | Wallbang kills |
| shots | int | Total shots fired |
| hits | int | Shots that connected |
| misses | int | Shots missed (shots - hits) |
| time_played | int64 | Seconds played |
| nukes | int | Nukes called in |
| airdrops | int | Airdrops called in |
| airdrops_stolen | int | Airdrops stolen from enemies |
| slimes | int | Slimes called in |
| juggernauts | int | Juggernaut killstreaks earned |
| juggernauts_killed | int | Enemy juggernauts killed |
| warmachines | int | Warmachines called in |
| class_stats | object | undocumented - class id (string) to score with that class, e.g. {"0": 236755, "1": 20644276} |
| hacker_tagged | bool | Whether the player has been flagged as a cheater |
| kpd | bool | Whether the player is a KPD moderator |
| premium | bool | Whether the player has an active premium subscription |
| twitch_name | string | undocumented - linked Twitch name, empty string if none |
| profile_picture | string | Profile picture URL (empty string if not premium) |
| created_at | string | Account creation date (RFC3339 format) |
| maps | array | undocumented - maps the player voted on as {id, name, votes}, only seen as [] so far |
| mods | array | undocumented - mods the player voted on as {id, name, votes}, only seen as [] so far |
Get Player Inventory
Retrieve a player's skin inventory.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| playername | string | path | yes | The player's in-game username |
Response Fields:
| Field | Type | Description |
|---|---|---|
| skin_index | int | Identifier for the skin |
| skin_id | int | undocumented - id of the owned item instance (not the definition) |
| count | int | Number owned (excludes market listings) |
| metadata | object | undocumented - null or kounter stats and float data, every field optional: wear, style, serial, kills, hskills, headshots, hits, shots, jumps |
| stackable | bool | undocumented |
Get Player Listings
Retrieve a player's active market listings (paginated).
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| playername | string | path | yes | The player's in-game username |
| page | int | query | no | Page number (default: 1) |
Response Fields:
| Field | Type | Description |
|---|---|---|
| skin_index | int | Identifier for the skin being sold |
| price | int | Listing price in KR |
| listed_at | string | When the listing was created |
Notes:
- Only active listings are returned
- Page size is fixed at 10 records
- Listings are ordered by date listed (newest first)
Get Player Match History
Retrieve a player's ranked match history (paginated).
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| playername | string | path | yes | The player's in-game username |
| page | int | query | no | Page number (default: 1) |
| season | int | query | no | Season filter (default: all seasons) |
Response Fields:
| Field | Type | Description |
|---|---|---|
| match_id | int64 | Unique match identifier |
| date | string | Match date (RFC3339 format) |
| map | int | Map ID |
| duration | int | Match duration in seconds |
| season | int | Ranked season number |
| region | int | Region ID |
| kills | int | Player's kills in this match |
| deaths | int | Player's deaths in this match |
| assists | int | Player's assists in this match |
| score | int | Player's score in this match |
| damage_done | int | Player's damage dealt in this match |
| headshots | int | Player's headshots in this match |
| accuracy | int | Player's accuracy percentage |
| objective_score | int | Player's objective score |
| kr | int | KR earned from this match |
| victory | int | Match result (1 = win, 0 = loss, -1 = draw) |
| rounds_won | int | Rounds won by player's team |
| team | int | Player's team number |
| play_time | int | Player's active play time in milliseconds |
| mmr | int | Player's MMR at the time of this match |
Notes:
- Only ranked matches are returned
- Page size is fixed at 10 records
- Matches are ordered by date descending
- observed:
matchesisnullinstead of[]when the page is past the end - The
mmrfield shows the player's rating after each match, allowing you to calculate MMR changes between matches
Get Player Posts
Retrieve a player's social media posts (paginated).
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| playername | string | path | yes | The player's in-game username |
| page | int | query | no | Page number (default: 1) |
Response Fields:
| Field | Type | Description |
|---|---|---|
page | number | |
per_page | number | |
posts | object[] | |
posts[].date | string | |
posts[].text | string | |
posts[].votes | number | |
posts[].comment_count | number |
Match Endpoints
Get Match Details
Retrieve detailed information about a specific ranked match.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| id | int64 | path | yes | The match ID |
Notes:
- Only ranked matches are returned
- Participants are ordered by team, then by score descending
Response Fields:
| Field | Type | Description |
|---|---|---|
match_id | number | |
date | string | |
map | number | |
duration | number | |
season | number | |
region | number | |
participants | object[] | ordered by team, then score descending |
participants[].player_name | string | |
participants[].kills | number | Player's kills in this match |
participants[].deaths | number | Player's deaths in this match |
participants[].assists | number | Player's assists in this match |
participants[].score | number | Player's score in this match |
participants[].damage_done | number | Player's damage dealt in this match |
participants[].headshots | number | Player's headshots in this match |
participants[].accuracy | number | Player's accuracy percentage |
participants[].objective_score | number | Player's objective score |
participants[].victory | 1 | 0 | -1 | 1 = win, 0 = loss, -1 = draw |
participants[].rounds_won | number | Rounds won by player's team |
participants[].team | number | Player's team number |
participants[].play_time | number | milliseconds |
Clan Endpoints
Get Clan Profile
Retrieve a clan's public information.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| clanname | string | path | yes | The clan's name |
Response Fields:
| Field | Type | Description |
|---|---|---|
| name | string | Clan name |
| owner_name | string | Name of the clan owner |
| score | int64 | Clan's total score |
| rank | int | Clan's leaderboard rank |
| member_count | int | Number of members in the clan |
| created_at | string | Clan creation date |
| discord | string | Discord invite code (empty string if not set) |
Get Clan Members
Retrieve a clan's member list (paginated).
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| clanname | string | path | yes | The clan's name |
| page | int | query | no | Page number (default: 1) |
Response Fields:
| Field | Type | Description |
|---|---|---|
| player_name | string | Member's username |
| role | int | Member's role in the clan |
| verified | bool | Whether the player is verified |
| score | int | undocumented - the member's total score |
Notes:
- Page size is fixed at 10 records
- Members are sorted by role (descending), then by score (descending)
Leaderboard Endpoints
Get Ranked Leaderboard
Retrieve the ranked leaderboard for a specific region.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| region | int | path | yes | The region ID (2 = Asia, 3 = Europe, 4 = North America) |
| page | int | query | no | Page number (default: 1) |
| season | int | query | no | Season number (default: current season) |
Response Fields:
| Field | Type | Description |
|---|---|---|
| page | int | Current page number |
| per_page | int | Number of entries per page (10) |
| season | int | Current ranked season |
| region | int | Region ID |
| entries | object[] | Array of leaderboard entries |
| entries[].position | int | Leaderboard position |
| entries[].player_name | string | Player's username |
| entries[].mmr | int | Matchmaking rating |
| entries[].wins | int | Total ranked wins |
| entries[].losses | int | Total ranked losses |
| entries[].kills | int | Total kills in ranked matches |
| entries[].deaths | int | Total deaths in ranked matches |
| entries[].assists | int | Total assists in ranked matches |
| entries[].score | int | Total score in ranked matches |
| entries[].damage_done | int | Total damage dealt in ranked matches |
Notes:
- Only players with 6+ completed placement matches appear on the leaderboard
- Page size is fixed at 10 records
- Entries are sorted by MMR descending
- Use the
seasonparameter to query historical seasons (omit or set to 0 for current season)
Map Endpoints
Get Map by Name
Retrieve map information by its name.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| mapname | string | path | yes | The map's name (URL encoded) |
Response Fields:
| Field | Type | Description |
|---|---|---|
| map_id | int | Unique map identifier |
| name | string | Map name |
| description | string | Map description |
| creator_name | string | Username of the map creator |
| votes | int | Total upvotes |
| gameplays | int | Number of times the map has been played |
| playtime | int64 | Total playtime in milliseconds |
| category | int | Map category ID |
| created_at | string | When the map was created |
| updated_at | string | When the map was last updated |
| leaderboard_type | string | Type of leaderboard (e.g., "time", "score", empty if none) |
| leaderboard_order | int | Sort order for leaderboard (0 = ascending/lower is better, 1 = descending/higher is better) |
| featured | bool | undocumented - whether the map is currently featured |
Notes:
- Returns 404 if map not found or is restricted/deleted
- Map name is case-sensitive (observed: it is not,
burgfindsBurg) - observed:
created_atandupdated_atcome asYYYY-MM-DD HH:MM:SS, not RFC3339
Get Featured Maps
List every map that is currently featured, newest first. Use Get Map by Name for a map's details. To be told when a map is featured or unfeatured, use Events.
Response Fields:
| Field | Type | Description |
|---|---|---|
| maps[].map_id | int | Unique map identifier |
| maps[].name | string | Map name |
| maps[].creator_id | int | Player id of the map creator |
| maps[].creator_name | string | Username of the map creator |
| count | int | Number of maps returned |
Get Map Leaderboard
Retrieve the leaderboard for a specific map (paginated).
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| mapname | string | path | yes | The map's name (URL encoded) |
| page | int | query | no | Page number (default: 1) |
| type | string | query | no | Built-in leaderboard to rank on: score or time. Defaults to the map's configured leaderboard |
| order | int | query | no | 0 = ascending, 1 = descending. Defaults to the map's order for its configured leaderboard; otherwise ascending for time, descending for score |
Response Fields:
| Field | Type | Description |
|---|---|---|
| page | int | Current page number |
| per_page | int | Number of entries per page (25) |
| map_name | string | The map name queried |
| leaderboard_type | string | Type of leaderboard (e.g., "time", "score") |
| leaderboard_order | int | Sort order (0 = ascending, 1 = descending) |
| entries | object[] | Array of leaderboard entries |
| entries[].position | int | Leaderboard position |
| entries[].player_name | string | Player's username |
| entries[].value | int | Score or time value |
| entries[].date | string | When the entry was recorded |
Notes:
- Returns empty entries array if the map has no leaderboard configured
- Maps can show both a score and a time leaderboard in-game; use
type(andorder) to fetch either. Invalid values return 400 - Page size is fixed at 25 records
- Entries are sorted by value according to leaderboard_order
Get Map Leaderboard Player
Look up a specific player's position on a map leaderboard.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| mapname | string | path | yes | The map's name (URL encoded) |
| playername | string | path | yes | The player's username (URL encoded) |
| type | string | query | no | Built-in leaderboard to rank on: score or time. Defaults to the map's configured leaderboard |
| order | int | query | no | 0 = ascending, 1 = descending. Defaults to the map's order for its configured leaderboard; otherwise ascending for time, descending for score |
Response Fields:
| Field | Type | Description |
|---|---|---|
| map_name | string | The map name queried |
| leaderboard_type | string | Type of leaderboard (e.g., "time", "score") |
| leaderboard_order | int | Sort order (0 = ascending, 1 = descending) |
| player_name | string | Player's username |
| position | int | Player's position on the leaderboard |
| value | int | Player's score or time value |
| date | string | When the entry was recorded |
Notes:
- Returns 404 if the map has no leaderboard (and no
typeis given) or the player has no entry - Position is calculated dynamically based on current leaderboard standings
Mod Endpoints
Get Mods
Retrieve a paginated list of mods, sorted by votes (most popular first).
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| page | int | query | no | Page number (default: 1) |
Response Fields:
| Field | Type | Description |
|---|---|---|
| page | int | Current page number |
| per_page | int | Number of mods per page (10) |
| mods | object[] | Array of mod objects |
| mods[].mod_id | int | Unique mod identifier |
| mods[].name | string | Mod name |
| mods[].description | string | Mod description |
| mods[].creator_name | string | Username of the mod creator |
| mods[].votes | int | Number of upvotes |
| mods[].featured | bool | Whether the mod is featured |
| mods[].version | int | Mod version number |
| mods[].created_at | string | When the mod was first published (observed: YYYY-MM-DD HH:MM:SS, not RFC3339) |
| mods[].updated_at | string | When the mod was last updated (same format) |
Notes:
- Mods are sorted by votes (most popular first)
- Only active mods are returned (deleted mods are excluded)
- Mods from banned users are excluded
Get Mod by Name
Retrieve detailed information about a specific mod.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| modname | string | path | yes | The mod's name (URL encoded) |
Response Fields:
| Field | Type | Description |
|---|---|---|
| mod_id | int | Unique mod identifier |
| name | string | Mod name |
| description | string | Mod description |
| creator_name | string | Username of the mod creator |
| votes | int | Number of upvotes |
| featured | bool | Whether the mod is featured |
| version | int | Mod version number |
| created_at | string | When the mod was first published |
| updated_at | string | When the mod was last updated |
Market Endpoints
Get Market Listings by Skin Index
Retrieve market information and active listings for a specific skin.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| skinindex | int | path | yes | The skin index (item type ID) |
| page | int | query | no | Page number for listings (default: 1) |
| days | int | query | no | Number of days for price history (default: 30, max: 120) |
Response Fields:
| Field | Type | Description |
|---|---|---|
| skin_index | int | The skin index queried |
| total_listings | int | Total number of active listings |
| lowest_price | int | Lowest listed price (0 if no listings) |
| average_price | float | Average sale price from last 7 days |
| total_circulating | int | Total quantity of this skin in circulation |
| listings | object[] | Paginated list of active listings (sorted by price ascending) |
| listings[].price | int | Listing price in KR |
| listings[].seller_name | string | Seller's username |
| listings[].listed_at | string | When the listing was created |
| owners | object[] | First 100 owners sorted by quantity owned (descending) |
| owners[].player_name | string | Owner's username |
| owners[].count | int | Number of this skin the player owns |
| price_history | object[] | Daily average sale prices for the specified time period |
| price_history[].date | string | Date (YYYY-MM-DD format) |
| price_history[].average_price | float | Average sale price on that day |
| price_history[].sales | int | Number of sales on that day |
Notes:
- Listings are sorted by price (lowest first)
- Page size is fixed at 10 records for listings
average_priceis calculated from sales in the last 7 days- Owners list shows up to 100 players, sorted by quantity owned (highest first)
- Banned users are excluded from the owners list
price_historycontains daily averages for days with sales within the specifieddaysparameter (default 30, max 120 days / 4 months)- observed: this is the slow endpoint, over 10 s cold and about 3 s warm; every other endpoint answers in under a second
Skin Definitions
Get Skin Definitions
List all skin definitions with optional filtering.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| page | int | query | no | Page number (default: 1) |
| per_page | int | query | no | Items per page (default: 100, max: 10000) |
| season | int | query | no | Season filter (returns only skins for the specified season) |
Response Fields:
| Field | Type | Description |
|---|---|---|
| page | int | Current page number |
| per_page | int | Items per page |
| total | int | Total number of matching skins |
| skins | array | Array of skin objects |
| skins[].index | int | Skin index (ID) |
| skins[].name | string | Skin name |
| skins[].type | int | Skin type |
| skins[].rarity | int | Skin rarity |
| skins[].weapon | int | Weapon index (omitted if null) |
| skins[].seas | int | undocumented - the skin's season; this is the field name the season filter refers to, there is no season field in the reply |
| skins[].thumbnail | string | undocumented - preview image URL |
| skins[].tradable | bool | undocumented |
| skins[].id | int | undocumented - equals index on GET /api/skins/:skinid, absent in the list |
Notes:
- Each skin object may contain additional properties from its configuration, merged into the root level (observed:
creator,glow,frames,frameT,mid,midT,pulsT, ...) - Use the
seasonparameter to retrieve skins for a specific ranked season - Skins without a season field are treated as season 1
Example:
Get skins from season 12:
Get Skin Definition
Get a single skin definition by its index.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| skinid | int | path | yes | The skin index (0-based) |
Error Responses:
- 400 if skinid is not a valid integer
- 404 if skin does not exist
Response Fields:
| Field | Type | Description |
|---|---|---|
index | number | Skin index (ID) |
name | string | Skin name |
type | number | Skin type |
rarity | number | Skin rarity |
weapon | number | omitted when the skin has no weapon |
seas | number | the season; the docs call this field season |
id | number | undocumented, equals index on the single skin endpoint |
thumbnail | string | undocumented |
tradable | boolean | undocumented |
KPD Endpoints
Get Latest KPD Calls
List the 50 most recent KPD (player hacker-report) calls, newest first, across all statuses so officer verdicts are visible. Requires the kpd scope.
Parameters:
None.
Response Fields:
| Field | Type | Description |
|---|---|---|
| id | int | Report id |
| date | string | Call time (RFC 3339, UTC) |
| caller_id | int | Reporter player id |
| caller_name | string | Reporter name |
| caller_score | int | Reporter score |
| caller_job_rating | int | Reporter's total KPD job rating |
| caller_job_rating_positive | int | Reporter's correct (positive) call count |
| called_id | int | Reported player id |
| called_name | string | Reported player name (guest if unregistered) |
| called_score | int | Reported player score |
| kdf_type | int | Call context: 0=public, 1=ranked, 2=comp, 3=custom |
| kdf_type_label | string | Readable kdf_type |
| status | int | 1=not responded, 2=responded, 3=reviewed |
| status_label | string | Readable status |
| rating | int | Officer verdict: 0=pending, 1=banned, 2=rejected |
| rating_label | string | Readable rating |
| officer_id | int | Officer who handled the call (0 if none) |
| officer_name | string | Officer name |
| status_date | string | Last status change (RFC 3339, UTC) |
| game_code | string | Game instance code |
| host | string | Game server host |
| port | string | Game server port |
| token | string | Internal call token |
Notes:
- Player IP addresses are not exposed.
- A fixed window of the 50 most recent calls; there is no pagination.
Events
Events tell you when something happens on Krunker, such as a map being featured. You can poll for them, or have them pushed to a webhook (below). Both use the same events.
Each event type belongs to one events scope. A key only sees the types whose scope it holds; ask for a scope from the partner portal.
| Type | Scope | Sent when |
|---|---|---|
map.featured | events.maps | A map was featured |
map.unfeatured | events.maps | A map is no longer featured |
Every key with the scope gets the event. Events are kept for 30 days.
List Events
Returns events newer than after, oldest first. Store next and pass it as after on your next call. Start with after=0. Keep the same filters between calls; next also moves past events your filters skipped, so changing filters later does not bring those back.
Rate limit: 6 requests per minute per key, on top of the general limit. The X-RateLimit-* headers on this endpoint describe this 6-per-minute limit. Poll every 20 to 60 seconds; if has_more is true, fetch the next page right away.
Parameters:
| Name | Type | Location | Required | Description |
|---|---|---|---|---|
| after | int | query | no | Event id cursor (default 0) |
| types | string | query | no | Comma-separated event types to return (default: all your scopes allow) |
| target_type | string | query | no | Only events about this kind of target, e.g. map |
| target_id | string | query | no | Comma-separated target ids, up to 100 (needs target_type), e.g. target_type=map&target_id=8497,8500 |
| limit | int | query | no | 1 to 100 (default 100) |
Events appear a few seconds after they happen. Ids only increase; gaps are normal.
Event data:
| Type | data fields |
|---|---|
map.featured, map.unfeatured | map_id, map_name, creator |
Response Fields:
| Field | Type | Description |
|---|---|---|
events | object[] | oldest first |
events[].id | number | only ever increases, gaps are normal |
events[].type | string | the types documented so far; a key only sees the types of the event scopes it holds |
events[].created_at | string | rfc3339 utc |
events[].target | object | what an event is about, e.g. type "map" and the map id |
events[].target.type | string | |
events[].target.id | number or string | |
events[].data | object or object | |
events[].data.map_id | number | |
events[].data.map_name | string | |
events[].data.creator | string | |
next | number | pass as `after` on the next call, it also moves past events the filters skipped |
has_more | boolean |
Webhooks
Instead of polling, Krunker can POST each event to your HTTPS endpoint. The Krunker team enables webhooks for your key; you then add endpoints, pick their event types and see deliveries under Developer > Webhooks in the partner portal.
Request: POST with a JSON body shaped like one item of events above, and these headers:
| Header | Description |
|---|---|
Krunker-Event-Id | The event id. The same event can arrive more than once; use this header, not the body's id, to ignore repeats (test and signature-check requests have "id": 0 and an id like test-... / verify-... here) |
Krunker-Event-Type | The event type, e.g. map.featured (webhook.test for test sends, webhook.verify for the signature check) |
Krunker-Delivery-Id | This delivery |
Krunker-Delivery-Attempt | 1 for the first try, then 2, 3, ... It can also go up without a failed request on your side (e.g. an attempt cut off by a restart on ours), and starts again at 1 when you redeliver from the portal |
Krunker-Signature | t=<unix seconds>,v1=<signature> |
Respond with any 2xx within 10 seconds. Anything else, a timeout or a redirect counts as a failure and is retried after 30s, 2m, 10m, 30m, 1h, 2h, 4h, 8h and 8h (about a day). If an endpoint keeps failing for that whole time it is turned off and you get a Krunker Mail; fix it and turn it back on in the portal. Events that happen while a webhook is off are not sent to it; catch up with GET /api/events, or redeliver single events from the portal. Delivery order is not guaranteed.
Verify the signature. Compute HMAC-SHA256 of <t>.<raw request body> with your endpoint's signing secret (whsec_..., shown once when you create or rotate it), hex-encode it and compare it with v1 in constant time. Reject requests whose t is more than 5 minutes old. Answer 401 when either check fails.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return parts.v1?.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}Signature check. A webhook receives nothing until its endpoint passes a check. Click Verify on the webhook page after you install the secret. Krunker then sends 10 webhook.verify requests in random order:
- Some are signed with your secret; answer them with a 2xx.
- The others carry a wrong signature; reject them with
401 Unauthorized. Any other status fails the check.
The first wrong answer, error or timeout fails the check, and the page says which request failed. After you rotate the secret, verify again. If you change the URL, the check runs again by itself. It also runs again when you turn a webhook back on after it was turned off for failing.
{
"id": 0,
"type": "webhook.verify",
"created_at": "2026-10-11T09:12:44Z",
"target": { "type": "webhook", "id": 12 },
"data": { "nonce": "4f9c0e6a1b2d3c4e5f60718293a4b5c6" }
}Endpoints must use https and resolve to a public address.
In krapio, verifyWebhook(rawBody, request.headers.get("Krunker-Signature"), secret) does the whole check above on WebCrypto, and signWebhook produces the header for your own tests.
Error Responses
All endpoints return standard error responses:
| Status Code | Description |
|---|---|
| 400 | Bad request (missing or invalid parameters) |
| 403 | Forbidden (invalid API key) |
| 404 | Resource not found |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
observed: 404 replies carry the plain text body Not Found, not the json below.
Error Response Format:
{
"error": "Error message description"
}