Skip to main content

Plugin API Reference

These are the endpoints the Paper plugin uses. They are documented for anyone building an alternative client, a bot, or a second server integration. Everything else in the API is either supporter-session or admin-session scoped and is not covered here.

Authentication

Every endpoint on this page requires the shared plugin API key, the same value as the plugin's api-key and the server's PLUGIN_API_KEY. Requests without it are rejected.

:::caution This key can issue access links for any entitlement Anyone holding it can mint a link that claims land as any supporter. Treat it like a database password: keep it out of version control, do not expose the API to the open internet without need, and rotate it on both sides together. :::

Endpoints

GET /api/plugin/world

Returns the most recently created enabled world and its derived state.

{"world": {"id": "...", "name": "Season 5", "state": "ACTIVE"}}

state is one of SCHEDULED, ACTIVE, ENDED, DISABLED. Returns 404 NO_WORLD when no world is enabled.

POST /api/plugin/access

Creates the player and entitlement if the player qualifies, and returns a fresh 24-hour access code.

{
"minecraftUuid": "...",
"username": "Steve",
"permissions": ["traverse.supporter"],
"groups": ["supporter"],
"worldId": "...",
"isOp": false
}
ResponseMeaning
200 {"code": "..."}Qualified. Any previously active code for this entitlement is revoked.
403 NOT_ELIGIBLEDoes not match the world's qualification rule, and is not an operator.
409 WORLD_NOT_ACTIVEThe world is scheduled, ended, or disabled.
404 WORLD_NOT_FOUNDUnknown worldId.

isOp: true bypasses the qualification check.

POST /api/plugin/access-token

Issues a single-use, 15-minute access token and the link to hand the player.

{"entitlementId": "..."}
{"token": "...", "url": "https://.../access/<token>", "expiresAt": "..."}

The URL is built from the server's PUBLIC_BASE_URL, which is why it must match the plugin's base-url.

GET /api/plugin/player/:uuid/status

Reports whether the player has an active claim, and, when worldId is supplied as a query parameter, their entitlement id for that world.

{"hasSelection": true, "centerX": 1234, "centerZ": -5678, "radius": 75, "entitlementId": "..."}

An unknown player returns {"hasSelection": false} rather than a 404.

POST /api/plugin/entitlement/:entitlementId/access-code

Issues a fresh 24-hour access code for an entitlement that already exists, revoking any earlier active one.

GET /api/plugin/onboarding/:uuid?worldId=...

Asks whether the join dialog should be shown. Returns {"shouldShow": false} when the player has no entitlement for that world, has already been shown the dialog, or the world has no dialogTitle and dialogMessage configured. Otherwise it returns the dialog copy and a ready-to-use access URL.

POST /api/plugin/onboarding/:uuid/dismiss

Marks the dialog as shown so it never appears again for that entitlement. Body is {"worldId": "..."}. Returns 204.

Supporter-facing endpoint

GET /access/:token

Not part of the plugin API and not authenticated by the key: this is the link a player clicks. It redeems the token, sets the supporter session cookie, and redirects into the app. A failure redirects with ?accessError=INVALID|EXPIRED|USED, which the app surfaces once and then strips from the URL.

The token segment is redacted from request logs, so an unused token cannot be read out of a log or proxy trail and replayed.