No licenses yet
Create your first license to get started with the system.
| Key | Owner | Product | Status | Type | Expires | Created | |
|---|
No products yet
Create your first product — this represents a plugin or software you want to license.
| ID | Name | Description | Licenses | Created | |
|---|
No customers yet
Customers are created automatically when you assign a license to a Discord ID.
| Discord ID | Name | Licenses | Products |
|---|
No blacklist entries
Add IP addresses or country codes to block them from verifying licenses.
| Type | Value | Product | Reason | Created | |
|---|
No webhooks yet
Webhooks send real-time HTTP notifications to your server when events occur.
| URL | Events | Status | Last Triggered | Success | Failed | |
|---|
Create Your Team
Give your team a name to get started. You can always rename it later from team settings.
Team
No team members yet
Invite members to manage your licence system together.
| Username | Discord ID | Email | Role | Products | Licences | Joined | |
|---|
Documentation
Ultimate Licences API
Integrate licence verification into your Minecraft plugin. Key-based access control, real-time webhooks, and IP blacklisting.
REST API
HMAC Webhooks
Heartbeat
Overview
Ultimate Licences provides key-based access control, server tracking via heartbeats, IP/country blacklisting, and real-time webhook notifications for Minecraft plugins.
Licence Keys25-char keys, permanent or trial with configurable expiry
HeartbeatTrack active servers and player counts
BlacklistBlock IPs or entire countries from verifying
WebhooksHMAC-SHA256 signed callbacks for events
Quick Start
Protect your plugin in 5 steps.
RegisterCreate an account on the dashboard
Create a ProductProducts page → Add Product. This is your plugin ID
Generate a KeyCreate License page. Enter Discord ID, select product, pick duration
Integrate APICopy the Java example into your Minecraft plugin
Ship ItPlugin verifies on startup, sends heartbeats while running
Verify Licence
Core endpoint. Plugin calls this on startup to validate a licence.
POST /api/licence/verify
Public — no auth needed. Called from your plugin.
Request
JSON Body
{
"key": "ABCDE-12345-FGHIJ-67890-KLMNO",
"product": "MY_PRODUCT",
"ip": "192.168.1.100",
"username": "PlayerName",
"world": "world",
"country": "United States",
"countryCode": "US",
"lat": 40.7128,
"lng": -74.006,
"hwid": "a1b2c3d4e5f6"
}
keyrequiredThe licence key to verify
productrequiredProduct ID (case-insensitive)
iprequiredServer IP address
usernameoptionalPlayer/user name shown in recent activity
worldoptionalMinecraft world/server world where the activity happened
countryoptionalCountry name for the world map
countryCodeoptionalISO country code, e.g. US, GB, DE
latoptionalLatitude for map dot placement
lngoptionalLongitude for map dot placement
hwidoptionalHardware ID (reserved)
Response (Valid)
200 OKJSON
{
"valid": true,
"status": "ACTIVE",
"ownerId": "123456789012345678",
"ownerName": "PlayerName",
"product": "MY_PRODUCT",
"type": "permanent",
"expiresAt": null,
"maxServers": 1,
"blacklisted": false
}
Response (Invalid)
200 OKJSON
{
"valid": false,
"status": "EXPIRED",
"reason": "Licence has expired"
}
Heartbeat
Plugins call this periodically to confirm they're still running.
POST /api/licence/heartbeat
Request Body
{
"key": "ABCDE-12345-FGHIJ-67890-KLMNO",
"product": "MY_PRODUCT",
"ip": "192.168.1.100",
"username": "PlayerName",
"world": "world",
"country": "United States",
"countryCode": "US",
"lat": 40.7128,
"lng": -74.006,
"onlinePlayers": 42
}
200 OK
{
"valid": true,
"nextHeartbeat": 300
}
Tip: nextHeartbeat is seconds until the next heartbeat. Currently 300 (5 min).
Error Codes
| Code | Error | Description |
400 | Bad Request | Missing/invalid params |
401 | Unauthorized | Invalid/expired session |
404 | Not Found | Resource not found |
409 | Conflict | Duplicate resource |
Licence Status Values
| Status | Description |
| ACTIVE | Valid and active |
| EXPIRED | Trial past expiry |
| REVOKED | Manually revoked |
| PENDING | Not activated |
| BLACKLISTED | IP/country blocked |
| NOT_FOUND | Key doesn't exist |
| PRODUCT_MISMATCH | Wrong product |
Plugin Integration Guide
1
ConfigureAdd API URL, product ID, licence key to config.yml
2
Verify on EnableCall POST /api/licence/verify in onEnable()
3
Heartbeat LoopSchedule periodic heartbeats via Bukkit async timer
4
Grace PeriodDon't disable on first API timeout
5
Log ResultsLog verification status for debugging
Important: Run all API calls off the main thread to avoid freezing the server.
config.yml
config.yml
licence-api-url: "https://your-domain.com"
licence-product: "MY_PRODUCT"
licence-key: "YOUR-LICENCE-KEY-HERE"
licence-check-interval: 300
Java Example (Spigot / Paper)
Complete working example. Copy and adapt to your project.
LicenceManager.javaJava
public class LicenceManager {
private final JavaPlugin plugin;
private final String apiUrl, product, key, ip, username, world;
private int heartbeatTask = -1;
private final Gson gson = new Gson();
public LicenceManager(JavaPlugin plugin, String url,
String product, String key) {
this.plugin = plugin; this.apiUrl = url;
this.product = product; this.key = key;
this.username = Bukkit.getServerName();
this.world = "world";
String ip = "unknown";
try { ip = Bukkit.getIp().isEmpty()
? InetAddress.getLocalHost().getHostAddress()
: Bukkit.getIp();
} catch (Exception ignored) {}
this.ip = ip;
}
public boolean verify() {
try {
JsonObject body = new JsonObject();
body.addProperty("key", key);
body.addProperty("product", product);
body.addProperty("ip", ip);
body.addProperty("username", username);
body.addProperty("world", world);
JsonObject r = post(apiUrl+"/api/licence/verify", body);
if (!r.get("valid").getAsBoolean()) {
plugin.getLogger().warning(
"Licence failed: "+r.get("reason").getAsString());
return false;
}
plugin.getLogger().info("Licence OK - Owner: "
+ r.get("ownerName").getAsString());
return true;
} catch (Exception e) {
plugin.getLogger().log(Level.WARNING, "API unreachable", e);
return false;
}
}
public void startHeartbeat(int secs) {
stopHeartbeat();
heartbeatTask = Bukkit.getScheduler()
.runTaskTimerAsynchronously(plugin, () -> {
try {
JsonObject b = new JsonObject();
b.addProperty("key", key);
b.addProperty("product", product);
b.addProperty("ip", ip);
b.addProperty("username", username);
b.addProperty("world", world);
b.addProperty("onlinePlayers",
Bukkit.getOnlinePlayers().size());
JsonObject r = post(
apiUrl+"/api/licence/heartbeat", b);
if (!r.get("valid").getAsBoolean())
Bukkit.getScheduler().runTask(plugin,
() -> Bukkit.getPluginManager()
.disablePlugin(plugin));
} catch (Exception ignored) {}
}, secs*20L, secs*20L).getTaskId();
}
public void stopHeartbeat() {
if (heartbeatTask != -1) {
Bukkit.getScheduler().cancelTask(heartbeatTask);
heartbeatTask = -1;
}
}
private JsonObject post(String url, JsonObject body)
throws IOException {
HttpURLConnection c = (HttpURLConnection)
new URL(url).openConnection();
c.setRequestMethod("POST");
c.setRequestProperty("Content-Type","application/json");
c.setConnectTimeout(10000);
c.setReadTimeout(10000);
c.setDoOutput(true);
try (var os = c.getOutputStream()) {
os.write(gson.toJson(body).getBytes(UTF_8));
}
var is = c.getResponseCode() >= 400
? c.getErrorStream() : c.getInputStream();
try (var br = new BufferedReader(
new InputStreamReader(is, UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
return JsonParser.parseString(sb.toString())
.getAsJsonObject();
} finally { c.disconnect(); }
}
}
Main.javaJava
private LicenceManager licence;
@Override
public void onEnable() {
saveDefaultConfig();
String url = getConfig().getString("licence-api-url");
String prod = getConfig().getString("licence-product");
String key = getConfig().getString("licence-key");
if (key == null || key.isEmpty()) {
getLogger().severe("Set licence-key in config.yml!");
Bukkit.getPluginManager().disablePlugin(this);
return;
}
licence = new LicenceManager(this, url, prod, key);
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
boolean ok = licence.verify();
Bukkit.getScheduler().runTask(this, () -> {
if (!ok) {
Bukkit.getPluginManager().disablePlugin(this);
return;
}
licence.startHeartbeat(
getConfig().getInt("licence-check-interval", 300));
getLogger().info("Plugin enabled with valid licence");
});
});
}
@Override
public void onDisable() {
if (licence != null) licence.stopHeartbeat();
}
Blacklist System
Block servers or countries from verifying licences. Runs automatically during verify.
| Type | Blocks | Example |
| IP | Specific server | 192.168.1.100 |
| Country | Entire country | US, DE |
Webhooks
Real-time HTTP callbacks for dashboard events. Configure in the Webhooks page.
Events
| Event | Triggered When |
licence.create | Licence created |
licence.delete | Licence deleted |
licence.verify | Plugin verifies |
licence.heartbeat | Plugin heartbeat |
blacklist.add | Blacklist added |
blacklist.delete | Blacklist removed |
* | All events |
Payload
POST Body
{
"event": "licence.create",
"timestamp": 1714024800000,
"data": {
"licence": { "key": "ABCDE-...", "product": "MY_PRODUCT" },
"owner": "123456789012345678"
}
}
Headers
| Header | Description |
X-Webhook-Signature | HMAC-SHA256 of body using your secret |
X-Webhook-Event | Event name |
X-Webhook-ID | Webhook unique ID |
Verify Signature (Node.js)
verify.jsJavaScript
const crypto = require('crypto');
function verifyWebhook(body, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}