---
url: https://docs.sunlicense.com/how-to/paper-plugin.md
description: >-
  How to add license keys to a Paper (Minecraft) plugin - check the buyer's key
  with the SunLicense Java SDK off the main thread at start-up, disable the
  plugin on a rejection, keep running offline, and sell it on BuiltByBit,
  SpigotMC or Tebex.
---

# 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](https://github.com/sunlicense-com/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](/guide/paper-plugin).

**Or let your AI agent do it:** paste the [setup prompt](https://sunlicense.com/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](https://sunlicense.com/start?ref=docs-howto-paper).

## Step by step

1. **In the dashboard** ([app.sunlicense.com](https://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](/integrations/builtbybit)
   or [Voxel Shop](/integrations/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](/guide/sdks#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](/guide/concepts#validation-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](/guide/offline-licenses) 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

| Store | How the key reaches the server |
|---|---|
| [BuiltByBit](/integrations/builtbybit), [Voxel Shop](/integrations/voxel-shop) | The `%%__SUNLICENSE__%%` placeholder is filled in on download |
| [SpigotMC](/integrations/spigotmc), [Tebex](/integrations/tebex), [Gumroad](/integrations/gumroad) and the [other stores](/integrations/) | 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](/faq#offline)).

## Going further

* [Protection](/guide/protection): seal your features so a copy with the check deleted has nothing to run, and get
  reports of modified jars.
* [Code obfuscation](/guide/obfuscation) for the jar you sell.
* [Exclusions](/guide/access-rules) for your own test servers, so they don't use up slots.
* Selling a mod or a proxy plugin? See [Fabric](/how-to/fabric-mod) and [Velocity](/how-to/velocity-plugin).
  More on [licensing Minecraft plugins](https://sunlicense.com/for/minecraft-plugins/).
