DonutAPI
A simple REST API for DonutSMP player stats and online status.
Behind the API, a pool of Minecraft accounts stays logged in on DonutSMP. Each request is handed to a free account which runs the matching in-game command, reads the result, and returns it as JSON — so you get live, real data straight from the server.
Base URL
Two endpoints, both GET and both returning JSON:
/v1/stats/:username— a player's stats (money, shards, kills, playtime…)/v1/activity/:username— whether a player is currently online
Authentication
Every request needs an API key, created on the dashboard. Send it in the Authorization header:
# any of these work curl -H "Authorization: dnt_your_key" BASEURL/v1/stats/Notch curl -H "Authorization: Bearer dnt_your_key" BASEURL/v1/stats/Notch curl -H "X-API-Key: dnt_your_key" BASEURL/v1/stats/Notch
A missing or invalid key returns 401 unauthorized. Each key can have an optional per-minute rate limit.
Usernames — Java & Bedrock
Use the plain username for a Java player. For a Bedrock player, prefix it with a dot (.) — that's how DonutSMP stores Geyser players.
| Player | Request |
|---|---|
| Java | /v1/stats/Notch |
| Bedrock | /v1/stats/.SomeGamertag |
The response echoes which one it detected in the platform field ("java" or "bedrock").
Player stats
Runs /stats <username> in-game, parses the stats menu, and returns the numbers.
Example request
curl -H "Authorization: dnt_your_key" BASEURL/v1/stats/Kettunen47
Example response 200 OK
{
"status": 200,
"result": {
"username": "Kettunen47",
"platform": "java",
"money": 6000000000,
"shards": 2900,
"kills": 17,
"deaths": 47,
"playtime": 2592000,
"placed_blocks": 142000,
"broken_blocks": 98000,
"mobs_killed": 5400,
"display": {
"money": "6B", "shards": "2900", "playtime": "30d"
}
},
"cached": false,
"worker": "worker12",
"took_ms": 359
}
Result fields
| Field | Type | Notes |
|---|---|---|
username | string | as requested |
platform | string | "java" or "bedrock" |
money | number | exact balance, e.g. 6000000000 |
shards | number | |
kills / deaths | number | |
playtime | number | seconds (e.g. 2592000 = 30 days) |
placed_blocks | number | |
broken_blocks | number | |
mobs_killed | number | |
display | object | the same values exactly as shown in-game ("6B", "30d") |
display object keeps the short in-game strings if you'd rather show those. Add ?raw=1 to also get fields — every item in the menu — for debugging.Player activity (online check)
Whispers the player in-game and reads the server's reply to decide whether they're online. Returns fast (~200 ms).
Example request
curl -H "Authorization: dnt_your_key" BASEURL/v1/activity/Kettunen47
Online 200 OK
{
"status": 200,
"result": {
"username": "Kettunen47",
"platform": "java",
"online": true,
"status": "online",
"reason": "message delivered"
},
"cached": false,
"worker": "worker7",
"took_ms": 206
}
A player who is online but only accepts messages from friends, or has PMs disabled, still returns online: true — the server only gives those replies for players who are actually online.
Offline 200 OK
{
"status": 200,
"result": {
"username": "SomePlayer",
"platform": "java",
"online": false,
"status": "offline",
"reason": "not online"
},
"cached": false,
"worker": "worker3",
"took_ms": 190
}
Result fields
| Field | Type | Notes |
|---|---|---|
online | boolean | the quick answer — true / false |
status | string | "online", "offline", or "not_found" (never joined) |
reason | string | a short human-readable reason |
detail | string | the exact server line, if you add ?raw=1 |
online: false with status: "not_found". The server can't always tell "offline" from "never joined", so treat online as the reliable field.Query options
| Param | Effect |
|---|---|
?fresh=1 | skip the cache and force a live lookup |
?raw=1 | include extra detail — fields on stats, detail/chat on activity |
curl -H "Authorization: dnt_your_key" "BASEURL/v1/stats/Notch?fresh=1&raw=1"
Caching
Identical lookups are served from a short in-memory cache, and identical requests already in flight are de-duplicated onto one in-game command. The cached field tells you whether a response came from cache. Use ?fresh=1 to bypass it. Typical windows: stats a few seconds, activity even shorter (it changes fast).
Rate limits
Each API key can have an optional requests-per-minute limit (0 = unlimited). When exceeded you get 429 with a Retry-After header:
{ "status": 429, "error": "rate_limited", "message": "rate limit of 120/min exceeded" }
Errors
Errors use the shape { status, error, message }. The error code is stable; the message is for humans.
| HTTP | error | Meaning |
|---|---|---|
| 400 | bad_request | invalid username (not a Java name or .Bedrock name) |
| 401 | unauthorized | missing / invalid / disabled API key |
| 404 | not_found | no such player (stats only) |
| 429 | rate_limited | per-key limit exceeded — see Retry-After |
| 502 | unparsed | the menu opened but couldn't be read (rare) |
| 503 | no_workers / queue_timeout | no accounts online, or all busy too long |
| 504 | timeout | the in-game command didn't answer in time |
{ "status": 404, "error": "not_found", "message": "User does not exist" }
Manage workers, API keys and proxies on the dashboard. Health check: GET /health.