API reference
DonutStats API
DonutSMP player stats, history, leaderboards and the live player count as JSON, for your own app or site.
- Base URL
https://api.donutstats.cc- Authentication
- Send your key in the
x-api-keyheader with every request. - Format
- JSON over HTTPS. Every answer has an
okfield.
Coming soonAPI keys are not on sale yet. They will be sold by the month. Tell us what you want to build and we will tell you when they open.
On this page
Quick start
Ask for one player. Put your key where it says ds_live_your_key.
curl "https://api.donutstats.cc/v1/player/Steve" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/player/Steve", {
headers: { "x-api-key": "ds_live_your_key" }
})
const { player } = await res.json()
console.log(player.name, player.stats.money)
import requests
res = requests.get(
"https://api.donutstats.cc/v1/player/Steve",
headers={"x-api-key": "ds_live_your_key"},
)
player = res.json()["player"]
print(player["name"], player["stats"]["money"])
The answer, with most fields left out here:
{
"ok": true,
"player": {
"name": "Steve",
"stats": { "money": "12.4M", "kills": "412", "playtime": "21d 4h" },
"num": { "money": 12400000, "kills": 412, "playtime": 1828800 },
"online": true,
"ageSec": 42
}
}Next: every field of a player, the rate limits, and the errors.
Authentication
Every request needs a key. Send it in either of these headers:
x-api-key: ds_live_your_key
Authorization: Bearer ds_live_your_key- Keys start with
ds_live_. Keys are coming soon: tell us what you want to build. - Keep your key on your own server. Calls from a web page in a browser are not supported.
- A wrong or switched-off key gets
401 invalid_api_key. A request with no key gets403 origin_not_allowed.
Players
Get a player
GET/v1/player/{name}
One player: every stat, whether they are online, and where they rank.
Parameters
| Name | In | Type | Required | Meaning |
|---|---|---|---|---|
name | path | string | required | A Minecraft name: 2 to 16 letters, digits or underscores. Bedrock names start with a dot. Upper and lower case do not matter. |
maxAge | query | integer | optional | Accept a saved copy up to this many seconds old. 600 when left out, 15 at the least. See Caching. |
Example
curl "https://api.donutstats.cc/v1/player/Steve" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/player/Steve", {
headers: { "x-api-key": "ds_live_your_key" }
})
const data = await res.json()
import requests
res = requests.get(
"https://api.donutstats.cc/v1/player/Steve",
headers={"x-api-key": "ds_live_your_key"},
)
data = res.json()
Response 200ShowHide
{
"ok": true,
"player": {
"name": "Steve",
"stats": {
"money": "12.4M",
"shards": "3.2K",
"kills": "412",
"deaths": "198",
"playtime": "21d 4h",
"blocks_placed": "88K",
"blocks_broken": "140K",
"mobs_killed": "9.1K"
},
"num": {
"money": 12400000,
"shards": 3200,
"kills": 412,
"deaths": 198,
"playtime": 1828800,
"blocks_placed": 88000,
"blocks_broken": 140000,
"mobs_killed": 9100
},
"updatedAt": 1794700000000,
"ageSec": 42,
"stale": false,
"online": true,
"lastSeen": 1794700000000,
"firstSeen": 1790000000000,
"views": 12,
"tracked": false,
"boards": { "money": 128 },
"ranks": {
"money": { "rank": 2210, "of": 68000 },
"shards": { "rank": 4120, "of": 68000 },
"kills": { "rank": 5120, "of": 61000 },
"deaths": { "rank": 9800, "of": 68000 },
"playtime": { "rank": 3300, "of": 68000 },
"blocks_placed": { "rank": 7400, "of": 68000 },
"blocks_broken": { "rank": 6100, "of": 68000 },
"mobs_killed": { "rank": 8800, "of": 68000 }
}
}
}Response fields
| Field | Type | Meaning |
|---|---|---|
name | string | The name as the game spells it. |
stats | object | Each stat as the game shows it, as text. |
num | object | The same stats as numbers. playtime is in seconds. |
updatedAt | integer | When these stats were last updated, in milliseconds. |
ageSec | integer | Seconds since updatedAt. |
stale | boolean | true when this is the saved copy, sent in place of new data. |
online | boolean or null | null when it is not known. |
lastSeen | integer or null | When the player was last seen online, in milliseconds. |
firstSeen | integer or null | When the player was first seen online, in milliseconds. |
views | integer | How often this player's page on DonutStats has been opened. |
tracked | boolean | Whether the player is tracked. |
boards | object | The player's place on the server's own top 200 lists. Only the boards they are on. |
ranks | object | The player's place among the players on DonutStats, per stat: rank out of of. |
Notes
- A stat that is not available is left out of
statsandnum. - Over your new-data limit you get the saved copy, with
stale: trueon the player andlimited: trueandretryAfterbesideok. See Rate limits.
Errors
400 bad_name 404 player_not_found 429 rate_limited 503 lookup_unavailable
Player history
GET/v1/player/{name}/history
Saved points for one player, oldest first. Made for charts.
Parameters
| Name | In | Type | Required | Meaning |
|---|---|---|---|---|
name | path | string | required | A Minecraft name: 2 to 16 letters, digits or underscores. Bedrock names start with a dot. Upper and lower case do not matter. |
days | query | integer | optional | How far back to go: 1 to 365. 30 when left out. |
Example
curl "https://api.donutstats.cc/v1/player/Steve/history?days=7" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/player/Steve/history?days=7", {
headers: { "x-api-key": "ds_live_your_key" }
})
const data = await res.json()
import requests
res = requests.get(
"https://api.donutstats.cc/v1/player/Steve/history?days=7",
headers={"x-api-key": "ds_live_your_key"},
)
data = res.json()
Response 200ShowHide
{
"ok": true,
"name": "Steve",
"days": 7,
"points": [
{
"at": 1794300000000,
"money": 11900000,
"shards": 3100,
"kills": 405,
"deaths": 196,
"playtime": 1790000,
"blocks_placed": 87000,
"blocks_broken": 139000,
"mobs_killed": 9000
},
{
"at": 1794700000000,
"money": 12400000,
"shards": 3200,
"kills": 412,
"deaths": 198,
"playtime": 1828800,
"blocks_placed": 88000,
"blocks_broken": 140000,
"mobs_killed": 9100
}
]
}Notes
- Each point has the time
atin milliseconds and every stat as a number. A stat that was not available isnull. - A point is stored when something changed, so a quiet player has few points.
- Older points are thinned out over time.
- A player with no history yet gets an empty
pointslist.
Errors
400 bad_name
Leaderboards
Get a leaderboard
GET/v1/board/{board}
One of the server's own top 200 lists.
Parameters
| Name | In | Type | Required | Meaning |
|---|---|---|---|---|
board | path | string | required | One of money, deaths, playtime, blocks_placed, blocks_broken, mobs_killed. |
limit | query | integer | optional | How many entries: 1 to 200. 50 when left out. |
offset | query | integer | optional | How many entries to skip: 0 to 200. 0 when left out. |
Example
curl "https://api.donutstats.cc/v1/board/money?limit=2" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/board/money?limit=2", {
headers: { "x-api-key": "ds_live_your_key" }
})
const data = await res.json()
import requests
res = requests.get(
"https://api.donutstats.cc/v1/board/money?limit=2",
headers={"x-api-key": "ds_live_your_key"},
)
data = res.json()
Response 200ShowHide
{
"ok": true,
"board": "money",
"total": 200,
"updatedAt": 1794700000000,
"offset": 0,
"entries": [
{
"rank": 1,
"name": "Steve",
"value": "5.7T",
"n": 5700000000000,
"at": 1794700000000,
"online": true
},
{
"rank": 2,
"name": "Alex",
"value": "4.9T",
"n": 4900000000000,
"at": 1794700000000,
"online": false
}
]
}Notes
valueis the text the game shows andnis the same as a number (seconds forplaytime).totalis how many entries the board has.updatedAtandatare times in milliseconds.onlineistrue,false, ornullwhen it is not known.
Errors
400 bad_board
List the leaderboards
GET/v1/boards
Every leaderboard, with how many entries it has and when it was last updated.
Example
curl "https://api.donutstats.cc/v1/boards" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/boards", {
headers: { "x-api-key": "ds_live_your_key" }
})
const data = await res.json()
import requests
res = requests.get(
"https://api.donutstats.cc/v1/boards",
headers={"x-api-key": "ds_live_your_key"},
)
data = res.json()
{
"ok": true,
"boards": [
{ "board": "blocks_broken", "entries": 200, "at": 1794700000000 },
{ "board": "blocks_placed", "entries": 200, "at": 1794700000000 },
{ "board": "deaths", "entries": 200, "at": 1794700000000 },
{ "board": "mobs_killed", "entries": 200, "at": 1794700000000 },
{ "board": "money", "entries": 200, "at": 1794700000000 },
{ "board": "playtime", "entries": 200, "at": 1794700000000 }
]
}Notes
atis a time in milliseconds. A board with no entries yet is not listed.
Top players by stat
GET/v1/top/{stat}
The players on DonutStats ranked by any stat. That covers kills and shards, which the game has no board for.
Parameters
| Name | In | Type | Required | Meaning |
|---|---|---|---|---|
stat | path | string | required | One of money, shards, kills, deaths, playtime, blocks_placed, blocks_broken, mobs_killed. |
limit | query | integer | optional | How many players: 1 to 100. 25 when left out. |
offset | query | integer | optional | How many players to skip. 0 when left out. |
Example
curl "https://api.donutstats.cc/v1/top/kills?limit=1" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/top/kills?limit=1", {
headers: { "x-api-key": "ds_live_your_key" }
})
const data = await res.json()
import requests
res = requests.get(
"https://api.donutstats.cc/v1/top/kills?limit=1",
headers={"x-api-key": "ds_live_your_key"},
)
data = res.json()
Response 200ShowHide
{
"ok": true,
"stat": "kills",
"total": 61000,
"offset": 0,
"players": [
{
"rank": 1,
"name": "Steve",
"stats": {
"money": "12.4M",
"shards": "3.2K",
"kills": "48K",
"deaths": "198",
"playtime": "21d 4h",
"blocks_placed": "88K",
"blocks_broken": "140K",
"mobs_killed": "9.1K"
},
"num": {
"money": 12400000,
"shards": 3200,
"kills": 48000,
"deaths": 198,
"playtime": 1828800,
"blocks_placed": 88000,
"blocks_broken": 140000,
"mobs_killed": 9100
},
"updatedAt": 1794700000000,
"ageSec": 310,
"stale": false,
"online": true
}
]
}Notes
totalis how many players have this stat.- Each player has the same
stats,num,updatedAt,ageSecandonlineas in Get a player, plusrank.
Errors
400 bad_stat
Server
Counts
GET/v1/counts
The totals: players on record, players online now and the 24-hour peak.
Example
curl "https://api.donutstats.cc/v1/counts" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/counts", {
headers: { "x-api-key": "ds_live_your_key" }
})
const data = await res.json()
import requests
res = requests.get(
"https://api.donutstats.cc/v1/counts",
headers={"x-api-key": "ds_live_your_key"},
)
data = res.json()
{
"ok": true,
"players": 68000,
"snapshots": 1250000,
"tracked": 420,
"views": 91000,
"online": true,
"onlineNow": 21500,
"onlineAt": 1794700000000,
"onlinePeak24h": 24100,
"known": 150000
}Response fields
| Field | Type | Meaning |
|---|---|---|
players | integer | Players on record. |
snapshots | integer | History points stored. |
tracked | integer | Players being tracked. |
views | integer | Player pages opened on DonutStats. |
online | boolean | false while new data cannot be fetched. |
onlineNow | integer or null | Players online on DonutSMP now. |
onlineAt | integer or null | When that count was taken, in milliseconds. |
onlinePeak24h | integer or null | The most players online in the last 24 hours. |
known | integer | Player names seen so far. |
Players online
GET/v1/online
The player count over time, oldest first.
Parameters
| Name | In | Type | Required | Meaning |
|---|---|---|---|---|
hours | query | integer | optional | How far back to go: 1 to 2160 (90 days). 24 when left out. |
Example
curl "https://api.donutstats.cc/v1/online?hours=24" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/online?hours=24", {
headers: { "x-api-key": "ds_live_your_key" }
})
const data = await res.json()
import requests
res = requests.get(
"https://api.donutstats.cc/v1/online?hours=24",
headers={"x-api-key": "ds_live_your_key"},
)
data = res.json()
{
"ok": true,
"hours": 24,
"now": 21500,
"at": 1794700000000,
"points": [
{ "at": 1794613600000, "players": 19800 },
{ "at": 1794613900000, "players": 19950 }
]
}Notes
nowis the latest count andatis when it was taken.- There is about one point every 5 minutes for the last week, and one an hour before that.
Search names
GET/v1/search
Players on record whose name starts with some text. Made for a search box.
Parameters
| Name | In | Type | Required | Meaning |
|---|---|---|---|---|
q | query | string | required | The start of a name: 1 to 16 letters, digits or underscores. |
limit | query | integer | optional | How many names: 1 to 20. 8 when left out. |
Example
curl "https://api.donutstats.cc/v1/search?q=ste&limit=3" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/search?q=ste&limit=3", {
headers: { "x-api-key": "ds_live_your_key" }
})
const data = await res.json()
import requests
res = requests.get(
"https://api.donutstats.cc/v1/search?q=ste&limit=3",
headers={"x-api-key": "ds_live_your_key"},
)
data = res.json()
{
"ok": true,
"names": [
{ "name": "Steve", "online": true },
{ "name": "Stella", "online": false },
{ "name": "Steel_Fox", "online": null }
]
}Notes
- Richest first.
- Text that cannot start a name gets an empty
nameslist, not an error.
Tracking
A tracked player is updated about every 30 minutes, so their history fills in.
Start tracking
POST/v1/track/{name}
Start tracking a player. No body is needed.
Parameters
| Name | In | Type | Required | Meaning |
|---|---|---|---|---|
name | path | string | required | A Minecraft name: 2 to 16 letters, digits or underscores. Bedrock names start with a dot. Upper and lower case do not matter. |
Example
curl -X POST "https://api.donutstats.cc/v1/track/Steve" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/track/Steve", {
method: "POST",
headers: { "x-api-key": "ds_live_your_key" }
})
const data = await res.json()
import requests
res = requests.post(
"https://api.donutstats.cc/v1/track/Steve",
headers={"x-api-key": "ds_live_your_key"},
)
data = res.json()
{ "ok": true, "tracked": true, "everySec": 1800 }Notes
- The player must be on record first: ask for them once with Get a player.
everySecis how often a tracked player is updated, in seconds.- Tracking a player who is already tracked changes nothing.
Errors
400 bad_name 404 player_not_found 409 tracking_full
Stop tracking
DELETE/v1/track/{name}
Stop tracking a player. Their history so far is kept.
Parameters
| Name | In | Type | Required | Meaning |
|---|---|---|---|---|
name | path | string | required | A Minecraft name: 2 to 16 letters, digits or underscores. Bedrock names start with a dot. Upper and lower case do not matter. |
Example
curl -X DELETE "https://api.donutstats.cc/v1/track/Steve" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/track/Steve", {
method: "DELETE",
headers: { "x-api-key": "ds_live_your_key" }
})
const data = await res.json()
import requests
res = requests.delete(
"https://api.donutstats.cc/v1/track/Steve",
headers={"x-api-key": "ds_live_your_key"},
)
data = res.json()
{ "ok": true, "tracked": false }Errors
400 bad_name
Tracking status
GET/v1/track/{name}
Whether a player is tracked.
Parameters
| Name | In | Type | Required | Meaning |
|---|---|---|---|---|
name | path | string | required | A Minecraft name: 2 to 16 letters, digits or underscores. Bedrock names start with a dot. Upper and lower case do not matter. |
Example
curl "https://api.donutstats.cc/v1/track/Steve" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/track/Steve", {
headers: { "x-api-key": "ds_live_your_key" }
})
const data = await res.json()
import requests
res = requests.get(
"https://api.donutstats.cc/v1/track/Steve",
headers={"x-api-key": "ds_live_your_key"},
)
data = res.json()
{ "ok": true, "tracked": true }Errors
400 bad_name
Account
Your limits
GET/v1/limits
Both limits of your key and how much of each is left this minute.
Example
curl "https://api.donutstats.cc/v1/limits" \
-H "x-api-key: ds_live_your_key"
const res = await fetch("https://api.donutstats.cc/v1/limits", {
headers: { "x-api-key": "ds_live_your_key" }
})
const data = await res.json()
import requests
res = requests.get(
"https://api.donutstats.cc/v1/limits",
headers={"x-api-key": "ds_live_your_key"},
)
data = res.json()
{
"ok": true,
"key": "my-app",
"freshLookups": { "limit": 30, "used": 3, "remaining": 27, "resetSec": 41 },
"requests": { "limit": 600, "used": 12, "remaining": 588, "resetSec": 41 },
"cache": { "playerSec": 600, "notFoundSec": 3600, "minMaxAge": 15 }
}Response fields
| Field | Type | Meaning |
|---|---|---|
key | string | The name of your key. |
freshLookups | object | Requests that needed new data this minute. |
requests | object | All requests this minute. |
limit, used, remaining | integer | The limit a minute, how many are used and how many are left. |
resetSec | integer | Seconds until the minute starts over. |
cache | object | How long a player is saved (playerSec), how long a name that does not exist is remembered (notFoundSec), and the smallest maxAge (minMaxAge), all in seconds. |
Notes
- The numbers above are examples. Your key has its own.
- This request counts as a request too.
Rate limits
Each key has two limits. Both are counted per minute and start over every minute.
| Limit | What counts | Over the limit |
|---|---|---|
| All requests | Every request you send. | 429 rate_limited |
| New data | A GET /v1/player/{name} that cannot be answered from a saved copy. | The saved copy, marked stale. With no saved copy, 429 rate_limited. |
The numbers are set per key. Your limits shows them and how much is left.
Headers
Answers to GET /v1/player/{name} carry these, for the new-data limit:
| Header | Meaning |
|---|---|
x-ratelimit-limit | Your limit a minute. |
x-ratelimit-remaining | How many are left this minute. |
x-ratelimit-reset | Seconds until the minute starts over. |
Over a limit
A 429 has retryAfter in seconds in the body and the same number in a Retry-After header. Wait that long, then try again.
HTTP/1.1 429 Too Many Requests
Retry-After: 12
{ "ok": false, "error": "rate_limited", "retryAfter": 12 }Over the new-data limit, a player with a saved copy still gets an answer. It is marked so you can tell:
{
"ok": true,
"player": { "name": "Steve", "stale": true, "ageSec": 1840 },
"limited": true,
"retryAfter": 12
}The player is shortened here. stale is also true when new data could not be fetched just now.
Caching
A player is saved for 10 minutes. Asking again within that time gets the saved copy and does not count towards the new-data limit.
- Add
?maxAge=60to ask for data no older than that many seconds. The least is 15. - A larger
maxAgeaccepts an older saved copy, which uses less of your new-data limit. updatedAtandageSecsay how old an answer is.- A name that does not exist is remembered for an hour.
- Leaderboards are updated about every 30 minutes. Their
updatedAtsays when.
curl "https://api.donutstats.cc/v1/player/Steve?maxAge=60" \
-H "x-api-key: ds_live_your_key"Errors
An error has ok: false and a code in error. Check the HTTP status first, then the code.
{ "ok": false, "error": "player_not_found" }| Status | Code | Meaning |
|---|---|---|
| 400 | bad_name | The name is not a valid Minecraft name. |
| 400 | bad_board | No such leaderboard. The answer lists the valid ones in boards. |
| 400 | bad_stat | No such stat. The answer lists the valid ones in stats. |
| 401 | invalid_api_key | The key is wrong or has been switched off. |
| 403 | origin_not_allowed | No key was sent. |
| 403 | blocked | This address has been blocked. |
| 404 | player_not_found | No such player on DonutSMP. When you start tracking: the player is not on record yet. |
| 404 | not_found | No such route. |
| 405 | method_not_allowed | The route does not take that method. |
| 409 | tracking_full | No more players can be tracked right now. |
| 429 | rate_limited | Over a limit. Try again after retryAfter seconds. |
| 500 | server_error | Something went wrong on our side. |
| 503 | lookup_unavailable | New data could not be fetched just now. Try again shortly. |
Data notes
- Values are rounded the way the game shows them.
"12.4M"means about 12,400,000, not an exact balance. statsholds text andnumholds the same values as numbers. The letters areKthousand,Mmillion,Bbillion,Ttrillion andQquadrillion.playtimeis text such as"21d 4h"instatsand seconds innum.- Times (
updatedAt,lastSeen,at) are Unix time in milliseconds. Lengths of time (ageSec,retryAfter,resetSec) are in seconds. - The stats are
money,shards,kills,deaths,playtime,blocks_placed,blocks_broken,mobs_killed. - Names are matched without regard to case, and the answer has the real spelling. Bedrock names start with a dot.
onlineistrue,false, ornullwhen it is not known.
Fair use
- Say where the data comes from when you show it publicly.
- Do not resell raw copies of it.
- Keep your key to yourself.
DonutStats is not affiliated with DonutSMP or Mojang AB. Questions go through the contact page.