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 except the health check requires the shared plugin API key, the same value as the plugin's api-key and the server's PLUGIN_API_KEY, sent as a bearer token. Requests without it are rejected.

The plugin also stamps each request with an x-traverse-plugin-version header. The server records it for the admin dashboard and the health check, and uses it to decide whether the plugin is version-compatible (same major.minor). It is informational, not an auth requirement.

One world per installation

The web app runs exactly one world. These endpoints always act on that world, so a worldId is no longer needed; older plugin builds that still send one are accepted and the value is ignored.

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 installation's world, its derived state, and the server version the plugin checks itself against.

{
"world": {"id": "...", "name": "Season 5", "state": "ACTIVE", "startAt": "...", "endAt": "..."},
"serverVersion": "1.1.0"
}

state is one of SCHEDULED, ACTIVE, ENDED, DISABLED. The world always exists, so there is no not-found case; a world that is off simply reports DISABLED.

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"],
"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.
400 INVALID_REQUESTThe body failed validation.

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, their entitlement id if they have one, and, when the campaign is inside its closing window and the player still has access without a claim, a closing reminder to show them.

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

With no claim but an entitlement inside the reminder window:

{"hasSelection": false, "entitlementId": "...", "closingReminder": {"closesAt": "..."}}

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

POST /api/plugin/player/:uuid/closing-reminder​

Marks the closing reminder as sent for the player's entitlement, so it is only ever shown once. The plugin calls this right after displaying the reminder. Returns 204, or 404 PLAYER_NOT_FOUND for an unknown player.

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.

Health check​

GET /api/health​

Unauthenticated, for Coolify or any uptime monitor. It returns 200 whenever the server and its database are reachable, and 503 only when the database probe fails.

{
"status": "ok",
"serverVersion": "1.1.0",
"database": "ok",
"world": { "state": "ACTIVE" },
"biomeIndex": { "state": "READY", "builtAt": "2026-10-10T09:00:00.000Z" },
"plugin": { "lastSeenAt": "2026-10-10T10:12:00.000Z", "version": "1.1.0", "compatible": true }
}

A disabled world, a missing biome index, or a quiet plugin are reported in the body (as null or a differing state), not as a failure, since none of them mean the container is broken. The failure body is {"status": "error", "database": "unreachable", "serverVersion": "..."}. No seed, coordinates, or player data are ever exposed.

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.