Skip to content

Extensions

Pavel Erokhin edited this page Oct 19, 2026 · 1 revision

Your plugin can share its data through WorldAPI without depending on it: no library to add, nothing of WorldAPI's on your classpath. Your plugin stays the source of truth. WorldAPI reads your resources when a client asks and keeps none of them; it records only the events you mark recorded, in the event history like its own.

Clients then read it at /api/v1/extensions and subscribe to your topics. Mo’Bosses works this way.

1. Load after WorldAPI

Declare WorldAPI as an optional dependency that loads before your plugin, in paper-plugin.yml:

dependencies:
  server:
    WorldAPI:
      load: BEFORE
      required: false

Register in onEnable. If WorldAPI starts later, listen for ServiceRegisterEvent and register then.

2. Find the bridge

WorldAPI registers a java.util.function.Function<Map<String, Any?>, Any?> in Bukkit's ServicesManager. Find the one whose plugin is WorldAPI:

@Suppress("UNCHECKED_CAST")
val bridge = Bukkit.getServicesManager().getRegistrations(java.util.function.Function::class.java)
    .firstOrNull { it.plugin.name == "WorldAPI" }?.provider as? java.util.function.Function<Map<String, Any?>, Any?>

Send it requests as maps of plain Java values. Every request carries v, the contract version (1 now). WorldAPI refuses a version newer than it knows, so check the answer's contract if you rely on something new. Answers are maps: ok, contract, version. A bad request throws IllegalArgumentException.

3. Register resources and events

bridge?.apply(mapOf(
    "v" to 1, "op" to "register", "plugin" to this, "namespace" to "mobosses", "description" to "Bosses and their hunters",
    "resources" to listOf(
        mapOf("path" to "hunters", "description" to "The best hunters", "public" to true,
            "get" to java.util.function.Function<Map<String, String>, Any?> { topHunters() }),
        mapOf("path" to "players/{player}", "description" to "A player's card", "public" to true,
            "get" to java.util.function.Function<Map<String, String>, Any?> { card(it.getValue("player")) }),
        mapOf("path" to "bosses", "description" to "Bosses alive, with coordinates", "mainThread" to true,
            "get" to java.util.function.Function<Map<String, String>, Any?> { liveBosses() }),
    ),
    "events" to listOf(
        mapOf("name" to "boss.killed", "description" to "Players killed a boss", "public" to true, "recorded" to true),
    ),
))

4. Publish events

bridge?.apply(mapOf("v" to 1, "op" to "publish", "namespace" to "mobosses", "event" to "boss.killed",
    "data" to mapOf("type" to "zombie", "level" to 40), "player" to player.uniqueId, "playerName" to player.name, "world" to world.name))

Ops

Op Fields
register plugin (your plugin), namespace (1 to 32 of a-z 0-9 _ -), description, resources, events. Registering again replaces the namespace; another plugin can't take it
publish namespace, event, data (a map), and optionally player (a UUID), playerName, world. Safe from any thread; it reaches clients from the server thread
unregister namespace. WorldAPI also drops a plugin's namespaces when it stops

Resources

  • path is segments of a-z 0-9 _ -, or {name} for a parameter.
  • get takes the path parameters and the query parameters, as strings, and returns plain Java values: maps, lists, arrays, strings, numbers, booleans, and null. UUIDs, enums, and times turn into strings; anything else into its toString().
  • get may return a CompletableFuture of those too, which WorldAPI waits up to 10 seconds for.
  • get runs on a worker thread, so it must be thread-safe, unless the resource sets mainThread: true.
  • Throw IllegalArgumentException for 400, NoSuchElementException for 404.

Events

Each becomes the topic ext.<namespace>.<name>, on the WebSocket and, when recorded, in GET /events. Their data loses location, position, and the other coordinate keys for visitors unless privacy.anonymous.coordinates is on, as with WorldAPI's own topics.

Privacy

Everything a plugin shares needs a token, unless the plugin marks it public. Then visitors may read it too, unless the server's extensions.private lists its namespace (mobosses), its resource (mobosses/bosses), or its topic (ext.mobosses.boss.spawned). Visitors reach these through /extensions and /extensions/** in auth.anonymous.endpoints, which the default config has.

Reading it as a client

Endpoint Permission What it does
GET /extensions server:read Other plugins’ data: their namespaces, resources, and event topics
GET /extensions/{namespace}/{path} server:read One resource of a plugin, read from the plugin as it is now
  • GET /extensions lists the namespaces, with their resources and topics, that the client may see.
  • GET /extensions/{namespace}/{path...} reads one resource, like /extensions/mobosses/players/Steve, straight from the plugin.
  • Subscribe to ext.mobosses.boss.killed like any topic; GET /events?type=ext.mobosses.boss.killed returns the recorded ones.

Next: Java scripts.

Clone this wiki locally