Skip to content

How to add license keys to a Paper plugin ​

Short answer: add the SunLicense Java SDK, read the buyer's key from config.yml, check it off the main thread in onEnable, and disable the plugin when SunLicense says no. A network error is not a no: keep running and check again later.

java
// build.gradle.kts: implementation("com.sunlicense:sunlicense:0.0.1"), shaded and relocated into your jar
private final SunLicense client = new SunLicense.Builder()
        .baseUrl("https://api.sunlicense.com")
        .token("slk_…")   // your API key (it can only validate); put it in at build time, never in git
        .build();

@Override
public void onEnable() {
    saveDefaultConfig();
    String key = getConfig().getString("license-key", "");
    if (key.isBlank()) { getLogger().severe("Put your license key in config.yml"); getServer().getPluginManager().disablePlugin(this); return; }
    getServer().getScheduler().runTaskAsynchronously(this, () -> {
        try {
            SunLicense.Result r = client.validate(key, Map.of("host", machineId()));
            if (r.getStatus() == 400) {   // EXPIRED, REVOKED, TOO_MANY_MACHINES…: the body's "code" says which
                getLogger().severe("License rejected: " + r.getBody());
                getServer().getScheduler().runTask(this, () -> getServer().getPluginManager().disablePlugin(this));
            }
        } catch (SunLicense.SunLicenseException e) {
            getLogger().warning("SunLicense can't be reached, running anyway: " + e.getMessage());
        }
    });
}

Start from the template instead: example-paper-plugin (click Use this template) has all of this plus a stable machine id, masked keys, a re-check every 6 hours, a signed offline license fallback, BuiltByBit placeholders, a sealed core, tests and CI. Template guide.

Or let your AI agent do it: paste the setup prompt into Claude Code, Cursor or Codex in your plugin's folder. It adds the check and proves it with a test license.

The Free plan is enough to ship: 3 products, 100 licenses, no credit card. Create a free account.

Step by step ​

  1. In the dashboard (app.sunlicense.com): Products → New product for your plugin, then a policy, for example "Pro" with 2 machines (a machine is a server here), and an API key (API keys → Create API key). The API key can only validate licenses, so it is safe inside the jar.

  2. Add the SDK and relocate it with Shadow, so two plugins with different SDK versions never clash on a server:

    kotlin
    dependencies { implementation("com.sunlicense:sunlicense:0.0.1") }
    tasks.shadowJar { relocate("com.sunlicense", "com.example.myplugin.libs.sunlicense") }
  3. Keep the API key out of git. Put it in your user Gradle properties (~/.gradle/gradle.properties: sunlicenseApiKey=slk_…) and let processResources write it into a properties file in the jar, as the template does.

  4. Get the buyer's key from config.yml (license-key: ""). If you sell on BuiltByBit or Voxel Shop, a %%__SUNLICENSE__%% placeholder in your code is replaced with each buyer's own key on download, so there is nothing for them to paste.

  5. Send a stable machine id as host, so the policy's machine limit counts servers, not restarts. A hash of the OS, architecture and host name works; the template's SunLicenseProtection.machineId() adds the MAC addresses. Don't use the IP address. Choosing a machine id.

    java
    static String machineId() {
        try {
            String raw = System.getProperty("os.name") + "|" + System.getProperty("os.arch") + "|"
                    + java.net.InetAddress.getLocalHost().getHostName();
            return "paper-" + UUID.nameUUIDFromBytes(raw.getBytes(StandardCharsets.UTF_8));
        } catch (java.io.IOException e) {
            return "paper-unknown";
        }
    }
  6. Check off the main thread (runTaskAsynchronously) and touch the server only back on the main thread (runTask). A network call on the main thread would freeze the server until SunLicense answers.

  7. Handle each answer. 200: start your features. 400: log the reason (result codes) and disable the plugin; mask the key in logs (AX8P-XXXX-XXXX-XXXX). Anything else: keep running and retry in about 10 minutes, or use an offline license file if the buyer has one.

  8. Re-check every few hours (runTaskTimerAsynchronously), so revoking a leaked key reaches servers that are already running.

Selling it ​

StoreHow the key reaches the server
BuiltByBit, Voxel ShopThe %%__SUNLICENSE__%% placeholder is filled in on download
SpigotMC, Tebex, Gumroad and the other storesSunLicense issues a key per purchase; the buyer gets it by email or on the redeem page and puts it in config.yml

BuiltByBit allows license checks in resources; say in your listing that the plugin checks its license online and keeps running when SunLicense can't be reached (FAQ).

Going further ​

Send feedback

Found something broken, or have an idea? It goes straight to our board.

The first line becomes the title.
Images (optional, up to 5: paste, drop or capture)
Check each image before sending, and remove any that show something private.

We also receive this page's address and your browser version.

Ctrl Enter to send