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.
// 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
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.
Add the SDK and relocate it with Shadow, so two plugins with different SDK versions never clash on a server:
kotlindependencies { implementation("com.sunlicense:sunlicense:0.0.1") } tasks.shadowJar { relocate("com.sunlicense", "com.example.myplugin.libs.sunlicense") }Keep the API key out of git. Put it in your user Gradle properties (
~/.gradle/gradle.properties:sunlicenseApiKey=slk_…) and letprocessResourceswrite it into a properties file in the jar, as the template does.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.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'sSunLicenseProtection.machineId()adds the MAC addresses. Don't use the IP address. Choosing a machine id.javastatic 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"; } }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.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.Re-check every few hours (
runTaskTimerAsynchronously), so revoking a leaked key reaches servers that are already running.
Selling it
| Store | How the key reaches the server |
|---|---|
| BuiltByBit, Voxel Shop | The %%__SUNLICENSE__%% placeholder is filled in on download |
| SpigotMC, Tebex, Gumroad and the other stores | SunLicense 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
- Protection: seal your features so a copy with the check deleted has nothing to run, and get reports of modified jars.
- Code obfuscation for the jar you sell.
- Exclusions for your own test servers, so they don't use up slots.
- Selling a mod or a proxy plugin? See Fabric and Velocity. More on licensing Minecraft plugins.
