Skip to content

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:

  1. In-game /api — players get a persistent API token
  2. Built-in HTTP server — official /v1/... routes on port 7001 by default
  3. DonutSMPPublicApi — Java API for the same data
  4. Webhooks — auction/order events to configured URLs
  5. 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:

Authorization: Bearer f3c81d9a72be4e65a0d7c9185fb24e3a

Rate limit: 250 requests / minute / API key — call api.allowRequest(token) before serving data.


Java integration

Dependency

# plugin.yml
depend: [DonutSMPCore]

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):

{
  "result": [
    {
      "username": "string",
      "uuid": "string",
      "value": "string"
    }
  ],
  "status": 0
}

Lookup (api.LookupResponse):

{
  "result": {
    "location": "string",
    "rank": "string",
    "username": "string"
  },
  "status": 0
}

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:

PublicApiResponses.success(resultElement); // sets status = 0

HTTP 401 — unauthorized

{
  "message": "Please generate an API Key in game with /api",
  "reason": "Unauthorized",
  "status": 401
}
PublicApiResponses.unauthorized();

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):

PublicApiResponses.serverError();

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}:

{
  "search": "diamond",
  "sort": "lowest_price"
}

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:

{
  "event": "donutsmp.auction.purchase",
  "timestamp": 1710000000000,
  "data": { }
}

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