DonutStats

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-key header with every request.
Format
JSON over HTTPS. Every answer has an ok field.

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:

Response 200
{
  "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:

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 gets 403 origin_not_allowed.

Players

Get a player

GET/v1/player/{name}

One player: every stat, whether they are online, and where they rank.

Parameters

NameInTypeRequiredMeaning
namepathstringrequiredA Minecraft name: 2 to 16 letters, digits or underscores. Bedrock names start with a dot. Upper and lower case do not matter.
maxAgequeryintegeroptionalAccept 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

FieldTypeMeaning
namestringThe name as the game spells it.
statsobjectEach stat as the game shows it, as text.
numobjectThe same stats as numbers. playtime is in seconds.
updatedAtintegerWhen these stats were last updated, in milliseconds.
ageSecintegerSeconds since updatedAt.
stalebooleantrue when this is the saved copy, sent in place of new data.
onlineboolean or nullnull when it is not known.
lastSeeninteger or nullWhen the player was last seen online, in milliseconds.
firstSeeninteger or nullWhen the player was first seen online, in milliseconds.
viewsintegerHow often this player's page on DonutStats has been opened.
trackedbooleanWhether the player is tracked.
boardsobjectThe player's place on the server's own top 200 lists. Only the boards they are on.
ranksobjectThe 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 stats and num.
  • Over your new-data limit you get the saved copy, with stale: true on the player and limited: true and retryAfter beside ok. 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

NameInTypeRequiredMeaning
namepathstringrequiredA Minecraft name: 2 to 16 letters, digits or underscores. Bedrock names start with a dot. Upper and lower case do not matter.
daysqueryintegeroptionalHow 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 at in milliseconds and every stat as a number. A stat that was not available is null.
  • 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 points list.

Errors

400 bad_name

Leaderboards

Get a leaderboard

GET/v1/board/{board}

One of the server's own top 200 lists.

Parameters

NameInTypeRequiredMeaning
boardpathstringrequiredOne of money, deaths, playtime, blocks_placed, blocks_broken, mobs_killed.
limitqueryintegeroptionalHow many entries: 1 to 200. 50 when left out.
offsetqueryintegeroptionalHow 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

  • value is the text the game shows and n is the same as a number (seconds for playtime).
  • total is how many entries the board has. updatedAt and at are times in milliseconds.
  • online is true, false, or null when 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()
Response 200
{
  "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

  • at is 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

NameInTypeRequiredMeaning
statpathstringrequiredOne of money, shards, kills, deaths, playtime, blocks_placed, blocks_broken, mobs_killed.
limitqueryintegeroptionalHow many players: 1 to 100. 25 when left out.
offsetqueryintegeroptionalHow 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

  • total is how many players have this stat.
  • Each player has the same stats, num, updatedAt, ageSec and online as in Get a player, plus rank.

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()
Response 200
{
  "ok": true,
  "players": 68000,
  "snapshots": 1250000,
  "tracked": 420,
  "views": 91000,
  "online": true,
  "onlineNow": 21500,
  "onlineAt": 1794700000000,
  "onlinePeak24h": 24100,
  "known": 150000
}

Response fields

FieldTypeMeaning
playersintegerPlayers on record.
snapshotsintegerHistory points stored.
trackedintegerPlayers being tracked.
viewsintegerPlayer pages opened on DonutStats.
onlinebooleanfalse while new data cannot be fetched.
onlineNowinteger or nullPlayers online on DonutSMP now.
onlineAtinteger or nullWhen that count was taken, in milliseconds.
onlinePeak24hinteger or nullThe most players online in the last 24 hours.
knownintegerPlayer names seen so far.

Players online

GET/v1/online

The player count over time, oldest first.

Parameters

NameInTypeRequiredMeaning
hoursqueryintegeroptionalHow 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()
Response 200
{
  "ok": true,
  "hours": 24,
  "now": 21500,
  "at": 1794700000000,
  "points": [
    { "at": 1794613600000, "players": 19800 },
    { "at": 1794613900000, "players": 19950 }
  ]
}

Notes

  • now is the latest count and at is when it was taken.
  • There is about one point every 5 minutes for the last week, and one an hour before that.

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

NameInTypeRequiredMeaning
namepathstringrequiredA 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()
Response 200
{ "ok": true, "tracked": true, "everySec": 1800 }

Notes

  • The player must be on record first: ask for them once with Get a player.
  • everySec is 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

NameInTypeRequiredMeaning
namepathstringrequiredA 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()
Response 200
{ "ok": true, "tracked": false }

Errors

400 bad_name

Tracking status

GET/v1/track/{name}

Whether a player is tracked.

Parameters

NameInTypeRequiredMeaning
namepathstringrequiredA 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()
Response 200
{ "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()
Response 200
{
  "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

FieldTypeMeaning
keystringThe name of your key.
freshLookupsobjectRequests that needed new data this minute.
requestsobjectAll requests this minute.
limit, used, remainingintegerThe limit a minute, how many are used and how many are left.
resetSecintegerSeconds until the minute starts over.
cacheobjectHow 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.

LimitWhat countsOver the limit
All requestsEvery request you send.429 rate_limited
New dataA 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:

HeaderMeaning
x-ratelimit-limitYour limit a minute.
x-ratelimit-remainingHow many are left this minute.
x-ratelimit-resetSeconds 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.

Response 429
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:

Response 200
{
  "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=60 to ask for data no older than that many seconds. The least is 15.
  • A larger maxAge accepts an older saved copy, which uses less of your new-data limit.
  • updatedAt and ageSec say how old an answer is.
  • A name that does not exist is remembered for an hour.
  • Leaderboards are updated about every 30 minutes. Their updatedAt says when.
Request
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.

Response 404
{ "ok": false, "error": "player_not_found" }
StatusCodeMeaning
400bad_nameThe name is not a valid Minecraft name.
400bad_boardNo such leaderboard. The answer lists the valid ones in boards.
400bad_statNo such stat. The answer lists the valid ones in stats.
401invalid_api_keyThe key is wrong or has been switched off.
403origin_not_allowedNo key was sent.
403blockedThis address has been blocked.
404player_not_foundNo such player on DonutSMP. When you start tracking: the player is not on record yet.
404not_foundNo such route.
405method_not_allowedThe route does not take that method.
409tracking_fullNo more players can be tracked right now.
429rate_limitedOver a limit. Try again after retryAfter seconds.
500server_errorSomething went wrong on our side.
503lookup_unavailableNew 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.
  • stats holds text and num holds the same values as numbers. The letters are K thousand, M million, B billion, T trillion and Q quadrillion.
  • playtime is text such as "21d 4h" in stats and seconds in num.
  • 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.
  • online is true, false, or null when 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.