Public HTTP API¶
DonutSMPCore ships a built-in public API HTTP server (net.opmasterleo.serverapi) for official DonutSMP-compatible /v1/... routes.
This is separate from MinecraftServerAPI (default port 7000), which you can keep for private/admin endpoints (LuckPerms, server exec, etc.).
DonutSMPCore provides:
- In-game
/api— players get a persistent API token - Built-in HTTP server — official
/v1/...routes on port 7001 by default DonutSMPPublicApi— Java API for the same data- Webhooks — auction/order events to configured URLs
PublicApiResponses— exact JSON bodies from the official spec
Architecture¶
Player ──► /api ──► DonutSMPCore (API key in DB)
│
├── net.opmasterleo.serverapi (port 7001)
│ └── GET/POST /v1/...
│
External client ──► http://host:7001/v1/stats/player
│
└── Authorization: Bearer <token>
MinecraftServerAPI (port 7000) ──► private/admin routes only (unchanged)
Player API keys (/api)¶
| Behavior | Detail |
|---|---|
| Command | /api |
| Reset | /api reset — deletes your key if leaked; the old token stops working immediately. Run /api again to get a new one. |
| Cooldown | None — players can run it anytime |
| Token format | 32-char hex (UUID without dashes), e.g. f3c81d9a72be4e65a0d7c9185fb24e3a |
| Persistence | One token per player UUID; re-running /api shows the same token |
| Chat message | Your API Token is: <token> |
| Hover | Green: Click to copy your API Key to the clipboard |
| Click | Copies token to clipboard |
HTTP clients authenticate with:
Rate limit: 250 requests / minute / API key — call api.allowRequest(token) before serving data.
Java integration¶
Dependency¶
Entry points¶
import net.donutsmp.donutcore.api.DonutSMPApi;
import net.donutsmp.donutcore.api.publicapi.DonutSMPPublicApi;
import net.donutsmp.donutcore.api.publicapi.PublicApiResponses;
import com.google.gson.JsonObject;
DonutSMPPublicApi api = DonutSMPApi.publicApi().orElseThrow();
Authentication (per HTTP request)¶
String token = extractBearerToken(request); // raw token, no "Bearer " prefix
if (api.validateKey(token).isEmpty()) {
return http(401, PublicApiResponses.unauthorized());
}
if (!api.allowRequest(token)) {
// Official spec has no 429 body — return 401 or 500 as your proxy prefers
return http(401, PublicApiResponses.unauthorized());
}
JsonObject body = api.stats(username);
return http(200, body);
Use PublicApiResponses for error bodies so they match the official API exactly.
Endpoint mapping¶
| Official HTTP route | DonutSMPPublicApi method |
|---|---|
GET /v1/leaderboards/brokenblocks/{page} |
leaderboard("brokenblocks", page) |
GET /v1/leaderboards/deaths/{page} |
leaderboard("deaths", page) |
GET /v1/leaderboards/kills/{page} |
leaderboard("kills", page) |
GET /v1/leaderboards/mobskilled/{page} |
leaderboard("mobskilled", page) |
GET /v1/leaderboards/money/{page} |
leaderboard("money", page) |
GET /v1/leaderboards/placedblocks/{page} |
leaderboard("placedblocks", page) |
GET /v1/leaderboards/playtime/{page} |
leaderboard("playtime", page) |
GET /v1/leaderboards/sell/{page} |
leaderboard("sell", page) |
GET /v1/leaderboards/shards/{page} |
leaderboard("shards", page) |
GET /v1/leaderboards/shop/{page} |
leaderboard("shop", page) |
GET /v1/lookup/{user} |
lookup(user) |
GET /v1/stats/{user} |
stats(user) |
GET /v1/auction/list/{page} |
auctionList(page, search, sort) |
GET /v1/auction/transactions/{page} |
auctionTransactions(page) |
Exact response bodies (official spec)¶
HTTP 200 — success¶
JSON status field is 0 (not 200). HTTP status code is still 200.
Stats (api.StatsResponse):
{
"result": {
"broken_blocks": "string",
"deaths": "string",
"kills": "string",
"mobs_killed": "string",
"money": "string",
"money_made_from_sell": "string",
"money_spent_on_shop": "string",
"placed_blocks": "string",
"playtime": "string",
"shards": "string"
},
"status": 0
}
Leaderboards (api.LeaderboardResponse):
Lookup (api.LookupResponse):
Auction list (api.AhResponse):
{
"result": [
{
"item": { "...": "api.Item" },
"price": 0.0,
"seller": { "name": "string", "uuid": "string" },
"time_left": 0
}
],
"status": 0
}
Auction transactions (api.TransactionHistoryResponse):
{
"result": [
{
"item": { "...": "api.Item" },
"price": 0.0,
"seller": { "name": "string", "uuid": "string" },
"unixMillisDateSold": 0
}
],
"status": 0
}
Built by DonutSMPCore:
HTTP 401 — unauthorized¶
{
"message": "Please generate an API Key in game with /api",
"reason": "Unauthorized",
"status": 401
}
HTTP 500 — could not handle query¶
{
"message": "Could not handle your request. This may be because the specified user/page/item does not exist.",
"reason": "Error handling request",
"status": 500
}
Returned automatically when data lookup fails (unknown player, bad page, disabled module):
Data methods (stats, lookup, etc.) return serverError() on failure instead of throwing.
Swagger models¶
| Model | Java builder |
|---|---|
api.Stats |
PublicApiDataService.stats() |
api.StatsResponse |
PublicApiResponses.success(stats) |
api.Leaderboard |
PublicApiResponses.leaderboardEntry(...) |
api.LeaderboardResponse |
PublicApiResponses.success(array) |
api.Lookup |
PublicApiDataService.lookup() |
api.LookupResponse |
PublicApiResponses.success(lookup) |
api.Seller |
PublicApiResponses.seller(name, uuid) |
api.Ah |
PublicApiResponses.auctionListing(item, price, seller, timeLeft) |
api.AhResponse |
PublicApiResponses.success(array) |
api.PurchaseItem |
PublicApiResponses.purchaseItem(item, price, seller, millis) |
api.TransactionHistoryResponse |
PublicApiResponses.success(array) |
api.Item |
ApiItemSerializer.toJson(itemStack) |
api.ContainerItem |
ApiItemSerializer.toContainerItem(itemStack) |
api.ItemData |
nested under item.enchants / container.enchants |
api.Enchantments |
item.enchants.enchantments with levels map (minecraft:enchant: level) |
api.Trim |
item.enchants.trim with material, pattern (armor trim) |
main.NoAuth |
PublicApiResponses.unauthorized() |
api.InvalidResponse |
PublicApiResponses.serverError() |
ah.RequestBody |
optional JSON body on auction list (search, sort) |
api.Item fields¶
| Field | Type | Notes |
|---|---|---|
id |
string | Namespaced material id, e.g. minecraft:diamond |
count |
integer | Stack size |
display_name |
string | Omitted if default |
lore |
string[] | Worth/price lore stripped |
enchants |
ItemData | See below |
contents |
ContainerItem[] | Shulker contents only |
api.ContainerItem fields¶
| Field | Type |
|---|---|
id |
string |
count |
integer |
display_name |
string |
enchants |
ItemData |
No lore on container items (per spec).
api.ItemData (JSON field name enchants on items)¶
{
"enchantments": {
"levels": {
"minecraft:sharpness": 5
}
},
"trim": {
"material": "minecraft:gold",
"pattern": "minecraft:vex"
}
}
Both enchantments and trim are omitted when absent.
Auction list request body¶
Optional JSON body on GET /v1/auction/list/{page}:
Sort values: lowest_price, highest_price, recently_listed, last_listed
Not implemented in DonutSMPCore¶
None — all routes in doc.json are implemented by DonutSMPCore (shield routes are excluded from the spec).
Configuration (plugins/DonutSMPCore/publicapi/config.yml)¶
| Key | Default | Description |
|---|---|---|
rate-limit.requests-per-minute |
250 |
Per API key |
pagination.leaderboard-page-size |
10 |
Entries per leaderboard page |
pagination.auction-list-page-size |
45 |
Auction listings per page |
pagination.auction-transactions-page-size |
100 |
Transactions per page (max page 10) |
http.enabled |
true |
Start built-in public API HTTP server |
http.port |
7001 |
Port (use 7001 so MinecraftServerAPI can keep 7000) |
http.swagger |
true |
Swagger UI at / and OpenAPI spec at /doc.json |
webhooks.enabled |
true |
Emit DonutSMP webhook events |
webhooks.urls |
[] |
POST targets (Discord, automation, etc.) |
webhooks.only-message |
false |
Send only message field as plain text when present |
webhooks.events.* |
true |
Per-event toggles |
Built-in HTTP server (net.opmasterleo.serverapi)¶
Package layout (MSAPI-style):
net.opmasterleo.serverapi/
ServerApi.java # lifecycle (start/stop/reload)
http/
ApiWebServer.java # NanoHTTPD
RouteDefinition.java
endpoints/
RegisterEndpoints.java
v1/
LeaderboardsEndpoint.java
LookupEndpoint.java
StatsEndpoint.java
AuctionEndpoint.java
webhook/
WebhookEvent.java
WebhookDispatcher.java
HTTP routes¶
| Pattern | Methods |
|---|---|
/v1/leaderboards/brokenblocks/{page} |
GET |
/v1/leaderboards/deaths/{page} |
GET |
/v1/leaderboards/kills/{page} |
GET |
/v1/leaderboards/mobskilled/{page} |
GET |
/v1/leaderboards/money/{page} |
GET |
/v1/leaderboards/placedblocks/{page} |
GET |
/v1/leaderboards/playtime/{page} |
GET |
/v1/leaderboards/sell/{page} |
GET |
/v1/leaderboards/shards/{page} |
GET |
/v1/leaderboards/shop/{page} |
GET |
/v1/lookup/{user} |
GET |
/v1/stats/{user} |
GET |
/v1/auction/list/{page} |
GET, POST |
/v1/auction/transactions/{page} |
GET |
Auth: Authorization: Bearer <token> from in-game /api.
Webhook events¶
| Event ID | When emitted |
|---|---|
donutsmp.auction.listing_created |
Auction listing placed |
donutsmp.auction.purchase |
Auction item purchased |
donutsmp.order.placed |
Buy order created |
donutsmp.order.filled |
Buy order delivery |
donutsmp.order.cancelled |
Buy order cancelled |
Payload envelope:
Emit from other code:
import net.opmasterleo.serverapi.webhook.WebhookEvent;
DonutSMPApi.publicApiWebhooks().ifPresent(pub ->
pub.publish(WebhookEvent.ORDER_PLACED, data));
Required modules¶
Modules:
publicapi:
enabled: true
stats:
enabled: true
auction:
enabled: true # auction endpoints
shards:
enabled: true # shards leaderboard/stats
shop:
enabled: true # shop spend stats
worth:
enabled: true # sell stats/leaderboard
Maintenance mode¶
DonutSMPApi.publicApi().ifPresent(api -> {
boolean maintenance = api.isMaintenanceEnabled();
api.toggleMaintenance();
api.addMaintenanceBypass(uuid);
api.removeMaintenanceBypass(uuid);
int bypassCount = api.getMaintenanceBypassCount();
});
Reference¶
- Official spec: https://api.donutsmp.net/index.html
- In-process plugin API (non-HTTP): Java API